在Python中,进程、线程和协程是处理并发和并行任务的三种主要方式。下面我将对它们进行简要的比较,并提供一些示例代码。
进程(Process)
进程是操作系统进行资源分配和调度的基本单位,它包含独立的内存空间、系统资源以及执行一个或多个线程。
示例:使用multiprocessing模块创建进程
import multiprocessing
def worker_process(num):
"""子进程执行的函数"""
print(f'Worker Process {num} is running')
if __name__ == "__main__":
for i in range(5):
p = multiprocessing.Process(target=worker_process, args=(i,))
p.start()
线程(Thread)
线程是操作系统调度的最小单位,它包含在进程之中,共享进程的资源(如内存空间、文件句柄等),但每个线程有独立的执行栈和线程局部存储。
# 示例:1、使用threading模块创建线程
import threading
def worker_thread(num):
"""子线程执行的函数"""
print(f'Worker Thread {num} is running')
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=worker_thread, args=(i,))
t.start()
# thread.Thread(group=Nore,targt=None,args=(),kwargs={},*,daemon=None)
# 参数解释:
# group:必须为None,于ThreadGroup类相关,一般不使用。
# target:线程调用的对象,就是目标函数。
# name:为线程起这个名字。默认是Tread-x,x是序号,由1开始,第一个创建的线程名字就是Tread-1。
# args:为目标函数传递关键字参数,字典。
# daemon:用来设置线程是否随主线程退出而退出。当daemon设置False时,线程不会随主线程退出而退出,主线程会一直等着子线程执行完;。当daemon设置True时,线程会随主线程退出而退出,主线程结束其他的子线程会强制退出
# 示例2:通过继承threading.Thread类的继承
import threading
class mythread(threading.Thread):
def run(self):
for i in range(1,10):
print(i)
thread1 = mythread();
thread2 = mythread();
thread1.start()
thread2.start()
线程间通信
1. 使用共享数据
虽然 GIL 限制了多线程的并行执行,但它不限制线程之间的数据共享。你可以使用全局变量、类的属性或其他数据结构来在线程间共享数据。但是,需要小心处理数据竞争和条件竞争的问题,通常需要使用同步机制来确保数据的一致性和完整性
import threading # python的锁
class mythread(threading.Thread):
def run(self):
global x #声明一个全局变量
lock.acquire() #上锁
x +=10
print('%s:%d'%(self.name,x))
lock.release() #解锁
x = 0 #设置全局变量初始值
lock = threading.RLock() #创建可重入锁
list1 = []
for i in range(5):
list1.append(mythread()) #创建五个线程,放到同一列表中
for i in list1:
i.start() #开启列表线程
2. 使用 threading 模块中的同步机制
Python 的 threading 模块提供了多种同步机制,如锁(Lock 和 RLock)、条件变量(Condition)、事件(Event)和信号量(Semaphore),这些都可以用于多线程间的通信和同步。
锁(Lock):确保同一时间只有一个线程可以访问某个资源。
条件变量(Condition):允许一个或多个线程等待某个条件的发生,并在条件满足时被通知。
事件(Event):用于在线程间发送信号,一个线程可以等待某个事件的发生,而另一个线程可以触发该事件。
信号量(Semaphore):用于控制同时访问某个资源的线程数量
3. 使用 queue.Queue
Python 的 queue 模块提供了一个线程安全的队列实现,即 queue.Queue。多个线程可以安全地向队列中添加项目,并从队列中移除项目,而无需担心数据竞争或条件竞争的问题。这使得 queue.Queue 成为多线程间通信的一种非常有效的方式
import threading
import queue
def producer(q):
for i in range(5):
item = f'item_{i}'
q.put(item)
print(f'Produced {item}')
def consumer(q):
while True:
item = q.get()
if item is None: # 使用 None 作为结束信号
break
print(f'Consumed {item}')
q.task_done() # 表示之前入队的一个任务已经完成
q = queue.Queue()
t1 = threading.Thread(target=producer, args=(q,))
t2 = threading.Thread(target=consumer, args=(q,))
t1.start()
t2.start()
t1.join() # 等待生产者线程完成
# 发送结束信号
q.put(None)
t2.join() # 等待消费者线程处理完所有项目
协程(Coroutine)
协程是一种用户态的轻量级线程,它的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
在Python中,通常使用async/await语法和asyncio库来实现协程。
# 示例:使用asyncio创建协程
import asyncio
async def worker_coroutine(num):
"""协程执行的函数"""
print(f'Worker Coroutine {num} is running')
await asyncio.sleep(0) # 模拟I/O等待
async def main():
tasks = [worker_coroutine(i) for i in range(5)]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
比较
- 资源占用:进程拥有独立的内存空间,资源占用较大;线程共享进程资源,资源占用较小;协程几乎不占用系统资源,只消耗少量栈空间。
- 通信方式:进程间通信(IPC)通常通过管道、消息队列、共享内存等方式;线程间通信可以通过共享内存直接读写;协程间通信通常通过await表达式隐式进行。
- 切换开销:进程切换需要操作系统内核参与,开销较大;线程切换也需要操作系统内核参与,但开销相对较小;协程切换由用户态程序自行调度,开销极小。
- 并发能力:在Python中,由于全局解释器锁(GIL)的存在,多线程并不能实现真正的并行执行(在CPU密集型任务上);协程则可以在单线程中通过异步I/O实现高并发。
总的来说,进程适用于需要独立资源的场景,线程适用于需要共享资源但不需要并行执行的场景,而协程则适用于需要高并发且主要是I/O密集型任务的场景。