漫天飞雪

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

'''
1. 什么是GIL全局解释器锁
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。相当于执行权限,每个进程内都会存在一把GIL,同一进程内的多个线程



必须抢到GIL之后才能使用Cpython解释器来执行自己的代码,即同一进程下的多个线程无法实现并行
但是可以实现并发


2. 为何要有GIL
因为Cpython解释器的垃圾回收机制不是线程安全的

那么怎么样来保证数据安全?

比如进程内有三个我们创建的线程(线程1,线程2,线程3)。而且有一个垃圾回收机制,这也是一个线程,这四个线程都需要运行。拿垃圾回收机制来举例,首先需要明白,垃圾回收机制回收哪些东西——垃圾回收机制会回收那些没有指向性的内存地址,也就是没有被引用的内存地址。
举个例子,比如你写了一个a.py文件,里面有一个x = 1,这句python语句回去调用cpython解释器。那么解释器会开辟一块内存空间将1放进去,再开辟一块内存地址,放入x,然后关联1的内存地址和x的内存地址。
那么问题来了,如果没有GIL会怎么样?
如果没有GIL的话,就会有种可能——当内存刚刚开了一块空间,放入1,垃圾回收机制检测到这个1的内存空间没有被任何其他的地址引用到,就会回收掉这块内存空间。这与我们的初衷是相悖的。
所以我们需要GIL。所谓的GIL就是(全局解释器锁)global interpreter lock。与互斥锁相类似,在python线程运行的时候,先拿到GIL(可以看作是一个权限),当其他线程再想使用解释器运行代码时,因为没有拿到权限(GIL),就会进入等待状态。从而阻止了线程的并行运行,表面上看这是降低了代码运行的效率,但其实这有效了保护了数据的安全性。
在Cpython解释器下,如果想实现并行可以开启多个进程(使用每个进程内跑一个线程)


3. 如何用GIL
有了GIL,应该如何处理并发

'''
# from threading import Thread
# import time
#
# def task(name):
# print('%s is running' %name)
# time.sleep(2)
#
# if __name__ == '__main__':
# t1=Thread(target=task,args=('线程1',))
# t2=Thread(target=task,args=('线程1',))
# t3=Thread(target=task,args=('线程1',))
# t1.start()
# t2.start()
# t3.start()


以下两种类型的讨论都是在多核cpu下进行的

# 计算密集型:应该使用多进程
多核的CPU下,每个CPU都可以运行一个进程,假如有四个CPU,运行四个进程,那么就可以达到并行的效果。在计算密集型的程序里(不需要等待IO的时间),每个CPU都可以算。如果用线程去计算的话,由于有GIL锁,所以多个CPU其实只有一个CPU在运行,多个线程相当于串行。所耗费的时间大概是(线程的个数)* 计算时间
# 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())
# start=time.time()
# for i in range(6):
# # p=Process(target=work)
# p=Thread(target=work)
# l.append(p)
# p.start()
# for p in l:
# p.join()
# stop=time.time()
# print('run time is %s' %(stop-start)) #4.271663427352905

# IO密集型: 应该开启多线程
因为线程来回切换的时间短,由于等待IO输入时线程就会暂停被CPU剥夺走GIL锁去运行其他的线程,所以在IO密集型程序里,使用线程,缩短来回切的时间。总共耗时几乎就等于一个线程IO的等待时间(如果每个线程IO的等待时间相同的话)+ cpu在每个线程来回切的时间。
如果使用进程的话,开进程,切换进程的时间要远远大于线程的时间。其他的差别不大。
from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
time.sleep(2)

if __name__ == '__main__':
l=[]
start=time.time()
for i in range(300):
# p=Process(target=work) #2.225289821624756
p=Thread(target=work) #2.002105951309204
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))

总结:# 计算密集型:应该使用多进程
# IO密集型:应该使用多线程


GIL VS 自定义互斥锁




from threading import Thread,Lock
import time

mutex=Lock()
n=100
def task():
global n
with mutex:
temp=n
time.sleep(0.1)
n=temp-1

if __name__ == '__main__':
l=[]
for i in range(100):
t=Thread(target=task)
l.append(t)
t.start()

for t in l:
t.join()
print(n)
这个进程内有100个我们创建的线程(线程1,线程2,线程3.。。)。
每个线程都想要改变n的值,让n减去1。代码如上。
过程:线程1首先被创建,拿到GIL。运行 global n ,拿到mutex,运行temp=n ,然后进入休眠,释放GIL。在休眠的0.1秒内,其他99个线程创建完毕。线程1重新进入到就绪状态。
线程2抢到GIL,运行global n 但是,拿不到mutex(此时mutex正在线程1的手里),所以没法继续运行,释放GIL,进入阻塞状态。
如此循环去下,直到线程1拿到GIL,运行 n=temp-1,释放mutex,释放GIL,运行完毕。

由此可以得出结论,GIL并不能完全阻止脏数据的写入,GIL所管理的仅仅是线程与解释器的完美结合使用,保证底层内存地址不会发生错乱。需要配合自定义的mutex互斥锁,才能达到效果。

 


死锁现象与递归锁

from threading import Thread,Lock,RLock(递归锁)
import time

# mutexA=Lock()
# mutexB=Lock()
mutexB=mutexA=RLock() #递归锁


class Mythead(Thread):
def run(self):
self.f1()
self.f2()

def f1(self):
mutexA.acquire()
print('%s 抢到A锁' %self.name)
mutexB.acquire()
print('%s 抢到B锁' %self.name)
mutexB.release()
mutexA.release()

def f2(self):
mutexB.acquire()
print('%s 抢到了B锁' %self.name)
time.sleep(2)
mutexA.acquire()
print('%s 抢到了A锁' %self.name)
mutexA.release()
mutexB.release()

if __name__ == '__main__':
for i in range(100):
t=Mythead()
t.start()

当使用普通的锁Lock时,当线程1运行到f2方法的mutexB.acquire()时候(拿到了锁B)。线程2已经运行了,而此时,当线程2拿到了锁A(运行了mutexA.acquire()),又运行到mutexB.acquire()的是时候(想要得到锁B),发生了阻塞——
因为锁B此时在线程1手里,
而线程1需要锁A才能继续运行,释放锁A和锁B。
而线程2需要锁B才能继续运行,释放锁A和锁B。
这就是死锁发生的原因。

所以就引入了递归锁(特点:可以连续acquire多次)。
递归锁采用了计数的方式,mutexB=mutexA=RLock() ,所以现在只有一把锁。用过计数的方法,只要锁身上有大于0的计数,锁就无法被其他线程争抢acquire(),当线程1释放锁,达到计数为0时,其他线程可以来争抢这把锁,就完美的解决的死锁的问题。

 


event事件

同进程的一样,线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

Event几种方法:
event.isSet():返回event的状态值;

event.wait():如果 event.isSet()==False将阻塞线程;

event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

event.clear():恢复event的状态值为False。

from threading import Thread,Event
import time

event=Event()

def light():
print('红灯正亮着')
time.sleep(3)
event.set() #绿灯亮

def car(name):
print('车%s正在等绿灯' %name)
event.wait() #等灯绿 此时event为False,直到event.set()将其值设置为True,才会继续运行.
print('车%s通行' %name)

if __name__ == '__main__':
# 红绿灯
t1=Thread(target=light)
t1.start()
# 车
for i in range(10):
t=Thread(target=car,args=(i,))
t.start()



信号量

 

信号量相当于递归锁,被acquire()一次,计数减1,release一次,计数加1,题中,总计数为5。直到计数为0,不可以继续运行代码。

from threading import Thread,Semaphore
import time,random
sm=Semaphore(5)

def task(name):
sm.acquire()
print('%s 正在上厕所' %name)
time.sleep(random.randint(1,3))
sm.release()

if __name__ == '__main__':
for i in range(20):
t=Thread(target=task,args=('路人%s' %i,))
t.start()

 


线程queue


queue队列 :使用import queue,用法与进程Queue一样
import queue

queue.Queue() #先进先出
q=queue.Queue(3) #设置队列长度为3,如果put超过三个程序会阻塞
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
结果:
1
2
3


queue.LifoQueue() #后进先出->堆栈
q=queue.LifoQueue(3) #设置队列长度为3,如果put超过三个程序会阻塞
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
结果:
3
2
1


q=queue.PriorityQueue(3) #优先级,优先级用数字表示,数字越小优先级越高
q.put((10,'a'))
q.put((-1,'b'))
q.put((100,'c'))
print(q.get())
print(q.get())
print(q.get())
结果:
(-1, 'b')
(10, 'a')
(100, 'c')

posted on 2018-12-27 09:15  漫天飞雪世情难却  阅读(112)  评论(0编辑  收藏  举报