本节内容为①进程线程的基础知识;②在Python的实现方法;

学习总结自:

一文看懂Python多进程与多线程编程(工作学习面试必读) - 知乎

multiprocessing 官方文档

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的更多相关文章

  1. 多进程---multiprocessing/threading/

    一.多进程:multiprocessing模块 多用于处理CPU密集型任务 多线程 多用于IO密集型任务 Input Ouput 举例: import multiprocessing,threadin ...

  2. python 进程池(multiprocessing.Pool)和线程池(threadpool.ThreadPool)的区别与实例

    一般我们是通过动态创建子进程(或子线程)来实现并发服务器的,但是会存在这样一些缺点: 1.动态创建进程(或线程)比较耗费时间,这将导致较慢的服务器响应.  2.动态创建的子进程通常只用来为一个客户服务 ...

  3. python学习笔记-进程线程

    1.什么是进程(process)? 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程.程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述 ...

  4. python进阶------进程线程(三)

    python中的进程 1.multiprocessing模块 由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进 ...

  5. python 进程 线程

    进程 线程 操作系统 为什么要有操作系统? 操作系统:操作系统是一个用来协调,管理和控制计算机硬件和软件资源的系统程序.位于底层硬件与应用软件之间 工作方式:向下管理硬件 向上提供接口 切换 1.出现 ...

  6. python基础(16)-进程&线程&协程

    进程之multiprocessing模块 Process(进程) Process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建. 介绍 初始化参数 Process([group [, t ...

  7. 进程&线程&协程

    进程  一.基本概念 进程是系统资源分配的最小单位, 程序隔离的边界系统由一个个进程(程序)组成.一般情况下,包括文本区域(text region).数据区域(data region)和堆栈(stac ...

  8. python -- 进程线程协程专题

    进程专栏 multiprocessing 高级模块 要让Python程序实现多进程(multiprocessing),我们先了解操作系统的相关知识. Unix/Linux操作系统提供了一个fork() ...

  9. 并发编程---线程---开启方式---进程线程的区别----Thread的其他属性

    线程 进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合)而线程才是cpu上的执行单位 1.同一个进程内的多个线程共享该进程内的地址资源 2.创建线程的开销远小于创建进程的开销(创建一 ...

随机推荐

  1. py调用shell

    py调用shell

  2. 免密码提交gitlab

    在你的用户目录下新建一个文本文件.git-credentials echo 'https://henry:123456@ggithub.com' > /root/.git-credentials ...

  3. 笔记:Bridging the Gap Between Relevance Matching and Semantic Matching for Short Text Similarity Modeling

    笔记:Bridging the Gap Between Relevance Matching and Semantic Matching for Short Text Similarity Model ...

  4. Kubernetes常见的部署方案(十四)

    一.常见的部署方案 滚动更新 服务不会停止,但是整个pod会有新旧并存的情况. 重新创建 先停止旧的pod,然后再创建新的pod,这个过程服务是会间断的. 蓝绿 (无需停机,风险较小) 部署v1的应用 ...

  5. SQL语句 order by 升序和 降序查询

    原文 https://blog.csdn.net/u010649766/article/details/76180523?utm_medium=distribute.pc_relevant_t0.no ...

  6. 从我做起[AutoMapper实现模块化注册自定义扩展MapTo<>()].Net Core 之二

    AutoMapper实现模块化注册自定义扩展MapTo<>() 我们都知道AutoMapper是使用的最多的实体模型映射,如果没有AutoMapper做对象映射那么我们需要想一下是怎么写的 ...

  7. JAVA多线程学习十一-线程锁技术

    前面我们讲到了synchronized:那么这节就来将lock的功效. 一.locks相关类 锁相关的类都在包java.util.concurrent.locks下,有以下类和接口: |---Abst ...

  8. div置顶

    转载请注明来源:https://www.cnblogs.com/hookjc/ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transit ...

  9. 通过Python自带模块SimpleHTTPServer快速共享服务的配置文件

    简介 SimpleHTTPServer是Python 2自带的一个模块,是Python的Web服务器,简单小巧,快速启动. 它在Python 3已经合并到http.server模块中. SimpleH ...

  10. 有序取出Map集合的元素

    最近写到一个程序,返回了map,但是经过查阅资料,map是没有顺序的,各种查阅资料无果,最后自己写了这个方法.. 1,通过map集合的keySet()方法,获取到一个包含map所有key的Set集合 ...