GIL
在大多数环境中,单核CPU情况下,本质上某一时刻只能有一个线程被执行,多核CPU时则可以支持多个线程同时执行。但是在Python中,无论CPU有多少核,同时只能执行一个线程。这是由于GIL的存在导致的。
GIL的全称是Global Interpreter Lock
(全局解释器锁),是Python设计之初为了数据安全所做的决定。GIL本质就是一把互斥锁,都是让多个并发线程同时只能有一个执行,既有了GIL的存在之后,同一进程内的多个线程只能有一个在运行,意味着在cpython中一个进程下的多个线程无法实现并行, 意味着无法复用多核优势 但不影响并发,
GIL是Cpython的特性,因为Cpython解释器自带垃圾回收机制,而这种机制是线程不安全的,而这种意味着多线程同时只能有一个线程运行
Python多线程的工作流程
- 拿到公共数据
- 申请GIL
- Python解释器调用操作系统原生线程
- cpu执行运算
- 当该线程执行一段时间消耗完,无论任务是否已经执行完毕,都会释放GIL
- 下一个被CPU调度的线程重复上面的过程
为何要存在GIL
因为Cpython解释器自带垃圾回收器不是线程安全的,GIL可以被比喻成执行权限,同一进程下的所有线程, 要想执行都需要先抢执行权限
GIL vs 自定义互斥锁
- GIL相当于是执行权限, 会在任务无法执行的情况,会被强制释放
- 自定义互斥锁即使无法执行,也无法被操作系统强制释放
Python针对不同类型的任务,多线程执行效率是不同的:
- 对于CPU密集型任务(各种循环处理、计算等等),由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换是需要消耗资源的),所以Python下的多线程对CPU密集型任务并不友好。
- IO密集型任务(文件处理、网络通信等涉及数据读写的操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以Python的多线程对IO密集型任务比较友好。
在实际使用中的建议:
Python中想要充分利用多核CPU,就用多进程。因为每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行。在Python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。同时建议在IO密集型任务中使用多线程,在计算密集型任务中使用多进程
进程池,线程池
# 1. 什么时候用池
1. 池的功能是限制启动的进程数或线程数,
2. 什么时候限制,当并发的任务远远超过了计算机的承受能力,即一次性无法开启过多的进程或线程,就应该用池的概念将开启的进程数或线程数限制在计算机的承受能力之内
# 2. 同步或异步 (提交任务的两种方式)
1. 同步: 提交完任务原地等待, 直到任务结束拿到返回值,再继续运行下一行代码
2. 异步:提交完任务(绑定一个回调函数)不在原地等待,直接运行下一行代码,直到任务有返回值之后,直接触发回调函数 (线程是由哪个线程开始,哪个结束,进程池是由主进程来执行最后的回调函数,线程池是哪个线程闲着就执行哪个)
使用线程池
由于线程预先被创建并放入线程池中,同时处理完当前任务之后并不销毁而是被安排处理下一个任务,因此能够避免多次创建线程,从而节省线程创建和销毁的开销,能带来更好的性能和系统稳定性。
线程最大利用数
抽象一下,计算线程数设置的公式就是:N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。由于有GIL的影响,python只能使用到1个核,所以这里设置N=1
进程池
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import os
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def tesk(res):
time.sleep(1)
print("{} hello {}".format(os.getpid(), res))
if __name__ == '__main__':
pool = ProcessPoolExecutor(5)
start = time.time()
for i in range(1, 10):
pool.submit(tesk, i)
pool.shutdown(wait=True) # 相当于是 p1.join() 等待子进程完成,异步提交
stop = time.time()
print("主{} 时间:{}".format(os.getpid(), stop - start)) # 主24428 时间:2.42763
进程池-回调函数-1
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import os
import time
from concurrent.futures import ProcessPoolExecutor
def tesk(res):
time.sleep(5)
print("进程: {} 结果: {}".format(os.getpid(), res))
# return res ** 2
return parse(res) # 2、返回解析之后的结果
def parse(res): # 1、如果需要获取到 结果并将给另一个函数调用使用,需要这么写
return res ** 2
if __name__ == '__main__':
pool = ProcessPoolExecutor(4)
l = []
start = time.time()
for i in range(1, 5):
func = pool.submit(tesk, i)
# print(func.result()) # 直接取到结果,会变成同步, 每次都行返回结果, 如同 串行
l.append(func) # 3、获取到future并附加到l中
for fu in l:
print(fu.result()) # 4、最终返回结果, 普通回调
pool.shutdown(wait=True)
stop = time.time()
print("master: {}, 时间: {}".format(os.getpid(), stop - start))
# master: 24272, 时间: 5.320957899093628
多进程-回调函数-add_done_callback()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import os
import time
from concurrent.futures import ProcessPoolExecutor
def tesk(res):
time.sleep(5)
print("进程: {} 结果: {}".format(os.getpid(), res))
return res ** 2
def parse(res):
time.sleep(1)
print("{} 处理的数据为: {}".format(os.getpid(),res.result()))
if __name__ == '__main__':
pool = ProcessPoolExecutor(4)
start = time.time()
for i in range(1, 5):
func = pool.submit(tesk, i)
func.add_done_callback(parse) # 回调函数,
# func 添加回调函数,需要有一个返回值 return结果,将其结果丢给另一个函数处理
pool.shutdown(wait=True)
stop = time.time()
print("master: {}, 时间: {}".format(os.getpid(), stop - start))
"""
# 多进程处理时间为 9.278696298599243, 其每次处理完的结果都会交给主进程来调用
进程: 42224 结果: 1
进程: 11764 结果: 2
进程: 36444 结果: 3
进程: 25480 结果: 4
41044 处理的数据为: 1
41044 处理的数据为: 4
41044 处理的数据为: 9
41044 处理的数据为: 16
master: 41044, 时间: 9.779107809066772
"""
1、 ProcessPoolExecutor(4) # 开启了四个进程池
2、 pool.submit(tesk, i) # 运行进程池, 并提交参数
3、 def tesk(res) # 进程处理,每次都带回return值
4、 func.add_done_callback(parse) # 添加回调函数,将return的值丢给parse来二次处理
5、 def parse(res) # 回调的函数都由主进程来调用发起, 每次都是串行执行
6、 pool.shutdown(wait=True) # 等待子进程完成 最后在执行主进程
多线程-回调函数- add_done_callback()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import os
import time
from threading import current_thread
from concurrent.futures import ThreadPoolExecutor
def tesk(res):
time.sleep(5)
print("进程: {} 结果: {}".format(current_thread().name, res))
return res ** 2
def parse(res):
time.sleep(1)
print("{} 处理的数据为: {}".format(current_thread().name,res.result()))
if __name__ == '__main__':
pool = ThreadPoolExecutor(4)
start = time.time()
for i in range(1, 5):
func = pool.submit(tesk, i)
func.add_done_callback(parse) # 回调函数,
# func 添加回调函数,需要有一个返回值 return结果,将其结果丢给另一个函数处理
pool.shutdown(wait=True)
stop = time.time()
print("master: {}, 时间: {}".format(current_thread().name, stop - start))
"""
# 线程池 处理时间为 9.278696298599243, 哪个线程有空闲就会直接回调函数处理
进程: ThreadPoolExecutor-0_0 结果: 1
进程: ThreadPoolExecutor-0_1 结果: 2
进程: ThreadPoolExecutor-0_3 结果: 4
进程: ThreadPoolExecutor-0_2 结果: 3
ThreadPoolExecutor-0_1 处理的数据为: 4
ThreadPoolExecutor-0_0 处理的数据为: 1
ThreadPoolExecutor-0_3 处理的数据为: 16
ThreadPoolExecutor-0_2 处理的数据为: 9
master: MainThread, 时间: 6.002583265304565
"""
'''
产生在写代码之前, 先要确定好该需求是 计算密集型的还是IO密集型的
'''
# 进程池
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from multiprocessing import current_process
import time
import os
def tesk(n):
print("进程 {} 开始".format(current_process().name))
x=n
time.sleep(5)
for i in range(100):
x += i
return "最终的结果 {}".format(x)
if __name__ == '__main__':
p1 = ProcessPoolExecutor(4)
start = time.time()
li_ret = []
for i in range(4):
ret = p1.submit(tesk, 1)
li_ret.append(ret)
p1.shutdown(wait=True) # 相当于是 p1.join() 等待
子进程运行完在执行 最后的主进程代码
for li in li_ret:
print("{}进程名, 结果: {}".format(os.getpid(), li.result()))
stop = time.time() # 主, time: 10.125417947769165
# 4个进程同时运行, 那么时间就是5秒,
# 如果只定义了三个进程池, 却有四个进程那么就得等前三个运行完,然后才能执行第四个,就是10.1秒以上了
print("主, time: {}".format(stop-start) )
# 回调函数
ret.add_done_callback("name") # 回调函数最终总接收一个参数
xx.submit(name).add_done_callback(func)
方法与属性 | 说明 |
queue.join() | 作用是让主程序阻塞等待队列完成,就结束退出 |
queue.get() | 通知主程序队列取完了,但不代表队列里的任务都完成 |
queue.task_done() | 告诉主程序,又一个任务完成了,直到全部任务完成,主程序退出 |