多线程进阶与中国古拳法
- GIL:全局解释器锁
- io密集型和计算密集型
- 死锁现象
- Semaphore:信号量
- Event:事件
- 线程队列
- 线程池与进程池
-
- 引入语法:from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
- 池对象 = ThreadPoolExecutor(2):控制进程/线程的数量
- 池对象.submit(功能函数,参数,) :开启进程/线程,会返回一个相关对象,可以进行进一步处理
- 相关对象.add_done_callback(功能函数2):对submit返回的相关对象进行处理,处理逻辑为功能函数2,会自动将相关对象传给功能函数2的实参
- 相关对象.result():submit返回的相关对象.result(),会返回功能函数1的返回值,但会阻塞进程/线程,直到功能函数1结束并返回值
- 语法模板:
- 代码演示:
GIL:全局解释器锁
理论知识:
-
全局解释器锁:
在cpython解释器内部有一把大锁,线程要执行,必须获取到这把锁 -
为什么要有它?
python的垃圾回收机制是线程不安全的,所有所有线程要抢到GIL才能执行
cpython的多线程不是真正的多线程,同一时刻,只有一个线程在执行,不能利用多核优势
代码验证GIL锁的存在方式
from threading import Thread
from multiprocessing import Process
def task():
while True:
pass
if __name__ == '__main__':
# 假设当前电脑的CPU有6核
for i in range(6):
# t=Thread(target=task) # 因为有GIL锁,同一时刻,只有一条线程执行,所以cpu不会满
t=Process(target=task) # 由于是多进程,进程中的线程会被cpu调度执行,6个cpu全在工作,就会跑满
t.start()
-
多进程:执行大于等于CPU核数的无限死循环进程数量,CPU占用率100%
-
多线程:因为有GIL锁,同一时刻,只有一条线程执行,CPU占用率很小
代码验证GIL与普通互斥锁的区别
GIL锁是不能保证数据的安全,需要普通互斥锁来保证数据安全
from threading import Thread, Lock
import time
mutex = Lock()
money = 100
def task():
global money
# mutex.acquire()
temp = money
time.sleep(1)
money = temp - 1
# mutex.release()
if __name__ == '__main__':
ll=[]
for i in range(10):
t = Thread(target=task)
t.start()
ll.append(t)
for t in ll:
t.join()
print(money) # 不加互斥锁的输出为, 99
但目标效果是用十个线程,每个线程使全局变量的money-1,最后得到90,但事与愿违,
GIL锁虽然保证了每次只运行一个线程,但在time.sleep的时候相当于进入IO,会自动切换到下一个线程运行,
导致每个线程里的局部temp都接收的是100,于是十个线程不过在重复进行100-1,
所以要保证数据安全还是得使用互斥锁。
io密集型和计算密集型
----------------以下只针对于cpython解释器----------------
-
在单核情况下:
开多线程还是开多进程?不管干什么都是开线程 -
在多核情况下:
-
如果是计算密集型,需要开进程,能被多个cpu调度执行
-
如果是io密集型,需要开线程,cpu遇到io会切换到其他线程执行
-
代码演示计算密集型
多进程解决计算密集型的速度远远优于多线程
def task():
count = 0
for i in range(100000000):
count += i
if __name__ == '__main__':
ctime = time.time()
ll = []
for i in range(10):
t = Thread(target=task) # 开线程:42.68658709526062
# t = Process(target=task) # 开进程:9.04949426651001
t.start()
ll.append(t)
for t in ll:
t.join()
print(time.time()-ctime)
代码演示io密集型
多线程处理IO的速度远远优于多进程
def task():
time.sleep(2)
if __name__ == '__main__':
ctime = time.time()
ll = []
for i in range(400):
t = Thread(target=task) # 开线程:2.0559656620025635
# t = Process(target=task) # 开进程:9.506720781326294
t.start()
ll.append(t)
for t in ll:
t.join()
print(time.time()-ctime)
死锁现象
是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
中国古拳法演示死锁现象:
代码逻辑:
死锁现象一:大师兄拿到了B锁,等A锁。何金银拿到了A锁,等B锁。
死锁现象二:何金银拿到了A锁,再去拿A锁。
from threading import Thread, Lock
import time
mutexA = Lock() # A锁
mutexB = Lock() # B锁
def eat_apple(name): # 拿到A,B锁,执行代码,解B,A锁
mutexA.acquire()
print('%s 获取到了a锁' % name)
mutexB.acquire()
print('%s 获取到了b锁' % name)
print('开始吃苹果,并且吃完了')
mutexB.release()
print('%s 释放了b锁' % name)
mutexA.release()
print('%s 释放了a锁' % name)
def eat_egg(name): # 拿B,A锁,执行代码,解A,B锁
mutexB.acquire()
print('%s 获取到了b锁' % name)
time.sleep(2)
mutexA.acquire()
print('%s 获取到了a锁' % name)
print('开始吃鸡蛋,并且吃完了')
mutexA.release()
print('%s 释放了a锁' % name)
mutexB.release()
print('%s 释放了b锁' % name)
if __name__ == '__main__':
ll = ['大师兄', '何金银', ]
for name in ll:
t1 = Thread(target=eat_apple, args=(name,)) # 何金银进入t1,拿到A锁,等B锁
t2 = Thread(target=eat_egg, args=(name,)) # 大师兄进入t2,拿到B锁,等A锁
t1.start()
t2.start()
预防方案:递归锁(重入锁)
递归锁(可重入),同一个人可以多次acquire,每acquire一次,内部计数器加1,每relaese一次,内部计数器减一
只要计数器不为0,其他人都无法获得这把锁
需要导入模块:from threading import RLock
代码演示
from threading import Thread, Lock,RLock
import time
# 这种创建两个对象的方法无效,因为是同一把锁
# mutexA = Lock()
# mutexB = mutexA
# 使用可重入锁解决(同一把锁,两种写法皆可)
# mutexA = RLock()
# mutexB = mutexA
mutexA = mutexB =RLock()
def eat_apple(name):
mutexA.acquire()
print('%s 获取到了a锁' % name)
mutexB.acquire()
print('%s 获取到了b锁' % name)
print('开始吃苹果,并且吃完了')
mutexB.release()
print('%s 释放了b锁' % name)
mutexA.release()
print('%s 释放了a锁' % name)
def eat_egg(name):
mutexB.acquire()
print('%s 获取到了b锁' % name)
time.sleep(2)
mutexA.acquire()
print('%s 获取到了a锁' % name)
print('开始吃鸡蛋,并且吃完了')
mutexA.release()
print('%s 释放了a锁' % name)
mutexB.release()
print('%s 释放了b锁' % name)
if __name__ == '__main__':
ll = ['大师兄', '何金银', ]
for name in ll:
t1 = Thread(target=eat_apple, args=(name,))
t2 = Thread(target=eat_egg, args=(name,))
t1.start()
t2.start()
Semaphore:信号量
Semaphore:信号量,可以理解为多把锁,控制同一时间运行锁内代码的线程的数量
代码演示:
from threading import Thread,Semaphore
import time
import random
sm=Semaphore(3) # 数字表示可以同时有多少个线程操作
def task(name):
sm.acquire() # 只有当运行锁内代码的线程数量小于3时,才会开新的线程进锁运行
print('%s 正在蹲坑'%name)
time.sleep(random.randint(1,5))
sm.release()
if __name__ == '__main__':
for i in range(20):
t=Thread(target=task,args=('屌丝男%s号'%i,))
t.start()
Event:事件
一些线程需要等到其他线程执行完成之后才能执行,类似于发射信号
比如一个线程等待另一个线程执行结束再继续执行
引入语法:from threading import Event
对象.set() 发送信号
对象wait() 接收信号,只要没来信号,就卡在这
代码演示
from threading import Thread, Event
import time
event = Event()
def girl(name):
print('%s 现在不单身,正在谈恋爱'%name)
time.sleep(10)
print('%s 分手了,给屌丝男发了信号'%name)
event.set()
def boy(name):
print('%s 在等着女孩分手'%name)
event.wait() # 只要没来信号,就卡在这
print('女孩分手了,机会来了,冲啊')
if __name__ == '__main__':
lyf = Thread(target=girl, args=('刘亦菲',))
lyf.start()
for i in range(10):
b = Thread(target=boy, args=('屌丝男%s号' % i,))
b.start()
线程队列
进程queue和线程不是一个
queue队列:使用import queue,用法与进程Queue一样
线程队列理论知识:
-
同一个进程下多个线程数据是共享的
-
为什么先同一个进程下还会去使用队列呢
-
因为队列是:管道 + 锁
-
所以用队列还是为了保证数据的安全
-
-
三种线程Queue
- Queue:队列,先进先出
- PriorityQueue:优先级队列,谁小谁先出
- LifoQueue:栈,后进先出
Queue:先吃先拉(先进先出)
import queue
q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
'''
结果(先进先出):
first
second
third
'''
LifoQueue:后吃先吐(Last In Frist Out Queue后进先出)
import queue
q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
'''
结果(后进先出):
third
second
first
'''
PriorityQueue:数字越小,优先级越高
import queue
q=queue.PriorityQueue()
#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))
print(q.get())
print(q.get())
print(q.get())
'''
结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''
线程池与进程池
为什么会出现池?不管是开进程还是开线程,不能无限制开,通过池,假设池子里就有10个,不管再怎么开,永远是这10个
用于控制同一时间,处于运行态的线程或进程的数量
引入语法:from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
池对象 = ThreadPoolExecutor(2):控制进程/线程的数量
池对象.submit(功能函数,参数,) :开启进程/线程,会返回一个相关对象,可以进行进一步处理
相关对象.add_done_callback(功能函数2):对submit返回的相关对象进行处理,处理逻辑为功能函数2,会自动将相关对象传给功能函数2的实参
相关对象.result():submit返回的相关对象.result(),会返回功能函数1的返回值,但会阻塞进程/线程,直到功能函数1结束并返回值
语法模板:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
pool = ThreadPoolExecutor(2) # 限制同一时间只能有两个线程处于运行态
pool2 = ProcessPoolExecutor(2) # 限制同一时间只能有两个进程处于运行态
pool.submit(功能函数, 参数).add_done_callback(用于处理pool.submit()返回的对象的函数)
代码演示:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
from threading import Thread
import time
import random
pool = ThreadPoolExecutor(5) # 数字是池的大小
# pool = ProcessPoolExecutor(5) # 数字是池的大小
def task(name):
print('%s任务开始' % name)
time.sleep(random.randint(1, 4))
print('任务结束')
return '%s 返回了'%name
def call_back(f): # 实参会自动传入pool.submit()返回的对象,用f接收后,f.result()代表着线程执行的函数的返回值,在这里可以对其进行各种逻辑处理
# print(type(f))
print(f.result())
if __name__ == '__main__':
# ll=[]
# for i in range(10): # 起了10个线程
# res = pool.submit(task, '屌丝男%s号' % i) # 参数不需要再写在args中了
# # res是Future对象
# # from concurrent.futures._base import Future # 实验性导入一下去看看Future的属性,发现result方法可以输出返回值
# # print(type(res))
# # print(res.result()) # 像join,只要执行result,就会等着结果回来,就变成串行了
# ll.append(res)
#
# for res in ll: # 于是把res.result()模仿join()一样,放到下面一起执行,可无法并发的处理task的返回值了,于是乎利用res.result()的这个方法pass
# print(res.result())
# 最终版本
for i in range(10): # 起了10个线程
# 向线程池中提交一个任务,等任务执行完成,自动回到到call_back函数执行
pool.submit(task,'屌丝男%s号' % i).add_done_callback(call_back) # # call_back的实参会自动传入pool.submit()返回的对象