线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。 线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。 同样多线程也可以实现并发操作,每个请求分配一个线程来处理。合理地利用进程和线程,可以让程序实现多个进程线程并发执行技术,进而提高程序整体运行处理速度。
本文对Python进程线程编程进行简单的总结:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/f8b7474bc5be89502f0966f9b45b622c.png)
进程
进程(英语:process),是指计算机中已运行的程序
进程的创建
在Unix/Linux下,可以使用fork()
调用实现多进程。本文将使用跨平台的multiprocessing
多进程模块, multiprocessing
模块允许程序员充分利用机器上的多核,使用它可以在Python程序中轻松地创建新的进程。
import multiprocessing as mp
import os
# 子进程要执行的代码
def new_process(name):
print('New process is %s(%s)' % (name, os.getpid()))
if __name__ == '__main__':
process = mp.Process(target=new_process, args=('process1',))
process.start()
print('Main process id is %s' % os.getpid())
这里在主程序使用multiprocessing.Process
实例化了一个进程,其中参数target
传入子进程需要执行的函数,args
传入了函数的参数,注意当以元组形式传入一个参数时需要紧跟,
执行结果如下:
Main process id is 42460
New process is process1(41556)
可以看到主进程中的print
函数先于子进程中的执行,这是因为操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的。有时为了避免线程间随意切换造成的不必要错误,可以在需要的地方加入join
函数
import multiprocessing as mp
import os
# 子进程要执行的代码
def new_process(name):
print('New process is %s(%s)' % (name, os.getpid()))
if __name__ == '__main__':
process = mp.Process(target=new_process, args=('process1',))
process.start()
process.join()
print('Main process id is %s' % os.getpid())
结果如下:
New process is process1(20968)
Main process id is 41048
可以看到程序按照我们预想中的进行,因为在join
函数处会阻塞主进程,直到process
子进程结束。
进程间通信
为了方便进程间的分工合作,数据的交换一定必不可少,multiprocessing
模块提供了Queue
等强大的方式来进行数据交换,以Queue
为例:
import multiprocessing as mp
import random
import time
# 写数据进程执行的代码:
def write(q):
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
# 读数据进程执行的代码:
def read(q):
while True:
value = q.get(True)
print('Get %s from queue.' % value)
if __name__ == '__main__':
q = mp.Queue()
pw = mp.Process(target=write, args=(q,))
pr = mp.Process(target=read, args=(q,))
pw.start()
pr.start()
pw.join()
pr.terminate()
这里使用multiprocessing.Queue()
创建了一个队列,进程间通过args
传入参数可以实现数据共享。值得一提的是,由于函数read()
是死循环,当完成读取操作后需要调用terminate()
函数手动结束进程。
除了队列以外,进程间的通信还可以通过共享内存来进行,共享内存指 (shared memory)在多处理器的计算机系统中,可以被不同中央处理器(CPU)访问的大容量内存,是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。
multiprocessing
共享数据类型有:Value、Array、dict、list、Lock、Semaphore等等,同时还可以共享类的实例对象。具体实例如下:
import multiprocessing as mp
def func1(a,arr):
a.value=3.14
for i in range(len(arr)):
arr[i]=-arr[i]
if __name__ == '__main__':
num=mp.Value('d',1.0)#num=0
arr=mp.Array('i',range(10)) #arr=range(10)
process=mp.Process(target=func1,args=(num,arr))
process.start()
process.join()
print(num.value)
print(arr[:])
输出:
3.14
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
进程池Pool
在程序实际处理问题过程中,忙时会有成千上万的任务需要被执行,闲时可能只有零星任务。那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么?首先,创建进程需要消耗时间,销毁进程也需要消耗时间。第二即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务开启或者结束进程。那么我们要怎么做呢?
首先得介绍一下进程池的概念,定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。
import multiprocessing as mp
def job(x):
return x*x
def multicore():
pool=mp.Pool(processes=3)
res=pool.map(job,range(10))
print(res)
res=pool.apply_async(job,(2,))
print(res.get())
multi_res=[pool.apply_async(job,(i,)) for i in range(10)]
print([res.get() for res in multi_res])
if __name__ == '__main__':
multicore()
进程池Pool内的进程数默认是cpu核数,这里给定参数processes=3
,表示池中使用的最大进程数为3,map()
函数会将第二个参数的需要迭代的列表元素一个个的传入第一个参数我们的函数中,第一个参数是我们需要引用的函数,这里我们看到第一个参数我们自己定义的函数并没有设置形参传值。因为我们的map会自动将数据作为参数传进去。非常方便。
函数apply_async()
对调用进程池Pool中的进程,如果进程池中没有空闲的进程,则需要等待前面某个task完成后才可执行,注意该函数无法传入可迭代类型数据。
map()
或apply_async()
返回值是AsyncResul的实例obj,实例具有以下方法:
obj.get()
:返回结果,如果有必要则等待结果到达。timeout是可选的。如果在指定时间内还没有到达,将引发一场。如果远程操作中引发了异常,它将在调用此方法时再次被引发。obj.ready()
:如果调用完成,返回Trueobj.successful()
:如果调用完成且没有引发异常,返回True,如果在结果就绪之前调用此方法,引发异常obj.wait([timeout])
:等待结果变为可用。obj.terminate()
:立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。如果p被垃圾回收,将自动调用此函数。
对Pool
对象调用join()
方法会等待所有子进程执行完毕,调用join()
之前必须先调用close()
,调用close()
之后就不能继续添加新的Process
了。
线程
进程创建
由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。
python线程的创建使用与进程极其相似,这里简单的提一下:
import threading
import time
def thread_job():
print('T1 start\n')
for i in range(10):
time.sleep(0.1)
print('T1 finish')
def td():
added_thread=threading.Thread(target=thread_job)
added_thread.start()
added_thread.join()
print('all done')
# print(threading.active_count()) #当前运行线程数目
# print(threading.enumerate()) #罗列出当前运行的线程
# print(threading.current_thread()) #当前运行的线程
if __name__ == '__main__':
td()
输出如下:
T1 start
T1 finish
all done
进程间通信
进程锁Lock
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
来看看多个线程同时操作一个变量怎么把内容给改乱了:
import time, threading
# 假定这是你的银行存款:
balance = 0
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
我们定义了一个共享变量balance
,初始值为0
,并且启动两个线程,先存后取,理论上结果应该为0
,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance
的结果就不一定是0
了。
原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:
balance = balance + n
也分为两步:
- 计算
balance + n
,存入临时变量中; - 将临时变量的值赋给
balance
。
这就造成线程在执行这几条语句时,可能中断,从而导致多个线程把同一个对象的内容改乱了。
如果我们要确保balance
计算正确,就要给change_it()
上一把锁,当某个线程开始执行change_it()
时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it()
,只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()
来实现:
balance = 0
lock = threading.Lock()
def run_thread(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
ThreadLocal
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦。
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
import threading
# 创建全局ThreadLocal对象:
local_school = threading.local()
def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
执行结果:
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
这里的local_school
是全局ThreadLocal
对象,每个进程都能使用它,但当线程试图改变其值时,并不会影响其他线程的使用,换句话说,在线程中的local_school
是全局local_school
生成的一个局部副本。
从local_school.student = name
可以理解为全局变量local_school
是一个dict
。
ThreadLocal
最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
分布式进程
Python的multiprocessing
模块不但支持多进程,其中managers
子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers
模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。
原有的Queue
可以继续使用,但是,通过managers
模块把Queue
通过网络暴露出去,就可以让其他机器的进程访问Queue了
首先创建文件master.py,写入代码:
#master.py
from multiprocessing.managers import BaseManager
import queue,random
class QueueManager(BaseManager):
pass
# 发送任务的队列:
task_queue = queue.Queue()
def return_task_queue():
global task_queue
return task_queue
# 接收结果的队列:
result_queue = queue.Queue()
def return_result_queue():
global result_queue
return result_queue
def master():
QueueManager.register('get_task_queue', callable=return_task_queue)
QueueManager.register('get_result_queue', callable=return_result_queue)
# 绑定端口8080, 设置验证码'123':
manager = QueueManager(address=('127.0.0.1', 8080), authkey=b'123')
# 启动Queue:
manager.start()
task = manager.get_task_queue()
result = manager.get_result_queue()
for i in range(10):
n = random.randint(0, 10000)
print('Put task %d...' % n)
task.put(n)
# 从result队列读取结果:
print('Try get results...')
for i in range(10):
r = result.get(timeout=10)
print('Result: %s' % r)
# 关闭:
manager.shutdown()
print('master exit.')
if __name__ == '__main__':
master()
这里使用QueueManager
继承BaseManager
,更加直观易于理解。使用register()
函数写入注册Queue
的名称,callable参数关联了Queue对象。再绑定地址、端口、验证码,验证码相当于其他机器访问注册Queue
的密码。需要注意的是,当我们在一台机器上写多进程程序时,创建的Queue
可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue
不可以直接对原始的task_queue
进行操作,那样就绕过了QueueManager
的封装,必须通过manager.get_task_queue()
获得的Queue
接口添加。
同样,在另一台机器或本机编写worker.py,来实现分布计算:
import time, queue
from multiprocessing.managers import BaseManager
# 创建类似的QueueManager:
class QueueManager(BaseManager):
pass
# 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
# 连接到服务器,也就是运行task_master.py的机器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m = QueueManager(address=(server_addr, 8080), authkey=b'123')
# 从网络连接:
m.connect()
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
for i in range(10):
try:
n = task.get(timeout=1)
print('run task %d * %d...' % (n, n))
r = '%d * %d = %d' % (n, n, n*n)
time.sleep(1)
result.put(r)
except queue.Empty:
print('task queue is empty.')
# 处理结束:
print('worker exit.')
现在,可以试试分布式进程的工作效果了。先启动master.py
服务进程:
Put task 395...
Put task 459...
Put task 7170...
Put task 2367...
Put task 3910...
Put task 7742...
Put task 2651...
Put task 2764...
Put task 6120...
Put task 9652...
Try get results...
master.py
进程发送完任务后,开始等待result
队列的结果。现在启动worker.py
进程:
Connect to server 127.0.0.1...
run task 395 * 395...
run task 459 * 459...
run task 7170 * 7170...
run task 2367 * 2367...
run task 3910 * 3910...
run task 7742 * 7742...
run task 2651 * 2651...
run task 2764 * 2764...
run task 6120 * 6120...
run task 9652 * 9652...
worker exit.
worker.py
进程结束,在master.py
进程中会继续打印出结果:
Result: 395 * 395 = 156025
Result: 459 * 459 = 210681
Result: 7170 * 7170 = 51408900
Result: 2367 * 2367 = 5602689
Result: 3910 * 3910 = 15288100
Result: 7742 * 7742 = 59938564
Result: 2651 * 2651 = 7027801
Result: 2764 * 2764 = 7639696
Result: 6120 * 6120 = 37454400
Result: 9652 * 9652 = 93161104
master exit.
这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker,就可以把任务分布到几台甚至几十台机器上,比如把计算n*n的代码换成发送邮件,就实现了邮件队列的异步发送。
Queue对象存储在哪?注意到task_worker.py中根本没有创建Queue的代码,所以,Queue对象存储在task_master.py进程中:
而Queue
之所以能通过网络访问,就是通过QueueManager
实现的。由于QueueManager
管理的不止一个Queue
,所以,要给每个Queue
的网络调用接口起个名字,比如get_task_queue
。worker.py
这里的QueueManager
注册的名字必须和master.py
中的一样。对比上面的例子,可以看出Queue
对象从另一个进程通过网络传递了过来。只不过这里的传递和网络通信由QueueManager
完成。
写在最后
经常我们会听到老手说:“Python下多线程是鸡肋,推荐使用多进程!”,但是为什么这么说呢?
在Python多线程下,每个线程的执行方式:
- 获取GIL
- 执行代码直到sleep或者是python虚拟机将其挂起。
- 释放GIL
这里的GIL的全称是GlobalInterpreterLock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。它确保任何时候都只有一个Python线程执行。 GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势。
而且每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。并且由于GIL锁存在,python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,python的多线程效率并不高。
**那么是不是python的多线程就完全没用了呢?**在这里我们进行分类讨论:
- CPU密集型代码(各种循环处理、计数等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。
- IO密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。
而针对多核CPU而言,每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在python中,多进程的执行效率一般优于多线程。
所以在这里说结论:多核下,想做并行提升效率,比较通用的方法是使用多进程,能够有效提高执行效率。
参考资料
- https://blog.csdn.net/qq_41922768/article/details/84024682
- https://blog.csdn.net/weixin_36637463/article/details/86496763
- https://blog.csdn.net/lechunluo3/article/details/79005910
- https://zh.wikipedia.org/wiki/%E8%A1%8C%E7%A8%8B
- https://blog.csdn.net/universe_ant/article/details/51243137
- https://www.liaoxuefeng.com/