python 基础(三) 线程 Threading GIL 线程同步
Threading 基本语法
其实和Process类几乎一样,简单看看一个用了四线程模拟抢票的实例:
#-*- utf-8 -*-
from threading import Thread
from random import random
from time import time,sleep
ticket_remain = 400
def getTicket(q):
global ticket_remain
for i in range(100):
ticket_remain-=1
if __name__ == "__main__":
t1 = Thread(target=getTicket, name="t1", args=(0.4,))
t2 = Thread(target=getTicket, name="t1", args=(0.4,))
t3 = Thread(target=getTicket, name="t1", args=(0.4,))
t4 = Thread(target=getTicket, name="t1", args=(0.4,))
t1.start()
t2.start()
t3.start()
t4.start()
t1.join()
t2.join()
t3.join()
t4.join()
print("ticket remain:",ticket_remain)
理解了语法后我们可以更简洁一点
#-*- utf-8 -*-
from threading import Thread
ticket_remain = 400
def getTicket(q):
global ticket_remain
for i in range(100):
ticket_remain-=1
if __name__ == "__main__":
for i in range(4):
thread = Thread(target=getTicket, name="t"+str(i), args=(0.4,))
thread.start()
thread.join()
print("ticket remain:",ticket_remain)
结果当然是
线程并发具体过程
之前在python 基础(一)提到过,线程是共用系统资源的,包括CPU的计算资源,其实现形式就是并发concurrent,这个概念之前提到过,只要是单核,或者单个运算单元处理多项运算程序,以轮询的方式处理的都称为并发,这里一个进程process里面的多个线程threadings要同时使用一个运算资源,也属于并发。
上一节提到一个抢票的案例,我们把时间定格在最后两张票的时候,大概具体是这样的过程:(t1 线程1,t2 线程2)
线程操作 | CPU操作 | 数据库n的值 |
---|---|---|
t1获得CPU使用权 t2就绪挂起 | CPU调度t1 | n=2 |
t1执行:n=n-1 | 执行n-1=0 | n=2 |
t1执行:n=n-1 | 执行n = 0 | n=2 |
t1确认抢到票了,函数返回 | 接收函数返回 | n=1 |
t1就绪挂起 t2获得执行权 | CPU调度t2 | n=1 |
其他就和t1 相同套路
我们来看下一个案例:
#-*- utf-8 -*-
from threading import Thread
ticket_remain = 2000000
def getTicket1(q):
global ticket_remain
for i in range(1000000):
ticket_remain-=1
print(q," ticket remained:", ticket_remain)
def getTicket2(q):
global ticket_remain
for i in range(1000000):
ticket_remain-=1
print(q," ticket remained:", ticket_remain)
if __name__ == "__main__":
t1 = Thread(target=getTicket1, name="aa", args=("t1",))
t1.start()
t2 = Thread(target=getTicket2, name="aa", args=("t2",))
t2.start()
t1.join()
t2.join()
print("ticket remain:",ticket_remain)
结果是0?
就好像抢到的是同一张票一般
其实也确实是这样的 我们来看上一次那个过程表
还是把时间定格在最后一张票的时候,
t1 在cpu的执行顺序就是
ticket_remain-1 = 0
ticket_remain = 0
print(q," ticket remained:", ticket_remain) #意思,确定获得了票 数据库可以更新了
大概具体是这样的过程:(t1 线程1,t2 线程2,n是剩余票数ticket_remain)
线程操作 | CPU操作 | 数据库n的值 |
---|---|---|
t1获得CPU使用权 t2就绪挂起 | CPU调度t1 | n=1 |
t1执行:n=n-1 | 执行n-1=0 | n=1 |
t1执行:n=n-1 | 执行n = 0 | n=1 |
t2获得CPU使用权 t1就绪挂起 | CPU调度t2 | n=1 |
t2执行:n=n-1 | 执行n-1=0 | n=1 |
t2执行:n=n-1 | 执行n = 0 | n=1 |
t2确认抢到票了,print(), 函数返回 | 接收函数返回 | n=0 |
t1获得CPU使用权 t2就绪挂起 | CPU调度t1 | n=0 |
t1 也 确认抢到票了,print(), 函数返回 | 接收函数返回 | n=0 |
线程执行中如果被打断,那么再次获得CPU执行权的时候只会继续执行未执行完的语句(比如调t1在还没print的时候被打断,下次夺回执行权的时候就会在打印)
这里 t1 t2抢的其实是同一张票,都是最后一张票
那么 为啥之前我们只有几百张票的时候不会有这个问题?
这就是Cpython的一个底层大“BUG”——GIL,其实也应该说是特性,主要是为了数据安全
GIL Global Interpreter Lock 全局锁
我们看到,之所以出现这样的问题,完全是由于,我抢票线程中被打断了,被阻塞了,那么如果加一个安全保护机制——锁,就好像精子卵子结合的时候,卵子接收完一个精子后就锁住自己不和别的精子结合。这样用在线程的锁就是GIL(Global Interpreter Lock)
优点显而易见,可以避免数据出问题——也就是所谓数据安全。
数据安全data security 不是说数据是否被盗用,而是数据是否按照我们的程序,我们的预期,从而有规律的变化。
——好像我还是没回答为啥数据量小会导致 上锁,数据大了就不上锁
好吧我的锅:这是因为,数据量大了他会主动释放掉GIL,为啥呢?
CPython比较机智,当检测到你线程运算量太大,严重延误后面的线程(我们能够跑代码感觉到要等好几秒),于是取消保护机制,宁可数据错,也不能太慢。
那,如果我就需要他有些时候数据一定不能错怎么办?他会释放锁啊?这时我们得用到一个锁对象
,具体请看下集。
复习 线程同步
那么有了锁以后,结合上一节(传送门:python 基础(二)阻塞 非阻塞 同步 异步)我们说的同步与异步的知识,我们可以发现:线程抢占CPU计算资源的时候是,CPU调用线程是异步的(asynchronous),线程执行是非阻塞的 (non-blocking),
然而因为GIL,每个线程只能一个个执行,也就是 串行顺序(serial sequential) 的,CPU调用线程也是 同步的synchronous 这就是所谓 线程同步
这样确实是安全了,问题是如果遇到大量计算都在一个线程里面,时间该有多长?另外最大的问题是效率低下,这还算是“多线程”嘛,加上线程start join操作的时间,还不如单线程来得快,而且肯定数据安全:)
所以说,Cpython环境下,默认给每个线程添加的GIL是一个“BUG”
下面是拓展阅读:
解释器 编译器 GIL的来源
什么是解释器Interpreter?在这之前我们得看看两种语言类型。
C++是一套语言(语法)标准,是编译型语言,以生成可执行文件为目的,具体可以用不同的编译器complier来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。
Python是 解释型语言 ,不能生成可执行文件,但是具体可以用不同的解释器Interpreter来编译成可执行代码。同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。其中CPython最普遍,某些场合甚至直接代替python执行环境。
我们的GIL就是CPython底层代码的产物,所以GIL的锅不能全甩给Python:)
问题
#-*- utf-8 -*-
from threading import Thread
ticket_remain = 40000000
def getTicket(q):
global ticket_remain
for i in range(10000000):
ticket_remain-=1
if __name__ == "__main__":
for i in range(4):
thread = Thread(target=getTicket, name="t"+str(i), args=(0.4,))
thread.start()
thread.join()
print("ticket remain:",ticket_remain)
为什么当target执行目标是相同的函数不会出现数据崩掉的问题,数据量比上个例子还要大。按理来说,虽然是相同函数,但是是不同的线程,理应本质相同。
期待大佬回答:)
下集预告
下一站比较轻松,因为硬核部分已经搞定勒。
就是学习如何人为加锁(GIL是默认自动加锁的)
传送:python 基础(四)锁 threading.Lock 死锁