2.并发编程
2.5 线程
2.5.1 线程概念
1)线程是计算机中能够被操作系统调度(给CPU执行)的最小单位。
同一个进程中的多个线程,可以同时被不同的CPU执行
进程之间数据隔离,线程之间数据共享,线程数据不安全,线程开启关闭切换时间开销小。
一般情况下我们开启的进程数不会超过CPU个数的2倍,线程可以开很多50、100
2)CPython的gc(垃圾回收)机制:引用计数+分代回收
为了确保gc机制中引用计数的准确性,CPython使用全局解释器锁GIL。
3)全局解释器锁GIL(Global Interpreter Lock)导致了一个进程中的多个线程同一时刻只能有一个线程真正被CPU执行。GIL负责各个线程轮转执行
4)提高程序运行速度,主要考虑节省I/O操作时间,而不是CPU计算时间,因为CPU的计算速度非常快。大部分情况下,我们没有办法把一条进程中所有的I/O操作都规避掉。多线程主要是为了节省I/O操作时间。
注意:JPython不用gc机制, 可以在一个进程中使用多核。
2.5.2 threading:线程模块
1)多线程并发
2)join():产生同步阻塞
注意:进程可以使用terminate()方法强制关闭,线程不可以从外部关闭。线程只能执行完成所有代码后自己关闭。
3)ident查看线程id
4)current_thread():获取当前线程对象
5)current_thread().ident:线程id
6)enumerate()获取线程对象列表:存储了所有活着的线程对象,包括主线程。
导入enumerate方法后,会和内置函数enumerate重名。导致内置函数不能使用
处理方法:
(1)导入时使用as起别名
(2)导入threading,使用threading.enumerate()
7)active_count:存储了所有活着线程的数量。
2.5.3 面向对象启动线程
2.5.4 线程之间的数据是共享的
2.5.5 守护线程
主线程会等子线程结束之后才结束。因为主线程结束进程就会结束,进程结束进程的资源就会被回收,进程内的线程无法运行。
子进程死循环,会一直执行:
子进是守护进程,程序很快结束:
守护线程在主线程代码结束后,会继续守护其它子线程,所有子线程结束后,守护线程才会结束。
主进程代码结束后,守护进程不会守护其它子进程。
主线程代码结束后,守护线程会守护其它子线程
工作过程:
其它子线程结束-->主线程结束-->主进程结束-->进程资源被回收-->守护线程也被回收
2.5.6 线程锁:互斥锁(重要)
1)执行加减操作时数据不安全
CPU指令执行过程:
dis函数将python代码编译成cpu指令
如果a加载后但存储前,被GIL锁轮转;再次轮转回来时,会把计算结果直接保存,导致数据错误。
数据共享并且多线程异步执行以下操作时,数据不安全:
+=、-=、*=、/=、=、while、if
多个进程执行if判断时存在数据,但执行pop时,数据可能已被其它进程弹空了,导致pop操作报错。while操作与if类似,所以while和if也是不安全的。
2)执行append时数据是安全的
执行append或pop时,只是添加或弹出一个数据。如果在执行中被GIL轮转,转回时继续添加或弹出即可,不会破坏数据。
append pop数据安全,列表或字典中的方法去操作全局变量的时候,数据是安全的。另外Queue和logging也是数据安全的
3)添加线程锁,保护数据
增加线程锁后,如果a加载后但存储前,被GIL锁轮转。由于其它线程没有钥匙被阻塞,只能轮转回当前线程继续操作,保证了数据的安全。
if判断加锁
4)单例模式加锁
在类中增加阻塞,由于if语句在线程并发中的不安全性,导致单例模型失效
给单例模式加锁
小贴士:多线程同时操作全局变量或类里操作静态变量,会出现数据不安全
2.5.7 线程锁:递归锁
递归锁可以多次acquire(拿钥匙),多次release(还钥匙)
1)互斥锁无法多次拿钥匙
2)递归锁可以多次拿钥匙
拿几次钥匙,放几次钥匙;代码可以顺利执行
注意:递归锁的效率比互斥锁低
3)嵌套使用递归锁
在fun_1内调用fun_2时,如果使用互斥锁程序会阻塞,使用递归锁可以正常运行
2.5.8 死锁现象
多把锁(包括递归锁和互斥锁),在多个线程中交叉使用,容易出现死锁现象
快速解决办法:将所有锁都统一成一把递归锁,但这样修改会降低代码的运行效率。
noodle_lock = fork_lock = RLock()
2.5.9 queue:队列
队列是线程之间数据安全的容器,先进先出。
原理:加锁 + 链表
实现先进先出,利用列表处理效率低
队列分类: Queue先进先出、LifoQueue 后进先出、PriorityQueue 优先级队列
每种队列都有4种方法:put、get、put_nowait、get_nowait,各种队列的用法相同
1)Queue先进先出
(1)get、put
(2)put_nowait:会导致数据丢失,一般不使用
队列数据装满后,使用put方法继续存储数据,程序不会报错,当程序会阻塞无法继续执行。使用put_nowait程序会报错,该错误是queue模块内部的错误,可以使用try方法捕获错误
(3)get_nowait:
队列数据取空后,使用get方法继续取数据,程序不会报错,当程序会阻塞无法继续执行。使用get_nowait程序会报错,该错误是queue模块内部的错误,可以使用try方法捕获错误
2)LifoQueue:last in first out 后进先出:栈
3)PriorityQueue:优先级队列(VIP)
按ASCII值进行排序