全局解释器锁GIL
是什么?
GIL本质也是一把互斥锁,将并发变成串行,降低效率以保证数据的安全性
每有一个进程,进程内就必然有一个用来执行代码的线程,也会有一个用来执行垃圾回收的线程。为了避免执行代码的线程和执行垃圾回收的线程对同一份数据进行操作造成数据混乱,这时候需要有一个锁,用来保证同一时间内只有一个线程在执行,这个锁就叫做全局解释器锁GIL。
线程抢到GIL之后,就可以使用Python解释器来运行自己的代码。
只有CPython中有GIL。ps:python解释器有很多种:CPython, JPython,IronPython(C#实现),PyPy(python实现)…最常见的是CPython解释器
为什么要有GIL?
因为CPython解释器的垃圾回收机制不是线程安全的。
用来阻止同一个进程下的多线程的同时执行(同一个进程内的多线程无法实现并行,但可以实现并发)。
Python的多线程无法利用多核的优势,那么要多线程有什么用?
研究Python的多线程是否有用需要分情况讨论:
计算密集型情况:要进行大量的计算,消耗CPU资源
单核:开线程更省资源
多核:使用进程,可以利用多核优势
IO密集型情况:CPU消耗很少,任务的大部分时间都在等待IO操作完成
单核:开线程更省资源
多核:开线程,更省资源。
按具体情况,多进程与多线程需要配合使用。
# 计算密集型
from multiprocessing import Process
from threading import Thread
import os,time
def work():
res=0
for i in range(100000000):
res*=i
if __name__ == '__main__':
l=[]
print(os.cpu_count()) # 本机为6核
start=time.time()
for i in range(6):
# p=Process(target=work) #耗时 4.732933044433594
p=Thread(target=work) #耗时 22.83087730407715
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))
# IO密集型
from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
time.sleep(2)
if __name__ == '__main__':
l=[]
print(os.cpu_count()) #本机为6核
start=time.time()
for i in range(4000):
p=Process(target=work) #耗时9.001083612442017s多,大部分时间耗费在创建进程上
# p=Thread(target=work) #耗时2.051966667175293s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))
同步锁、互斥锁
同步:访问资源必须按照某种规定,有先后次序的访问
互斥:访问同一资源时,在同一时间只能有一个访问者。
互斥时一种特殊的同步,而同步是更为复杂的互斥。
互斥锁特点:一次只能有一个线程拥有锁,其他锁只能等待。
join vs 互斥锁
join是等待所有,即整体串行,相当于锁住了所有代码
锁是锁住修改共享数据的部分,即部分串行,相当于只锁住了修改共享数据部分的代码
要想让数据安全的根本原理是让并发变成串行,即join和锁都能做到,但是,毫无疑问,锁的效率更高!
#不加锁:并发执行,速度快,数据不安全
from threading import current_thread,Thread,Lock
import os,time
def task():
global n
print('%s is running' %current_thread().getName())
temp=n
time.sleep(0.5)
n=temp-1
if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:0.5216062068939209 n:99
'''
#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,Thread,Lock
import os,time
def task():
#未加锁的代码并发运行
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
#加锁的代码串行运行
lock.acquire()
temp=n
time.sleep(0.5)
n=temp-1
lock.release()
if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:53.294203758239746 n:0
'''
#有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊
#没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是
#start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
#单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.
from threading import current_thread,Thread,Lock
import os,time
def task():
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
temp=n
time.sleep(0.5)
n=temp-1
if __name__ == '__main__':
n=100
lock=Lock()
start_time=time.time()
for i in range(100):
t=Thread(target=task)
t.start()
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
'''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 #耗时是多么的恐怖
GIL vs 互斥锁
提出问题:Python已经有一个GIL来保证同一时间只能有一个线程运行,为什么还需要互斥锁?
首先,锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享数据。
因此:不同的共享数据需要不同的锁来保护
结论:GIL保护的是解释器层面的数据(垃圾回收的数据),互斥锁保护的是用户自己自定义的共享数据。
from threading import Thread,Lock
import os,time
def work():
global n
lock.acquire()
temp=n
time.sleep(0.1)
n=temp-1
lock.release()
if __name__ == '__main__':
lock=Lock()
n=100
l=[]
for i in range(100):
p=Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全
'''
分析:
1.100个线程去抢GIL锁,即抢执行权限
2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
4.直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
'''
死锁、递归锁
什么是死锁?
两个或两个以上的进程或线程,因争夺资源而造成的互相等待现象。
from threading import Thread, Lock
import time
mutexA = Lock()
mutexB = Lock()
class MyThread(Thread):
def run(self):
self.fun1()
self.fun2()
def fun1(self):
mutexA.acquire()
print('{}拿到A锁'.format(self.name))
mutexB.acquire()
print('{}拿到B锁'.format(self.name))
mutexB.release()
mutexA.release()
def fun2(self):
mutexB.acquire()
print('{}拿到B锁'.format(self.name))
time.sleep(2)
mutexA.acquire()
print('{}拿到A锁'.format(self.name))
mutexA.release()
mutexB.release()
if __name__ == '__main__':
for i in range(10):
t = MyThread()
t.start()
为了解决死锁,设计出递归锁(RLock)。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
信号量Semaphore
互斥锁mutex是信号量的一种特殊情况
from threading import Thread,Semaphore
import threading
import time
# def func():
# if sm.acquire():
# print (threading.currentThread().getName() + ' get semaphore')
# time.sleep(2)
# sm.release()
def func():
sm.acquire()
print('%s get sm' %threading.current_thread().getName())
time.sleep(3)
sm.release()
if __name__ == '__main__':
sm = Semaphore(5)
for i in range(23):
t = Thread(target=func)
t.start()
把互斥锁和信号量比喻成厕所。
互斥锁这个厕所只有一个坑位,其他人想进入,就得在外面等着。
信号量这个厕所有多个坑位,等坑被占满了,其他人想进去就只能在外面排队。
Event事件
线程的关键特性:每一个线程都是独立运行,且状态不可预测。
Event为了解决什么问题?当线程需要通过判断另一个线程的状态来确定自己的下一步的时候。
from threading import Thread, Event
import time
e = Event()
def light():
print('红灯亮')
time.sleep(3)
e.set() # 发信号
print('绿灯亮')
def car(name):
print('{}正在等红灯'.format(name))
e.wait() # 等待信号
print('冲冲冲')
t = Thread(target=light)
t.start()
for i in range(10):
t = Thread(target=car, args=(('汽车{}'.format(i)),))
t.start()
定时器
指定n秒后执行某操作
from threading import Timer
def hello():
print("hello, world")
t = Timer(1, hello)
t.start() # after 1 seconds, "hello, world" will be printed
from threading import Timer
import random,time
class Code:
def __init__(self):
self.make_cache()
def make_cache(self,interval=5):
self.cache=self.make_code()
print(self.cache)
self.t=Timer(interval,self.make_cache)
self.t.start()
def make_code(self,n=4):
res=''
for i in range(n):
s1=str(random.randint(0,9))
s2=chr(random.randint(65,90))
res+=random.choice([s1,s2])
return res
def check(self):
while True:
inp=input('>>: ').strip()
if inp.upper() == self.cache:
print('验证成功',end='
')
self.t.cancel()
break
if __name__ == '__main__':
obj=Code()
obj.check()
线程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
'''
class queue.LifoQueue(maxsize=0) #last in fisrt out
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
'''
复制代码
class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列
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')
'''