进程&线程(一)——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()
进程&线程(一)——multiprocessing,threading的更多相关文章
- 多进程---multiprocessing/threading/
一.多进程:multiprocessing模块 多用于处理CPU密集型任务 多线程 多用于IO密集型任务 Input Ouput 举例: import multiprocessing,threadin ...
- python 进程池(multiprocessing.Pool)和线程池(threadpool.ThreadPool)的区别与实例
一般我们是通过动态创建子进程(或子线程)来实现并发服务器的,但是会存在这样一些缺点: 1.动态创建进程(或线程)比较耗费时间,这将导致较慢的服务器响应. 2.动态创建的子进程通常只用来为一个客户服务 ...
- python学习笔记-进程线程
1.什么是进程(process)? 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程.程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述 ...
- python进阶------进程线程(三)
python中的进程 1.multiprocessing模块 由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进 ...
- python 进程 线程
进程 线程 操作系统 为什么要有操作系统? 操作系统:操作系统是一个用来协调,管理和控制计算机硬件和软件资源的系统程序.位于底层硬件与应用软件之间 工作方式:向下管理硬件 向上提供接口 切换 1.出现 ...
- python基础(16)-进程&线程&协程
进程之multiprocessing模块 Process(进程) Process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建. 介绍 初始化参数 Process([group [, t ...
- 进程&线程&协程
进程 一.基本概念 进程是系统资源分配的最小单位, 程序隔离的边界系统由一个个进程(程序)组成.一般情况下,包括文本区域(text region).数据区域(data region)和堆栈(stac ...
- python -- 进程线程协程专题
进程专栏 multiprocessing 高级模块 要让Python程序实现多进程(multiprocessing),我们先了解操作系统的相关知识. Unix/Linux操作系统提供了一个fork() ...
- 并发编程---线程---开启方式---进程线程的区别----Thread的其他属性
线程 进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合)而线程才是cpu上的执行单位 1.同一个进程内的多个线程共享该进程内的地址资源 2.创建线程的开销远小于创建进程的开销(创建一 ...
随机推荐
- iptables匹配条件总结1
源地址 -s选项除了指定单个IP,还可以一次指定多个,用"逗号"隔开即可 [root@web-1 ~]# iptables -I INPUT -s 172.16.0.116,172 ...
- 源码分析axios(1)~源码分析、模拟axios的创建
■ 查看源码发现,起初axios[instance=bind(Axios.prototype.request, context);]是一个函数, 但后续[ utils.extend(instance, ...
- WebSphere--WAS概念和原理解析
WebSphere--WAS概念和原理解析--tigergao收录于2021/04/25
- python浮点数计算--5
#!/usr/bin/python #coding=utf-8 i=1.0 j=3 print(i*j) print(i+j) print(i**j) 备注:无论是哪种运算,只要有操作数是浮点数,py ...
- 张高兴的 .NET IoT 入门指南:(七)制作一个气象站
距离上一篇<张高兴的 .NET Core IoT 入门指南>系列博客的发布已经过去 2 年的时间了,2 年的时间 .NET 版本发生了巨大的变化,.NET Core 也已不复存在,因此本系 ...
- Html设置文本换行与不按行操作
图片来源:W3C 部分引自大佬:https://zhidao.baidu.com/question/424920602093167052.html 强制不换行 div{ white-space:now ...
- 3M 高可用架构----拓展
3M 高可用架构 一.MMM 1. MMM的概述 MMM(Master-Master replication manager for MySQL,MySQL主主复制管理器)是一套支持双主故障切换和双主 ...
- 微信小程序音频播放 InnerAudioContext 的用法
今天项目上涉及到了微信小程序播放音频功能,所以今天跟着一些教程做了个简单的播放器 1.实现思路 刚开始想着有没有现成的组件可以直接用,找到了微信的媒体组件 audio,奈何看着 1.6.0版本开始,该 ...
- 浅谈Java中重写与重载的区别
重载和重写是Java中两个截然不同的概念.但是却因为名字相近导致很多人经常混淆. 下面用例子展示出他们之间的区别. 在Java中,重载(overloading) 发生在本类.方法名相同,参数列表不同, ...
- linux_12
一.主从复制及主主复制的实现 主从复制 master配置 # yum -y install mysql-server # systemctl enable --now # vim /etc/my.cn ...