十六、进程与线程
进程与线程
16.1、并发与并行
16.1.1、并发
单个 CPU 处理多个任务。各个任务交替执行一段时间。

16.1.2、并行
多个 CPU 同时执行多个任务。

16.2、多进程
16.2.1、什么是进程
进程是操作系统进行资源分配的基本单位。
操作系统中一个正在运行的程序或软件就是一个进程。
每个进程都有自己独立的一块内存空间。
一个进程崩溃后,在保护模式下不会对其他进程产生影响。
多进程是指在操作系统中同时运行多个程序。
16.2.2、使用 multiprocessing.Process 创建进程
Unix/Linux操作系统提供了一个 os.fork() 系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是 fork() 调用一次,返回两次,因为操作系统自动把当前进程(父进程)复制了一份(子进程),然后,分别在父进程和子进程内返回。
Windows 中没有 fork() 调用,不过Python提供了一个跨平台的多进程模块 multiprocessing。multiprocessing 模块提供了一个 Process 类来代表一个进程对象。
(1)、Process 的创建
multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
参数解析:
-
group: 应当始终为
None,它的存在仅是为了与threading.Thread兼容。 -
target: 由
run()方法来发起调用的可调用对象,默认为None。 -
name: 进程名称,默认为
None则自动分配。 -
args: 针对目标调用的参数元组。
-
kwargs: 针对目标调用的关键字参数字典。
-
daemon: 是否为守护进程,
True或False。默认为None则继承父进程。
(2)、Process 的属性和方法与其他常用方法
-
name: 获取进程名称。
-
pid: 获取进程号。
-
daemon: 判断或设置进程是否为守护进程。
-
exitcode: 获取子进程的退出状态码。
-
start(): 启动进程,调用传入
target的对象。start()只能被调用一次。 -
run(): 默认调用传入
target的对象,如果子类化了Process,可以重写此方法来自定义行为 -
join([timeout]): 阻塞主进程,直到子进程结束或超时。
timeout参数可选,意为阻塞多少秒。 -
terminate(): 强制终止子进程。
-
kill(): 杀死进程,与
terminate()类似,但更彻底。 -
is_alive(): 检查进程是否仍在运行。
-
os.getpid(): 获取当前进程编号。
-
os.getppid(): 获取当前进程的父进程编号。
(3)、案例:同时读写文件
注意:在Windows上执行要加上if __name__ == "__main__"。
import time
import multiprocessing
# 向文件中写入数据
def write_file():
with open("test.txt", "a") as f:
while True:
f.write("hello world\n")
f.flush()
time.sleep(0.5)
# 从文件中读取数据
def read_file():
with open("test.txt", "r") as f:
while True:
time.sleep(0.1)
print(f.read(1))
if __name__ == "__main__":
# 创建一个子进程用于写文件
p1 = multiprocessing.Process(target=write_file)
# 创建一个子进程用于读文件
p2 = multiprocessing.Process(target=read_file)
# 启动子进程
p1.start()
# 启动子进程
p2.start()
16.2.3、自定义 Process 子类创建进程
import os
import multiprocessing
class Worker(multiprocessing.Process):
def run(self):
print("进程id:", os.getpid(), "\t父进程id:", os.getppid())
if __name__ == "__main__":
for i in range(5):
p = Worker()
p.start()
16.2.4、进程池
当需要启动大量子进程时,可以使用进程池。
(1)、进程池的创建
multiprocessing.Pool([processes[,initializer[,initargs[,maxtasksperchild[,context]]]]])
-
processes:要使用的工作进程数量。如果
processes为None则使用os.cpu_count()所返回的数值。 -
initializer:如果不为
None,则每个工作进程将会在启动时调用initializer(*initargs)。 -
maxtasksperchild:一个工作进程在它退出或被一个新的工作进程代替之前能完成的任务数量,为了释放未使用的资源。默认的
maxtasksperchild是None,意味着工作进程与池同寿命。 -
context:可被用于指定启动的工作进程的上下文。通常一个进程池是使用函数
multiprocessing.Pool()或者一个上下文对象的Pool()方法创建的。
注意:进程池对象的方法只有创建它的进程能够调用。
使用时一般只指定 processes 参数。
(2)、进程池的常用方法
-
apply(func[, args[, kwds]]):使用
args参数以及kwds命名参数同步调用func,在返回结果前阻塞。另外func只会在一个进程池中的一个工作进程中执行。 -
apply_async(func[, args[, kwds[, callback[, error_callback]]]]):使用
args参数以及kwds命名参数异步调用func,并立即返回一个AsyncResult对象,不会阻塞。可以通过callback获取结果和通过error_callback处理异常。 -
close():阻止后续任务提交到进程池,当所有任务执行完成后,工作进程会退出。
-
terminate():不必等待未完成的任务,立即停止工作进程。当进程池对象被垃圾回收时,会立即调用
terminate()。 -
join():阻塞主进程,等待工作进程结束。调用
join()前必须先调用close()或者terminate()。
(3)、案例
import os
import time
import multiprocessing
# 打印10个数字,每次间隔0.5秒
def func():
for i in range(10):
print(os.getpid(), i)
time.sleep(0.5)
if __name__ == "__main__":
# 指定进程池大小
process_num = 5
pool = multiprocessing.Pool(process_num)
for p in range(process_num):
# 阻塞式
# pool.apply(func)
# 非阻塞式
pool.apply_async(func)
pool.close()
pool.join()
print("end")
16.3、进程间通信
A、进程间不共享全局变量
子进程向传入的列表中添加元素,最终发现主进程与子进程之间的列表结果不同:
import os
import multiprocessing
# 向list1中添加10个元素
def func(list1):
for i in range(10):
list1.append(i)
print(os.getpid(), list1)
if __name__ == "__main__":
list1 = []
p1 = multiprocessing.Process(target=func, args=(list1,))
p2 = multiprocessing.Process(target=func, args=(list1,))
p1.start()
p2.start()
p1.join()
p2.join()
print(os.getpid(), list1)
B、使用 Queue 通信
Python 的 multiprocessing 模块包含了底层的机制,提供了 Queue、Pipes 等多种方式来交换数据。
multiprocessing.Queue([maxsize]) 返回一个使用一个管道和少量锁和信号量实现的共享队列(先进先出)实例。当一个进程将一个对象放进队列中时,一个写入线程会启动并将对象从缓冲区写入管道中。默认队列是无限大小的,可以通过 maxsize 参数限制。
(1)、Queue 的常用方法
-
qsize(): 返回队列的大致长度。由于多线程或者多进程的上下文,这个数字是不可靠的。
-
empty(): 如果队列是空的返回
True。由于多线程或多进程的环境,该状态是不可靠的。 -
full(): 如果队列是满的返回
True。由于多线程或多进程的环境,该状态是不可靠的。 -
put(obj[, block[, timeout]]): 将
obj放入队列。如果可选参数block是True(默认值)而且timeout是None(默认值),将会阻塞当前进程,直到有空闲的缓冲槽。如果timeout是正数,将会在阻塞了最多timeout秒之后还是没有可用的缓冲槽时抛出queue.Full异常。反之(block是False时),仅当有可用缓冲槽时才放入对象,否则抛出queue.Full异常(在这种情形下timeout参数会被忽略)。 -
put_nowait(obj): 相当于
put(obj, False)。 -
get([block[, timeout]]): 从队列中取出并返回对象。如果可选参数
block是True(默认值)而且timeout是None(默认值),将会阻塞当前进程,直到队列中出现可用的对象。如果timeout是正数,将会在阻塞了最多timeout秒之后还是没有可用的对象时抛出queue.Empty异常。反之(block是False时),仅当有可用对象能够取出时返回,否则抛出queue.Empty异常(在这种情形下timeout参数会被忽略)。 -
get_nowait(): 相当于
get(False)。
(2)、案例:两个进程分别读写 Queue
import time
import random
import multiprocessing
# 间隔随机时间向queue中放入随机数
def func1(queue):
while True:
queue.put(random.randint(1, 50))
time.sleep(random.random())
# 从queue中取出数据
def func2(queue):
while True:
print("=" * queue.get())
if __name__ == "__main__":
queue = multiprocessing.Queue()
p1 = multiprocessing.Process(target=func1, args=(queue,))
p2 = multiprocessing.Process(target=func2, args=(queue,))
p1.start()
p2.start()
p1.join()
p2.join()
注意:multiprocessing.Queue 存在兼容性问题,如果要使用进程池,可以使用 Mananger().Queue
C、进程池之间使用 Manager().Queue 通信
import time
import random
import multiprocessing
# 间隔随机时间向queue中放入随机数
def func1(queue):
while True:
queue.put(random.randint(1, 50))
time.sleep(random.random())
# 从queue中取出数据
def func2(queue):
while True:
print("=" * queue.get())
if __name__ == "__main__":
queue = multiprocessing.Manager().Queue()
pool = multiprocessing.Pool(2)
pool.apply_async(func1, (queue,))
pool.apply_async(func2, (queue
pool.close()
pool.join()
### 16.4、多线程
16.4.1、什么是线程
线程是处理器任务调度和执行的基本单位。
一个进程至少有一个线程,也可以运行多个线程。
多个线程之间可共享数据。
线程运行出错异常后,如果没有捕获,会导致整个进程崩溃。
多线程是指在同一进程中同时执行多个任务。
16.4.2、使用 threading.Thread 创建线程
Python的标准库提供了两个模块:_thread 和 threading,_thread 是低级模块,threading 是高级模块,对 _thread 进行了封装。绝大多数情况下,我们只需要使用 threading 这个高级模块。
(1)、Thread 的创建
threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
-
group: 应为
None,保留给将来实现ThreadGroup类的扩展使用。 -
target: 用于
run()方法调用的可调用对象。默认是None,表示不需要调用任何方法。 -
name: 线程名称。在默认情况下,会以 “Thread-N” 的形式构造唯一名称,其中 N 为一个较小的十进制数值,或是 "Thread-N (target)" 的形式,其中 “target” 为
target.__name__,如果指定了target参数的话。 -
args: 用于发起调用目标函数的参数列表或元组。默认为
()。 -
kwargs: 用于调用目标函数的关键字参数字典。默认是
{}。 -
daemon:
True或False来设置该线程是否为守护模式。如果是None(默认值),线程将继承当前线程的守护模式属性。
(2)、Thread 的属性和方法与其他常用方法
-
name: 线程的名称。
-
daemon: 线程是否为守护线程。
-
ident: 线程标识符。
-
native_id: 此线程的线程 id(tid),由 OS(内核)分配。
-
start(): 启动线程,调用线程的
run()方法。 -
run(): 定义线程的行为,默认调用传入的
target对象。 -
join([timeout=None]): 阻塞主线程,直到当前线程运行完成或达到超时时间。
-
is_alive(): 线程是否在运行。
-
threading.enumerate(): 查看都有哪些线程。
-
threading.current_thread(): 返回当前线程实例。
(3)、案例:两线程分别交替打印
import time
import threading
# 交替打印 00000 和 11111
def func():
flag = 0
while True:
print(threading.current_thread().name, f"{flag}" * 5)
flag = flag ^ 1 # 替换0和1
time.sleep(0.5)
if __name__ == "__main__":
t1 = threading.Thread(target=func, name="线程1")
t2 = threading.Thread(target=func, name="线程2")
t1.start()
t2.start()
16.4.3、自定义 Thread子类 创建线程
import time
import threading
class Worker(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
flag = 0
while True:
print(f"\r{self.name}:{str(flag)*5}", end="")
flag = flag ^ 1 # 替换0和1
time.sleep(0.2)
if __name__ == "__main__":
t1 = Worker("线程1")
t2 = Worker("线程2")
t1.start()
t2.start()
16.4.4、线程池
ThreadPoolExecutor 是 concurrent.futures 模块中的线程池实现,它允许我们轻松地提交任务到线程池,并管理任务的执行和结果。
(1)、线程池的创建
concurrent.futures.ThreadPoolExecutor(max_workers=None, thread_name_prefix="", initializer=None, initargs=())
-
max_workers:线程池的最大线程数(默认取决于系统资源)。 -
thread_name_prefix:线程名称前缀。 -
initializer:可选的初始化函数。 -
initargs:传递给初始化函数的参数。
(2)、线程池的常用方法
-
submit(fn, *args, **kwargs):提交一个任务到线程池,返回一个Future对象。可使用Future.result()获取任务结果。 -
map(func, *iterables, timeout=None, chunksize=1):类似于内置的map()函数,但在线程池中并行执行。iterables为可选传入对象,传递给目标函数。chunksize对ThreadPoolExecutor没有效果。 -
shutdown(wait=True, cancel_futures=False):关闭线程池,等待所有任务完成。 -
wait:表示是否等待线程池中的所有线程完成任务。cancel_futures表示是否取消尚未开始的任务。
(3)、案例,3个线程,每个线程都将字符列表中的每个字符与 1 异或。
import concurrent.futures
def func(tname):
global word
for i, char in enumerate(word):
word[i] = chr(ord(char) ^ 1)
print(f"{tname}: {word}\n", end="")
return word
if __name__ == "__main__":
word = list("idmmn!vnsme")
# 使用 with 语句来确保线程被迅速清理
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
future1 = executor.submit(func, "线程1")
future2 = executor.submit(func, "线程2")
future3 = executor.submit(func, "线程3")
word = future1.result()
word = future2.result()
word = future3.result()
print("".join(word)) # hello world
16.5、互斥锁
(1)、线程安全问题
线程之间共享数据会存在线程安全的问题。
比如下面这段代码,3个线程,每个线程都将g_num +1 十次:
import time
import threading
def func():
global g_num
for _ in range(10):
tmp = g_num + 1
# time.sleep(0.01)
g_num = tmp
print(f"{threading.current_thread().name}: {g_num}\n", end="")
if __name__ == "__main__":
g_num = 0
threads = [threading.Thread(target=func, name=f"线程{i}") for i in range(3)]
[t.start() for t in threads]
[t.join() for t in threads]
print(g_num) # 30
结果为30,看似没有问题,这是因为这个修改操作花费的时间太短了,短到我们无法想象。所以,线程间轮询执行时,都能获取到最新的 g_num 值。因此暴露问题的概率就变得微乎其微。
我们添加0.01秒的延迟时间:
import time
import threading
def func():
global g_num
for _ in range(10):
tmp = g_num + 1
time.sleep(0.01)
g_num = tmp
print(f"{threading.current_thread().name}: {g_num}\n", end="")
if __name__ == "__main__":
g_num = 0
threads = [threading.Thread(target=func, name=f"线程{i}") for i in range(3)]
[t.start() for t in threads]
[t.join() for t in threads]
print(g_num) # 10
可以看到最终结果并不是 30。这是因为在修改 g_num 前,有 0.01 秒 的休眠时间,某个线程延时后,CPU立即分配计算资源给其他线程。此时 0.01 秒 的休眠还未结束,这个线程还未将修改后的数据赋值给 g_num,
因此其他线程获取到的并不是最新值,所以才出现上面的结果。
(2)、互斥锁的概念
某个线程要更改共享数据时,先将其锁定,此时其他线程不能更改。直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。
互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
(3)、互斥锁的使用
可以通过 threading.Lock() 创建互斥锁。
使用 lock.acquire([blocking=True][, timeout=-1]) 来获取锁(blocking 如果为 True,线程会阻塞直到获取到锁。如果为 False,线程立即返回。获取锁成功返回 True,否则返回 False。timeout 为等待的超时时间,单位为秒。如果超时仍未获取到锁,则返回 False。)。
使用 lock.release() 释放锁。
import time
import threading
def func():
global g_num
for _ in range(10):
lock.acquire() # 获取锁
tmp = g_num + 1
time.sleep(0.01)
g_num = tmp
lock.release() # 释放锁
print(f"{threading.current_thread().name}: {g_num}\n", end="")
if __name__ == "__main__":
g_num = 0
lock = threading.Lock() # 创建锁
threads = [threading.Thread(target=func, name=f"线程{i}") for i in range(3)]
[t.start() for t in threads]
[t.join() for t in threads]
print(g_num) # 30

浙公网安备 33010602011771号