python 基础(三) 线程 Threading GIL 线程同步

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调度t1n=2
t1执行:n=n-1执行n-1=0n=2
t1执行:n=n-1执行n = 0n=2
t1确认抢到票了,函数返回接收函数返回n=1
t1就绪挂起 t2获得执行权CPU调度t2n=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调度t1n=1
t1执行:n=n-1执行n-1=0n=1
t1执行:n=n-1执行n = 0n=1
t2获得CPU使用权 t1就绪挂起CPU调度t2n=1
t2执行:n=n-1执行n-1=0n=1
t2执行:n=n-1执行n = 0n=1
t2确认抢到票了,print(), 函数返回接收函数返回n=0
t1获得CPU使用权 t2就绪挂起CPU调度t1n=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 死锁

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值