Python中执行并发任务有三种方式:多进程、多线程和协程。这三种方式各有特点,各自有不同的使用场景。执行并发任务的目的是为了提高程序运行的效率,但是如果使用不当则可能适得其反。
一、多进程:
多进程的优点是子进程之间数据独立,安全性较好;缺点则是系统资源的占用较大,进程间切换的开销也比较大
Python中实现多进程的有os.fork方法、multiprocess库。其中os.fork只有Linux环境才有;
multiprocess则是跨平台的,multiprocess库是一个专门的多进程库,可以很方便地创建子进程并指定执行任务。
1. 使用multiprocess实现多进程的方式一:实例multiprocess.Process类,并传入相应的执行对象
import os
from multiprocessing import Process
def run(name):
print('子进程id:%s,witn %s'%(os.getpid(),name))
if __name__=='__main__':
print('父进程id:%s'%os.getpid())
p=Process(target=run,args=('test',))
p.start()
执行结果:
父进程id:14164
子进程id:9224,witn test
实例Process类,并传入要处理的函数及对应的函数参数,然后通过调用start方法来启动子进程。
2.用multiprocess实现多进程方式二:继承multiprocess.Process类,并复写其run方法来处理业务。
import os
from multiprocessing import Process
class SubProcess(Process):
def __init__(self, name):
super(SubProcess, self).__init__()
self.name = name
def run(self):
print ('子进程id: %s, with %s' % (os.getpid(), self.name))
if __name__=='__main__':
print('父进程id: %s' % os.getpid())
p = SubProcess('test')
p.start()
执行结果:
父进程id: 5256
子进程id: 4164, with test
############################################################################
可以看到执行的效果是一样的,但这次具体的处理函数被封装到了自定义的类中。这种方式的好处是可以对子进程类进行更多的扩展,例如,提供一些对外的接口方法 。
3.如果需要同时启动多个子进程处理一批任务,那么就可以使用进程池来批量创建子进程。具体代码如下:
import os
import time
import random
from multiprocessing import Pool
def run(name):
print('子进程id:%s,witn %s' % (os.getpid(), name))
time.sleep((random.random()))
if __name__=='__main__':
print('父进程id: %s' % os.getpid())
p=Pool() #创建进程池,它将使用 Pool 类执行提交给它的任务
for i in range(5):
# 并没有启动5个子进程,而是只启动了2个子进程。因为这个进程池中的进程数量默认等于 CPU的数量
p.apply_async(run,args=(i,)) #并发处理目标函数
p.close()
p.join()
print('All done')
执行结果:
父进程id: 5176
子进程id:12756,witn 0
子进程id:13676,witn 1
子进程id:12580,witn 2
子进程id:11996,witn 3
子进程id:12580,witn 4
All done
如果希望启动指定数量的子进程,则可以在创建进程池的时候指定即可。具体实现如下:
p=Pool(5) #启动5个子进程。Pool模块只是帮助启动多个进程,不会保证并发的进程安全
执行结果:
父进程id: 9008
子进程id:11776,witn 0
子进程id:188,witn 1
子进程id:1192,witn 2
子进程id:13288,witn 3
子进程id:14108,witn 4
All done
4.到目前为止,我们已经可以通过多种方式实现多进程。但这只是开始,有了多进程之后还要解决子进程之间的通信、资源竞争等问题。
进程间的通信方式有:共享内存、管道、信号、Socket、外部存储等。 而在多个子进程之间通信,最常用的就是共享队列。
关于多进程之间的资源竞争问题,则需要通过锁机制来解决。具体而言就是通过锁的方式来控制同一个时间内只有一个进程在操作临界资源。有了锁之后,只有拿到锁的进程才能对临界资源进行操作,而其他进程则只能等待。
5.锁的使用代码如下:
from multiprocessing import Process,Lock
def write(f,lock): # 一个多进程并发写文件的场景
lock.acquire()
try:
fs=open(f,'a+')
fs.write('write something\n')
fs.close()
finally:
lock.release()
if __name__=='__main__':
f='test.txt'
lock=Lock()
for i in range(3):
p=Process(target=write,args=(f,lock))
p.start()
#实例Process类,并传入要处理的函数及对应的函数参数,然后通过调用start方法来启动子进程
二、多线程:
相比于多进程,多线程则是多个线程共享一个进程,所以只需申请一份系统资源;并且线程间的上下文切换也更加高效;另外,线程间的通信也变得更加方便。
Python中多线程使用threading模块来实现;该模块下Thread类为线程实例类,Lock为线程锁类。
多线程的使用样例代码:
import threading
n=0
def inc(max):
global n
for i in range(max):
n=n+1
print('%s=>%d'%(threading.current_thread().name,n))
if __name__=='__main__':
for i in range(1):
threading.Thread(target=inc,args=(1000,)).start()
#通过新启动一个线程来执行inc方法
#执行结果:
Thread-1 => 1
Thread-1 => 2
…
Thread-1 => 1000
在执行inc方法时,并没有直接执行而是通过新启动1个线程来执行的。多线程实例方式与多进程类似,实例时需要传递一个待处理的函数,如果有参数则通过args来传递
如果启动5个线程,结果可能并非是5000,其原因是多线程共同操作的共享变量n,在这里属于临界资源;多个线程对它的操作有竞争关系,但在程序里却没有处理资源竞争的问题。
同多进程一样,处理竞争问题时可以使用锁机制。
而多线程使用的锁是‘线程锁’,存放在threading模块中。
在加入线程锁之后原代码更新后的内容如下:
import threading
lock=threading.Lock()
n=0
def inc(max):
global n
for i in range(max):
lock.acquire() #申请锁
try:
n=n+1
print('%s=>%d'%(threading.current_thread().name,n))
finally:
lock.release() #释放锁
if __name__=='__main__':
for i in range(5):
threading.Thread(target=inc,args=(1000,)).start()
#=> …
Thread-1 => 5000
这里每次在循环开始时都会申请锁,接着处理循环体中的内容,而每次循环结束时都会释放掉锁。这样就可以保证每次只有一个线程对共享变量n进行赋值操作。
tip: 使用多线程虽然比多进程更轻量级,但如果使用的是Cython解释器,那么可能需要了解下GIL(全局解释器锁)。GIL对多线程执行有一定的限制和影响,尤其是在多核CPU上。所以如果希望在多核CPU上使用多线程,则要先确保任务属于IO密集型的。
三、协程
协程又称为微线程,是比线程更轻量级的概念。协程通过在单个线程内进行函数执行切换来实现并发。也就是说,协程是‘单线程’执行,并且在线程内‘函数之间’的执行是可以切换的。
如果说多进程、多线程是‘抢占式’的任务处理方式,那么协程则是‘协作式’的任务处理方式。协程虽然是单线程,但是通过‘协作’切换来充分利用CPU,所以也可以实现高并发的场景
协程是通过分割主线程的计算能力来实现的。在单线程的情况下,协程始终都能获得到GIL,所以不会受GIL影响。如果希望通过协程利用多核CPU的计算能力,那么可通过多进程与协程配合的模式来实现。