---恢复内容开始---
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类似于是加在解释器上面的一把锁
什么是GIL:首先来看看官方的解释
在CPython中,这个全局解释器锁,也称之为GIL,是一个互斥锁,
防止多个线程在同一个时间执行Python字节码,这个锁是非常正要的,
因为CPython的内存管理非线程安全的,很多其他的特性依赖于GIL,
所以即使他影响了程序的效率也无法将其直接去除
总结:在CPython中,GIL会把线程的并行变成串行,导致效率降低
PS:需要知道的是,解释器并不只有CPython,还有PyPy,JPython等等。
GIL也仅存在于CPython中,这并不是python这门语言的问题,而是CPython解释器的问题
2.GIL带来的问题:
首先必须明确执行一个py文件,分为三个步骤
-
从硬盘加载Python解释器到内存
-
从硬盘加载py文件到内存
-
解释器解析py文件内容,交给CPU执行
其次需要明确的是每当执行一个py文件,就会立即启动一个python解释器,
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已经被清理掉了,导致无法正常定义变量。
有了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))
案例: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))
总结:
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)
三.死锁
死锁问题
当程序出现了不止一把锁,分别被不同的线程持有, 有一个资源 要想使用必须同时具备两把锁
这时候程序就会进程无限卡死状态 ,这就称之为死锁
案例:
案例:
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()
四.信号量
可以现在被锁定的代码 同时可以被多少线程并发访问
互斥锁: 锁住一个马桶 同时只能有一个
信号量: 锁住一个公共厕所 同时可以来一堆人
用途: 仅用于控制并发访问 并不能防止并发修改造成的问题
案例:
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()
五.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()
六.线程q对列
同一个进程下的多个线程本来就是数据共享 为什么还要用队列 因为队列是管道+锁 使用队列你就不需要自己手动操作锁的问题 因为锁操作的不好极容易产生死锁现象
1.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()
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'))