线程之全局解释器锁加一些了解知识点

---恢复内容开始---

1.GIL全局解释器锁

2.GIL与普通的互斥锁

3.死锁

4.信号量

5.event事件

6.线程q队列

7.补充 基于TCP使用线程实现高并发

一.GIL全局解释器锁

GIL jaosn总结

 1     TCP服务端实现并发
 2         1.将不同的功能尽量拆分成不同的函数
 3             拆分出来的功能可以被多个地方使用
 4         
 5         1.将连接循环和通信循环拆分成不同的函数
 6         2.将通信循环做成多线程
 7         
 8 
 9     GIL(全局解释器锁)
10     在CPython解释器才有GIL的概念,不是python的特点
11     GIL也是一把互斥锁
12         将并发变成串行 牺牲了效率但是提高了数据的安全
13         ps:
14             1.针对不同的数据 应该使用不同的锁去处理
15             2.自己不要轻易的处理锁的问题 哪怕你知道acquire和release
16             当业务逻辑稍微复杂的一点情况下 极容易造成死锁
17     CPython中的GIL的存在是因为python的内存管理不是线程安全的
18     
19     内存管理
20         引用计数:值与变量的绑定关系的个数
21         标记清除:当内存快要满的时候 会自动停止程序的运行 检测所有的变量与值的绑定关系
22                 给没有绑定关系的值打上标记,最后一次性清除
23         分代回收:(垃圾回收机制也是需要消耗资源的,而正常一个程序的运行内部会使用到很多变量与值
24         并且有一部分类似于常量,减少垃圾回收消耗的时间,应该对变量与值的绑定关系做一个分类
25         )        新生代(5S)》》》青春代(10s)》》》老年代(20s)
26                 垃圾回收机制扫描一定次数发现关系还在,会将该对关系移至下一代
27                 随着代数的递增 扫描频率是降低的
28     
29 
30     同一个进程下的多个线程能否同时运行
31     GIL类似于是加在解释器上面的一把锁
View Code

什么是GIL:首先来看看官方的解释

  在CPython中,这个全局解释器锁,也称之为GIL,是一个互斥锁,

防止多个线程在同一个时间执行Python字节码,这个锁是非常正要的,

因为CPython的内存管理非线程安全的,很多其他的特性依赖于GIL,

所以即使他影响了程序的效率也无法将其直接去除

总结:在CPython中,GIL会把线程的并行变成串行,导致效率降低

PS:需要知道的是,解释器并不只有CPython,还有PyPy,JPython等等。

GIL也仅存在于CPython中,这并不是python这门语言的问题,而是CPython解释器的问题

2.GIL带来的问题:

首先必须明确执行一个py文件,分为三个步骤

  1. 从硬盘加载Python解释器到内存

  2. 从硬盘加载py文件到内存

  3. 解释器解析py文件内容,交给CPU执行

其次需要明确的是每当执行一个py文件,就会立即启动一个python解释器,

当执行test.py时其内存结构如下:

GIL,叫做全局解释器锁,加到了解释器上,并且是一把互斥锁,那么这把锁对应用程序到底有什么影响?

这就需要知道解释器的作用,以及解释器与应用程序代码之间的关系

py文件中的内容本质都是字符串,只有在被解释器解释时,才具备语法意义,解释器会将py代码翻译为当前系统支持的指令交给系统执行。

开启子线程时,给子线程指定了一个target表示该子线程要处理的任务即要执行的代码。代码要执行则必须交由解释器,即多个线程之间就需要共享解释器,为了避免共享带来的数据竞争问题,于是就给解释器加上了互斥锁!

由于互斥锁的特性,程序串行,保证数据安全,降低执行效率,GIL将使得程序整体效率降低!

3.那么为什么需要GIL锁:

GIL与GC

在使用Python中进行编程时,程序员无需参与内存的管理工作,这是因为Python有自带的内存管理机制,简称GC。那么GC与GIL有什么关联?

要搞清楚这个问题,需先了解GC的工作原理,Python中内存管理使用的是引用计数,每个数会被加上一个整型的计数器,表示这个数据被引用的次数,当这个整数变为0时则表示该数据已经没有人使用,成了垃圾数据。

当内存占用达到某个阈值时,GC会将其他线程挂起,然后执行垃圾清理操作,垃圾清理也是一串代码,也就需要一条线程来执行。

示例代码:

from threading import Thread
def task():
a = 10
print(a)


# 开启三个子线程执行task函数
Thread(target=task).start()
Thread(target=task).start()
Thread(target=task).start()

 

上述代码的内存结构如下:


通过上图可以看出,GC与其他线程都在竞争解释器的执行权,而CPU何时切换,以及切换到哪个线程都是无法预支的,这样一来就造成了竞争问题,假设线程1正在定义变量a=10,而定义变量第一步会先到到内存中申请空间把10存进去,第二步将10的内存地址与变量名a进行绑定,如果在执行完第一步后,CPU切换到了GC线程,GC线程发现10的地址引用计数为0则将其当成垃圾进行了清理,等CPU再次切换到线程1时,刚刚保存的数据10已经被清理掉了,导致无法正常定义变量。

当然其他一些涉及到内存的操作同样可能产生问题问题,为了避免GC与其他线程竞争解释器带来的问题,CPython简单粗暴的给解释器加了互斥锁,如下图所示:


有了GIL后,多个线程不可能在同一时间使用解释器,从而保证了解释器的数据安全

GIL的加锁时机:在调用解释器时立即加锁

解锁时机:
  当前线程遇到了IO时释放

  当前线程执行时间超过设定值时释放

GIL锁有优点也有缺点:
优点:

  保证了数据的安全

缺点:
  互斥锁的特性使得多线程无法并行

研究python的多线程是否有用需要分情况讨论
四个任务 计算密集型的  10s
单核情况下
    开线程更省资源
多核情况下
    开进程 10s
    开线程 40s

四个任务 IO密集型的  
单核情况下
    开线程更节省资源
多核情况下
    开线程更节省资源

案例:计算密集型

 1 from multiprocessing import Process
 2 from threading import Thread
 3 import os,time
 4 def work():
 5     res=0
 6     for i in range(100000000):
 7         res*=i
 8 
 9 
10 if __name__ == '__main__':
11     l=[]
12     print(os.cpu_count())  # 本机为6核
13     start=time.time()
14     for i in range(6):
15         # p=Process(target=work) #耗时  4.732933044433594
16         p=Thread(target=work) #耗时 22.83087730407715
17         l.append(p)
18         p.start()
19     for p in l:
20         p.join()
21     stop=time.time()
22     print('run time is %s' %(stop-start))
View Code

案例:IO密集型

 1 from multiprocessing import Process
 2 
 3 import os,time
 4 def work():
 5     time.sleep(2)
 6 
 7 if __name__ == '__main__':
 8     l=[]
 9     print(os.cpu_count()) #本机为6核
10     start=time.time()
11     for i in range(4000):
12         p=Process(target=work) #耗时9.001083612442017s多,大部分时间耗费在创建进程上
13         # p=Thread(target=work) #耗时2.051966667175293s多
14         l.append(p)
15         p.start()
16     for p in l:
17         p.join()
18     stop=time.time()
19     print('run time is %s' %(stop-start))
View Code

总结:

python的多线程到底有没有用
需要看情况而定 并且肯定是有用的
多进程+多线程配合使用

二.GIL与普通的互斥锁区别

GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制详解。

对于程序中自己定义的数据则没有任何的保护效果,这一点在没有介绍GIL前我们就已经知道了,所以当程序中出现了共享自定义的数据时就要自己加锁,如下例:
案例:向一下代码那样,还是需要加互斥所的
 1 from threading import Thread
 2 import time
 3 
 4 n = 100
 5 
 6 
 7 def task():
 8     global n
 9     tmp = n
10     time.sleep(1)
11     n = tmp -1
12 
13 t_list = []
14 for i in range(100):
15     t = Thread(target=task)
16     t.start()
17     t_list.append(t)
18 
19 for t in t_list:
20     t.join()
21 
22 print(n)
View Code

三.死锁

死锁问题
当程序出现了不止一把锁,分别被不同的线程持有, 有一个资源 要想使用必须同时具备两把锁
这时候程序就会进程无限卡死状态 ,这就称之为死锁

 案例:

案例:

from threading import Thread RLock
import time

mutexA = Lock()
mutexB = Lock()



class MyThread(Thread):
    def run(self):  # 创建线程自动触发run方法 run方法内调用func1 func2相当于也是自动触发
        self.func1()
        self.func2()

    def func1(self):
        mutexA.acquire()
        print('%s抢到了A锁'%self.name)  # self.name等价于current_thread().name
        mutexB.acquire()
        print('%s抢到了B锁'%self.name)
        mutexB.release()
        print('%s释放了B锁'%self.name)
        mutexA.release()
        print('%s释放了A锁'%self.name)

    def func2(self):
        mutexB.acquire()
        print('%s抢到了B锁'%self.name)
        time.sleep(1)
        mutexA.acquire()
        print('%s抢到了A锁' % self.name)
        mutexA.release()
        print('%s释放了A锁' % self.name)
        mutexB.release()
        print('%s释放了B锁' % self.name)

for i in range(10):
    t = MyThread()
    t.start()
这样就会产生死锁现象:解释
首先执行的是func1然后线程1抢到A锁,其他九个线程需要的需要等待A锁被释放,
线程1不会释放A锁,紧接着就会抢B锁,B锁是没有人抢的,所以直接就可以拿到B锁,
然后释放B锁,紧接着释放A锁,那么其他九个在等待的线程看到A锁被线程1释放了,
他们就会立马区抢,那么线程1已经区执行func2了,因为其他九个线程都在func1哪里抢,
线程1直接就可以抢到B锁,然后线程1会进入阻塞状态1秒,但是线程1还持有者B锁,
func1里面的线程也开始抢B锁了,1秒阻塞状态过去,线程1也需要抢A锁了,
但是A锁被func1里面的线程持有,这样就会产生我要你的A锁,你要我的B锁,
但是都给不了,就会卡住

补充知识点:

Rlock 称之为递归锁或者可重入锁

与Lock唯一的区别: Rlock同一线程可以多次执行acquire 但是执行几次acquire就应该对应release几次 如果一个线程已经执行过acquire 其他线程将无法执行acquire

 案例:

 1 from threading import Thread,Lock,current_thread,RLock
 2 import time
 3 """
 4 Rlock可以被第一个抢到锁的人连续的acquire和release
 5 每acquire一次锁身上的计数加1
 6 每release一次锁身上的计数减1
 7 只要锁的计数不为0 其他人都不能抢
 8 
 9 """
10 # mutexA = Lock()
11 # mutexB = Lock()
12 mutexA = mutexB = RLock()  # A B现在是同一把锁
13 
14 
15 class MyThread(Thread):
16     def run(self):  # 创建线程自动触发run方法 run方法内调用func1 func2相当于也是自动触发
17         self.func1()
18         self.func2()
19 
20     def func1(self):
21         mutexA.acquire()
22         print('%s抢到了A锁'%self.name)  # self.name等价于current_thread().name
23         mutexB.acquire()
24         print('%s抢到了B锁'%self.name)
25         mutexB.release()
26         print('%s释放了B锁'%self.name)
27         mutexA.release()
28         print('%s释放了A锁'%self.name)
29 
30     def func2(self):
31         mutexB.acquire()
32         print('%s抢到了B锁'%self.name)
33         time.sleep(1)
34         mutexA.acquire()
35         print('%s抢到了A锁' % self.name)
36         mutexA.release()
37         print('%s释放了A锁' % self.name)
38         mutexB.release()
39         print('%s释放了B锁' % self.name)
40 
41 for i in range(10):
42     t = MyThread()
43     t.start()
View Code

四.信号量

可以现在被锁定的代码 同时可以被多少线程并发访问
互斥锁: 锁住一个马桶 同时只能有一个
信号量: 锁住一个公共厕所 同时可以来一堆人


用途: 仅用于控制并发访问 并不能防止并发修改造成的问题

案例:

 1 from threading import Semaphore,Thread
 2 import time
 3 import random
 4 
 5 sm = Semaphore(5)  # 造了一个含有五个的坑位的公共厕所
 6 
 7 def task(name):
 8     sm.acquire()
 9     print('%s占了一个坑位'%name)
10     time.sleep(random.randint(1,3))
11     sm.release()
12 
13 for i in range(40):
14     t = Thread(target=task,args=(i,))
15     t.start()
View Code

五.event事件

什么是事件

事件表示在某个时间发生了某个事情的通知信号,用于线程间协同工作。

因为不同线程之间是独立运行的状态不可预测,所以一个线程与另一个线程间的数据是不同步的,当一个线程需要利用另一个线程的状态来确定自己的下一步操作时,就必须保持线程间数据的同步,Event就可以实现线程间同步

可用的一些方法:

event.isSet():返回event的状态值;
event.wait():将阻塞线程;知道event的状态为True
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False

案例:

 1 from threading import Event,Thread
 2 import time
 3 
 4 # 先生成一个event对象
 5 e = Event()
 6 
 7 
 8 def light():
 9     print('红灯正亮着')
10     time.sleep(3)
11     e.set()  # 发信号
12     print('绿灯亮了')
13 
14 def car(name):
15     print('%s正在等红灯'%name)
16     e.wait()  # 等待信号
17     print('%s加油门飙车了'%name)
18 
19 t = Thread(target=light)
20 t.start()
21 
22 for i in range(10):
23     t = Thread(target=car,args=('伞兵%s'%i,))
24     t.start()
View Code

 六.线程q对列

同一个进程下的多个线程本来就是数据共享 为什么还要用队列

因为队列是管道+锁  使用队列你就不需要自己手动操作锁的问题 

因为锁操作的不好极容易产生死锁现象

1.Queue 先进先出队列

与多进程中的Queue使用方式完全相同,区别仅仅是不能被多进程共享。

 案例:

q =  Queue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get(timeout=1))
print(q.get(timeout=1))
print(q.get(timeout=1))

2.LifoQueue 后进先出队列

该队列可以模拟堆栈,实现先进后出,后进先出

 案例:

lq = LifoQueue()

lq.put(1)
lq.put(2)
lq.put(3)

print(lq.get())
print(lq.get())
print(lq.get())

3.PriorityQueue 优先级队列

该队列可以为每个元素指定一个优先级,这个优先级可以是数字,字符串或其他类型,但是必须是可以比较大小的类型,取出数据时会按照从小到大的顺序取出

 案例:

pq = PriorityQueue()
# 数字优先级
pq.put((10,"a"))
pq.put((11,"a"))
pq.put((-11111,"a"))

print(pq.get())
print(pq.get())
print(pq.get())
# 字符串优先级
pq.put(("b","a"))
pq.put(("c","a"))
pq.put(("a","a"))

print(pq.get())
print(pq.get())
print(pq.get())

 补充:如何解决基于TCP的高并发

server:

 1 import socket
 2 from threading import Thread
 3 
 4 """
 5 服务端
 6     1.要有固定的IP和PORT
 7     2.24小时不间断提供服务
 8     3.能够支持并发
 9 """
10 
11 server = socket.socket()
12 server.bind(('127.0.0.1',8080))
13 server.listen(5)
14 
15 
16 def talk(conn):
17     while True:
18         try:
19             data = conn.recv(1024)
20             if len(data) == 0:break
21             print(data.decode('utf-8'))
22             conn.send(data.upper())
23         except ConnectionResetError as e:
24             print(e)
25             break
26     conn.close()
27 
28 while True:
29     conn, addr = server.accept()  # 监听 等待客户端的连接  阻塞态
30     print(addr)
31     t = Thread(target=talk,args=(conn,))
32     t.start()
View Code

client:

 1 import socket
 2 
 3 
 4 client = socket.socket()
 5 client.connect(('127.0.0.1',8080))
 6 
 7 while True:
 8     client.send(b'hello')
 9     data = client.recv(1024)
10     print(data.decode('utf-8'))
View Code
原文地址:https://www.cnblogs.com/zahngyu/p/11353069.html