进程&线程(一)——multiprocessing,threading
本节内容为①进程线程的基础知识;②在Python的实现方法;
学习总结自:
一文看懂Python多进程与多线程编程(工作学习面试必读) - 知乎
1、进程线程基础
什么是进程、线程?
①进程:Process;线程:Thread;
②进程是OS分配资源的最小单元,线程是OS调度的最小单元;
③一个程序至少包括一个进程,一个进程至少包括一个线程;线程的尺度更小
④进程执行过程中拥有独立内存单元,不同进程间的内存单元互不干涉;
一个进程中的多个线程在执行过程中共享内存。
2、进程在Python中的实现
1)多进程编程与multiprocessing模块
Python多进程编程主要依靠multiprocessing模块。为了直观理解多进程的优势,我们可以看以下一个例子:
模拟一个非常耗时的任务,计算8的20次方,为了使这个任务显得更加耗时,我们中途还sleep 2s。第一段代码是单进程,我们按照顺序执行代码,重复计算两次,并打印出总共耗时。
import time import os def long_time_task(): print('当前进程:{}'.format(os.getpid())) time.sleep(2) print('8^20={}'.format(8**20)) if __name__=='__main__': print('当前父进程:{}'.format(os.getpid())) start=time.time() for i in range(2): long_time_task() end=time.time() print('程序耗时{}s'.format(end-start))
输出结果如下:
当前父进程:14956 当前进程:14956 8^20=1152921504606846976 当前进程:14956 8^20=1152921504606846976 程序耗时4.010442018508911s
可以看出来,总共耗时4s,且自始至终只有一个进程14956。说明计算机计算8^20并不耗时。
第二段代码是多进程计算代码,我们利用multiprocessing模块的Process()方法创建了两个新的进程P1与P2进行并行计算。Process方法接收两个参数,第一个是target,一般指向某个函数,表明该进程执行的任务;第二个是args,即需要向函数传递的参数。此外,还有两个方法start()、join();
start()方法之后,进程开始执行;
join()方法用于阻塞父进程,等待子进程结束后继续执行父进程,通常用于进程间的同步。
from multiprocessing import Process import os import time def long_time_task(i): print('当前进程:{} - 任务{}'.format(os.getpid(),i)) time.sleep(2) print('8^20={}'.format(8**20)) if __name__=='__main__': print('当前父进程:{}'.format(os.getpid())) start=time.time() P1=Process(target=long_time_task,args=(1,)) P2=Process(target=long_time_task,args=(2,)) print('等待所有子进程完成。') P1.start() P2.start() P1.join() P2.join() end=time.time() print('总共用时{}s'.format(end-start))
输出结果:
当前父进程:3544 等待所有子进程完成。 当前进程:3968 - 任务2 当前进程:9028 - 任务1 8^20=1152921504606846976 8^20=1152921504606846976 总共用时2.1800246238708496s
耗时2s,时间减少了一半。另外,尽管我们创建了两个进程,但是在实际运行过程中却是一个父进程、2个子进程。这一点怎么看出来呢,可以在join方法之后添加一句打印父进程id的语句,可以看到这一句话并不是立刻打印出来的,而是子进程执行完毕后才继续执行的。说明了这两个进程并不是与父进程并列的,而是父进程的子进程。
父进程在所有子进程的join方法之后继续执行。
知识点
- 进程的创建与切换需要耗费资源,所以平时工作中的进程数不能太多。
- 同时运行的进程数(并行)一般受制于CPU的核数
- 除了使用Process方法创建进程,还可以使用Pool类
2)利用multiprocessing模块的Pool类创建多进程
很多时候系统都需要创建多个进程以提高CPU利用率,当数量较少时,可以手动生成一个个Process实例。当进程数量很多时,可以利用循环,但是这需要程序员手动管理系统中并发进程的数量,有时会很麻烦。这时进程池Pool就可以发挥其功效了。可以通过传递参数来限制并发进程的数量,默认为CPU的核数。
原理
Pool类可以提供指定数量的进程供用户调用,当有新的请求提交到Pool时,如果Pool没满,就会创建一个新的进程来执行请求。如果池满,请求就会告知先等待,直到池中有进程结束,才会创建新的进程来执行这些请求。
方法
apply_async |
向Pool提交需要执行的函数及参数,每提交一项相当于创建了一个待入池的进程。 各进程间异步执行,互不影响,这是默认方式。 |
map |
用法与内置的map函数一致。 会使进程阻塞直到结果返回,即各进程执行顺序同步。 |
map_async | 用法同map,区别在于不会阻塞进程,即各进程异步执行,互不妨碍。 |
close | 关闭Pool,不再接受新任务。 |
terminate | 结束工作进程,不再处理未处理的任务 |
join | 阻塞主进程。join方法必须要在close和terminate之后使用 |
例子
笔者CPU是8核的,所以一次最多可以同时运行8个进程,所以我开启了一个容量为4的进程池。
8个进程需要计算9次,所以可以想像过程:8个进程并行执行8次计算任务后,还剩一次计算任务没有完成,系统会等待8个进程完成后(而不是完成一个之后)重新安排一个进程来计算。
from multiprocessing import Pool,cpu_count import os import time def long_time_task(i): print('当前子进程:',os.getpid()) time.sleep(2) print('8^20=%s\n'%(8**20)) if __name__=='__main__': print('当前父进程:',os.getpid()) print('CPU核数:',cpu_count()) start=time.time() p=Pool(cpu_count()) for i in range(cpu_count()+1): p.apply_async(long_time_task,args=(i,)) print('所有子进程运行完毕') p.close() p.join() end=time.time() print('总共用时:',end-start)
输出结果如下:
当前父进程: 10808 CPU核数: 8 所有子进程运行完毕 当前子进程: 8424 当前子进程: 10756 当前子进程: 9464 当前子进程: 8532 当前子进程: 3172 当前子进程: 5268 当前子进程: 4568 当前子进程: 9604 8^20=1152921504606846976 当前子进程: 8424 8^20=1152921504606846976 8^20=11529215046068469768^20=1152921504606846976 8^20=11529215046068469768^20=1152921504606846976 8^20=1152921504606846976 8^20=1152921504606846976 8^20=1152921504606846976 总共用时: 4.21990966796875
由于9个进程并发执行了两轮,所以总用时只用时了4s,而不是2*9=18s。
知识点
- 对Pool对象调用join方法会等待所有子进程执行完毕,之后才会执行主进程(这一点和之前用Process创建单个子进程时的用法相同);
- 调用join之前必须先调用close或terminate方法,让其不再接受新的Process;
- 常用for循环加apply_async方法,往Pool中添加Process;
- Python解释器中存在GIL(全局解释器锁),其作用是保证同一时刻只有一个线程可以执行代码。由于GIL的存在,Python中的多线程并不是实际的多线程,如果想要充分地使用多核CPU的资源,在Python中大部分情况需要使用多进程。但这并不意味着Python多线程编程没有意义,关于多线程的部分可以看第3节。
3)多进程间的数据共享与通信
通常,进程之间是相互独立的,每个进程都有独立的内存。通过共享内存(nmap模块),进程之间可以共享对象,使多个进程可以访问同一个变量(地址相同,变量名可以不同)。多进程共享资源必然会导致进程之间的相互竞争,所以应尽最大可能防止使用共享状态。还有一种方式是使用Queue来实现不同进程间的通信或数据共享,这点和多线程编程类似。
例子
下面的代码中创建了两个独立进程,一个负责写(pw),另一个负责读(pr),实现了共享一个队列Queue:
from multiprocessing import Process,Queue import os , time , random #写数据 def write(q): print('Process to write:{}'.format(os.getpid())) for value in ['A','B','C']: print('Put %s to queue...'%value) q.put(value) time.sleep(random.random()) #读数据 def read(q): print('Process to read:{}'.format(os.getpid())) while True: value=q.get() print('Get %s from queue.'%value) if __name__=='__main__': #父进程创建Queue,并传给各个子进程: q=Queue() pw=Process(target=write,args=(q,)) pr=Process(target=read,args=(q,)) pw.start() pr.start() #等待pw结束 pw.join() # pr进程中是死循环,无法等待其结束,只能强行终止 pr.terminate()
运行结果:
Process to write:1720 Put A to queue... Process to read:9500 Get A from queue. Put B to queue... Get B from queue. Put C to queue... Get C from queue.
知识点
- 上文代码中,在主进程中创建Queue,作为参数q传入子进程;
- 在子进程中进行出队入队——写入数据时入队,读取数据时出队;入队——q.put(value);出队——value=q.get();
- 子进程的开始,依然是用方法start()
- 如果进程可能无法结束,就要用terminate方法,而不是join等待
3、多线程编程与threading模块
创建新线程与创建新进程的方式类似。threading.Thread方法接收两个参数:target——线程执行函数;args——向函数传递的参数。对新创建的线程,用start()方法让其开始,join()方法阻塞主线程。我们还可以使用current_thread().name方法打印出当前线程的名字。
例子
还是之前的例子,计算8^20,并等待2s。这里使用线程threading.Thread实现
import threading import time def long_time_task(i): print('当前子线程:{}——任务:{}'.format(threading.current_thread().name,i)) time.sleep(2) print('8^20={}'.format(8**20)) if __name__=='__main__': start=time.time() print('主线程:{}'.format(threading.current_thread().name)) t1=threading.Thread(target=long_time_task,args=(1,)) t2=threading.Thread(target=long_time_task,args=(2,)) t1.start() t2.start() t1.join() t2.join() end=time.time() print('程序运行时间:%ss'%(end-start))
结果:
主线程:MainThread 当前子线程:Thread-3——任务:1 当前子线程:Thread-4——任务:2 8^20=11529215046068469768^20=1152921504606846976 程序运行时间:2.01711368560791s
1)主线程、子线程同步
当我们设置多线程时,主线程会创建多个子线程,在Python中,默认情况下主、子线程异步运行互不干涉。如果需要主线程等待子线程实现线程的同步,需要join方法,这点和多进程倒是类似,如果我们主线程结束时不再执行子线程,我们可以使用Thread.setDaemon(True),代码示例如下:
import threading import time def long_time_task(): print('当子线程: {}'.format(threading.current_thread().name)) time.sleep(2) print("结果: {}".format(8 ** 20)) if __name__=='__main__': start = time.time() print('这是主线程:{}'.format(threading.current_thread().name)) for i in range(5): t = threading.Thread(target=long_time_task, args=()) t.setDaemon(True) t.start() end = time.time() print("总共用时{}秒".format((end - start)))
setDaemon(True):设置该线程为守护线程,表明该线程是不重要的,进程退出时不需要等待这个线程执行完毕。
这样做的意义在于:避免子线程死循环,导致退不出程序。
2)通过继承Thread类重写run方法创建新线程
除了使用Thread()方法创建新的线程外,还以通过继承Thread类,重写run方法创建新的线程,这种方法更加灵活。
例子
自定义Thread类MyThread,重写run方法。通过该类的实例化创建2个子线程:
import threading import time def long_time_task(i): print('当前子线程{}——任务{}'.format(threading.current_thread().name,i)) time.sleep(2) print('8^20=%s,%d'%(8**20,i)) class MyThread(threading.Thread): def __init__(self,func,args,name=''): threading.Thread.__init__(self) self.func=func self.args=args self.name=name self.result=None def run(self): self.func(self.args[0]) if __name__=='__main__': start=time.time() threads=[] for i in range(1,3): t=MyThread(long_time_task,(i,),str(i)) threads.append(t) for t in threads: t.start() for t in threads: t.join() print('结束子进程',t.name) end=time.time() print('总用时%ss'%(end-start))
输出结果如下:
当前子线程1——任务1 当前子线程2——任务2 8^20=1152921504606846976,2 8^20=1152921504606846976,1 结束子进程 1 结束子进程 2 总用时2.020406723022461s
3)不同线程间的数据共享
①加锁lock
一个进程中的不同线程间共享内存,这就意味着任何一个变量都可以被任何一个线程修改,因此线程之间共享数据的最大危险在于多线程同时修改变量,把内容改乱了。如果不同线程间有共享的变量,其中一个方法就是在修改之前给其加锁lock,确保一次只有一个线程能够修改它。
threading.Lock()方法可以实现对一个共享变量的锁定,修改完后释放供其它线程使用。
例子
模拟存取钱,其中的账户余额balance是一个共享变量,使用lock可以使其避免错误改动:
import threading class Account: def __init__(self): self.balance=0 #存取时第一步就是加锁 #最后一步就是释放锁 def save(self,lock): lock.acquire() for i in range(100000): self.balance+=1 lock.release() def load(self,lock): lock.acquire() for i in range(100000): self.balance-=1 lock.release() if __name__=='__main__': account=Account() lock=threading.Lock() thread_save=threading.Thread(target=account.save,args=(lock,),name='Save') thread_load=threading.Thread(target=account.load,args=(lock,),name='Load') thread_save.start() thread_load.start() thread_save.join() thread_load.join() print('The final balance is ',account.balance)
存取函数的第一步就是加锁,最后一步是释放锁。
结果:
The final balance is 0
②queue队列
另一种实现不同线程间数据共享的方法就是使用消息队列queue。
例子——生产者、消费者模型
下边的代码创建了两个线程,一个负责生产,另一个负责消费,所生成的产品放在Queue中,实现不同线程间沟通。
from queue import Queue import random,threading,time class Producer(threading.Thread): def __init__(self,name,queue): threading.Thread.__init__(self,name=name) self.queue=queue def run(self): for i in range(1,5): print('{} is producing {} to the queue!'.format(self.getName(),i)) self.queue.put(i) time.sleep(random.randrange(10)/5) print('%s finished!'%self.getName()) class Consumer(threading.Thread): def __init__(self,name,queue): threading.Thread.__init__(self,name=name) self.queue=queue def run(self): for i in range(1,5): val=self.queue.get() print('{} is consuming {} in the queue.'.format(self.getName(),val)) time.sleep(random.randrange(10)) print('%s finished!'%self.getName()) if __name__=='__main__': queue=Queue() producer=Producer('Producer',queue) consumer=Consumer('Consumer',queue) producer.start() consumer.start() producer.join() consumer.join() print('All threads finished!')
队列Queue的put方法可以将一个对象放入队列中。如果队列已满,此方法将阻塞,直至Queue有空间可用为止。
Queue的get方法一次移除并返回队列中的一个成员。如果队列为空,此方法将阻塞,直至Queue中有成员可用为止。
此外,Queue同时还自带empty、full方法来判断一个队列是否为空或满,但这些方法并不可靠,因为多线程与多进程,在返回结果与使用结果之间,队列中可能添加/删除了成员。
4)多进程与多线程的使用场景
对于CPU密集型代码(比如循环计算)——多进程效率更高
对于IO密集型代码(如文件操作、爬虫)——多线程效率更高
对此的理解:
对IO密集型操作,大部分消耗的时间其实是等待时间,在等待时间中CPU是不需要工作的,因此在此期间即使提供更多的CPU资源也是用不上的。
对CPU密集型代码,两个CPU干活肯定比一个CPU快很多。
那么为什么多线程会对IO密集型代码有用呢?这是因为Python碰到等待时会释放GIL提供给新的线程使用,实现了线程间的切换。
4、常用方法与语句
os.getpid():当前进程的id
time.sleep(2):睡眠2s
from multiprocessing import cpu_count:cpu_count()获取CPU核数
threading.current_thread().name:当前线程名
Thread.setDaemon(True):守护线程
5、总结
1)多进程
利用Process产生单个进程
from multiprocessing import Process def func(args): ...#每个进程所执行的任务 #利用Process创建单个进程 if __name__=='__main__': P1=Process(target=func,args=(x,)) #将参数x传入任务函数,构成一个进程 P2=Process(target=func,args=(y,)) #将参数y传入任务函数,构成另一个进程 P1.start() #启动进程1 P2.start() #启动进程2 P1.join() #阻塞主进程 P2.join() #阻塞主进程
#利用Pool产生多个进程
利用Pool产生进程池
from multiprocessing import Pool,cpu_count def func(args): #进程执行的方法 ... if __name__=='__main__': p=Pool(n)#池大小,即一次最多并行运行的进程数 for i in range(m):#一共m个任务,将它们加入进程池中 p.apply_async(func,args=(xm,))#xm是每个进程对应的传入变量 p.close() p.join()
不同进程之间的运行是并行的,所以会大幅减少运算时间。
进程间的数据共享与通信:Queue
from multiprocessing import Process,Queue def funcW(q):#存函数,把数据存入队列q,把q作为参数传入函数 ... for x in X: q.put(x)#将一系列x存入队列 def funcR(q):#取函数,用于从队列q中取数据,把q作为参数传入函数 ... while True:#由于不确定q中数据数量,所以要用无限循环的方式取数据 v=q.get() if __name__=='__main__': q=Queue() pw=Process(target=funcW,args=(q,)) pr=Process(target=funcR,args=(q,)) pw.start() pr.start() pw.join() #当进程中有死循环时,用方法terminate进行手动终结, #时间在pw进程运行完毕之后(即pw的join方法之后) pr.terminate()
2)多线程
利用Thread创建多线程,用法与Process相同
from threading import Thread def func(arg): #线程执行的函数 ... if __name__=='__main__': t1=Thread(target=func,args=(x,)) t2=Thread(target=func,args=(y,)) t1.start() t2.start() t1.join() t2.join()
通过继承Thread类重写run方法创建新线程
from threading import Thread def func(args): #线程执行函数 ... #自定义线程类 class MyThread(Thread): def __init__(self,func,args,name=''): Thread.__init__(self) self.func=func self.args=args self.name=name #这里的name就是之前current_thread().name的内容 def run(self): self.func(self.args[0]) if __name__=='__main__': threads=[] #通过for循环构建多线程 for i in range(n): t=MyThread(func,(x,),name)#调用函数,传入参数,线程名 threads.append(t) for t in threads: t.start() for t in threads: t.join()
不同线程间的数据共享
Lock
from threading import Lock,Thread def funcW(sum,lock): #写函数 lock.acquire()#加锁 ... lock.release()#解锁 def funcR(sum,lock): #读函数 lock.acquire()#加锁 ... lock.release()#解锁 if __name__=='__main__': sum=0 lock=Lock() thread_W=Thread(target=funcW,args=(sum,lock),name='Save') thread_R=Thread(target=funcR,args=(sum,lock),name='Read') thread_W.start() thread_R.start() thread_W.join() thread_R.join()
Queue
from queue import Queue from threading import Thread #读写类中的queue实际上是同一个queue class Producer(Thread): def __init__(self,name,queue): Thread.__init__(self,name=name) self.queue=queue def run(self): for x in range(X): #... self.queue.put(x)#往queue中写数据 #... class Consumer(Thread): def __init__(self,name,queue): Thread.__init__(self,name=name) self.queue=queue def run(self): while True: ... val=self.queue.get() ... if __name__=='__main__': queue=Queue()#将队列传入读写类中 producer=Producer('Producer',queue) consumer=Consumer('Consumer',queue) producer.start() consumer.start() producer.join() consumer.terminate()
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性