Python零基础从小白打怪升级中~~~~~~~多线程

线程安全和锁

一、全局解释器锁

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。

GIL全称global interpreter lock,全局解释器锁。

每个线程在执行的时候都需要先获取GIL,保证同一时刻只有一个线程可以执行代码,即同一时刻只有一个线程使用CPU。在CPython中,每一个Python线程执行前都需要去获得GIL锁 ,获得该锁的线程才可以执行,没有获得的只能等待 ,当具有GIL锁的线程运行完成后,其他等待的线程就会去争夺GIL锁,这就造成了,在Python中使用多线程,但同一时刻下依旧只有一个线程在运行 ,所以Python多线程其实并不是「并行 」的,而是「并发」 。

看到下图,图中是Python中GIL的工作实例,其中有3个线程,线程与线程之间是顺序执行的 ,每个线程开始执行时都会去获得GIL,防止其他线程线程运行 ,每执行完一段时间后,就会释放GIL,让别的线程可以去争夺执行权限,如果自己本身也没有执行完,则本身也会参与这次争夺 。

python 复制代码
# 多线程的代码
import threading, time


def add(n):
    sum = 0
    while sum < n:
        sum += 1
    print(f'sum:{sum}')


if __name__ == '__main__':
    start = time.time()
    n = 500000000
    t1 = threading.Thread(target=add, args=[n // 2])
    t2 = threading.Thread(target=add, args=[n // 2])
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('run time: %s' % str(time.time() - start))
python 复制代码
# 单线程的代码
import time


def add(n):
    sum = 0
    while sum < n:
        sum += 1
    print(f'sum:{sum}')


if __name__ == '__main__':
    start = time.time()
    add(500000000)
    print('run time: %s' % str(time.time() - start))

总结

  • GIL解决方法:

  • 使用其他语言写的python解释器(不推荐,还是用官方CPython好)

    eg:Jython(java);IronPython(.net);pypy(Python)

  • 不使用多线程,使用多进程-进程里加协程实现多任务来充分利用多核CPU (推荐)

  • 即使存在GIL 在有IO等待操作的程序中,还是多线程快,当然没有资源等待的还是单线程快(科学计算,累加等等)

但需要注意的是线程有了GIL后并不意味着使用Python多线程时不需要考虑线程安全 ,「GIL的存在是为了方便使用C语言编写CPython解释器的编写者,而顶层使用Python时依旧要考虑线程安全」 。

二、线程安全

当多个线程同时访问一个对象时,不管如何计算,如果调用这个对象的行为都可以获得正确的结果,那就称这个对象时线程安全的。 如果出现了"脏数据"。则线程不安全。

脏数据 :产生脏数据的原因是,当一个线程在对数据进行修改时,修改到一半时另一个线程读取了未经修改的数据并进行修改。如何避免脏数据的产生呢?一个办法就是用join方法,即先让一个线程执行完毕再执行另一个线程。但这样的本质是把多线程变成了单线程,失去了多线程的意义。另一个办法就是用线程锁。

python 复制代码
import threading

g_number = 0


def hello():
    global g_number
    for i in range(1000000):  # 加的次数越大越容易出现资源竞争问题
        g_number += 1
    print(f'thd1运行的结果为:{g_number}')


def world():
    global g_number
    for i in range(1000000):
        g_number += 1
    print(f'thd2运行的结果为:{g_number}')


if __name__ == '__main__':
    thd1 = threading.Thread(target=hello)
    thd2 = threading.Thread(target=world)

    thd1.start()
    thd2.start()

    # 阻塞等待
    thd1.join()
    thd2.join()
    print(g_number)  # 结果随机 可能小于等于2000000

三、锁

锁是Python提供给我们能够自行操控线程切换的一种手段,使用锁可以让线程的切换变的有序。

一旦线程的切换变的有序后,各个线程之间对数据的访问、修改就变的可控,所以若要保证线程安全,就必须使用锁。

threading模块中提供了5种最常见的锁,下面是按照功能进行划分:

  • 同步锁:lock(一次只能放行一个)
  • 递归锁:rlock(一次只能放行一个)
  • 条件锁:condition(一次可以放行任意个)
  • 事件锁:event(一次全部放行)
  • 信号量锁:semaphore(一次可以放行特定个)

1、同步锁

同一时刻的一个进程下的一个线程只能使用一个cpu,要确保这个线程下的程序在一段时间内被cpu执,那么就要用到同步锁。只需要在对公共数据的操作前后加上上锁和释放锁的操作即可。

死锁: 指两个或两个以上的线程或进程在执行程序的过程中,因争夺资源而相互等待的一个现象。

python 复制代码
import threading

g_number = 0
lock = threading.Lock()

def hello():
    global g_number
    for i in range(1000000):  # 加的次数越大越容易出现资源竞争问题
        with lock:
            g_number += 1
    print(f'thd1运行的结果为:{g_number}')


def world():
    global g_number
    for i in range(1000000):
        with lock:
            g_number += 1
    print(f'thd2运行的结果为:{g_number}')


if __name__ == '__main__':
    thd1 = threading.Thread(target=hello)
    thd2 = threading.Thread(target=world)

    thd1.start()
    thd2.start()

    # 阻塞等待
    thd1.join()
    thd2.join()
    print(g_number)  # 结果随机 可能小于等于2000000

2、递归同步锁

在同步锁的基础上可以做到连续重复使用多次acquire()后再重复使用多次release()的操作,但是一定要注意加锁次数和解锁次数必须一致,否则也将引发死锁现象。

递归锁RLock:它内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

3、条件锁

条件锁是在递归锁的基础上增加了能够暂停线程运行的功能。并且我们可以使用wait()与notify()来控制线程执行的个数。

注意:条件锁可以自由设定一次放行几个线程。

python 复制代码
import threading

currentRunThreadNumber = 0
maxSubThreadNumber = 10


def task():
    global currentRunThreadNumber
    thread_name = threading.currentThread().name

    with condLock:
        print("线程开始启动,并马上进入等待状态 : %s" % thread_name)
        condLock.wait()  # 暂停线程运行、等待唤醒
        print("线程唤醒了,开始运行后面的代码 : %s" % thread_name)
        currentRunThreadNumber += 1


if __name__ == "__main__":
    condLock = threading.Condition()

    for i in range(maxSubThreadNumber):
        subThreadIns = threading.Thread(target=task)
        subThreadIns.start()

    while currentRunThreadNumber < maxSubThreadNumber:
        notifyNumber = int(
            input("请输入要唤醒几个线程:"))

        with condLock:
            condLock.notify(notifyNumber)  # 放行

    print("main thread run end")

4、事件锁

事件锁是基于条件锁来做的,它与条件锁的区别在于一次只能放行全部,不能放行任意个数量的子线程继续运行。

我们可以将事件锁看为红绿灯,当红灯时所有子线程都暂停运行,并进入"等待"状态,当绿灯时所有子线程都恢复"运行"。

python 复制代码
import threading

maxSubThreadNumber = 3


def task():
    thread_name = threading.currentThread().name
    print("线程开始启动,并马上进入等待状态 : %s" % thread_name)
    eventLock.wait()  # 暂停运行,等待绿灯
    print("第一次绿灯打开,线程往下走:%s" % thread_name)
    eventLock.wait()  # 暂停运行,等待绿灯
    print("第二次绿灯打开,线程往下走:%s" % thread_name)


if __name__ == "__main__":
    eventLock = threading.Event()
    for i in range(maxSubThreadNumber):
        subThreadIns = threading.Thread(target=task)
        subThreadIns.start()
    eventLock.set()  # 设置为绿灯
    eventLock.clear()  # 设置为红灯
    eventLock.set()

5、信号量锁

Semaphore()

信号量锁也是根据条件锁来做的,它与条件锁和事件锁的区别如下:

  • 条件锁:一次可以放行任意个处于"等待"状态的线程
  • 事件锁:一次可以放行全部的处于"等待"状态的线程
  • 信号量锁:通过规定,成批的放行特定(指定)个处于"上锁"状态的线程
python 复制代码
import threading
import time

maxSubThreadNumber = 6


def task():
    thread_name = threading.currentThread().name
    with semaLock:
        print("线程获得锁,开始运行: %s" % thread_name)
        time.sleep(3)


if __name__ == "__main__":

    semaLock = threading.Semaphore(2)

    for i in range(maxSubThreadNumber):
        subThreadIns = threading.Thread(target=task)
        subThreadIns.start()
相关推荐
博观而约取厚积而薄发7 分钟前
Pytest 从入门到精通,一篇就够(超详细实战教程)
python·测试工具·单元测试·自动化·pytest
网络小白不怕黑14 分钟前
httpd监控
linux·运维·服务器
imzed18 分钟前
使用 Playwright + Pytest 构建 Web UI 自动化测试框架
python·自动化·pytest
普通网友23 分钟前
pytest一些常见的插件
开发语言·python·pytest
时空系31 分钟前
Python 高性能高压缩打包器 —— 基于 JianPy 语义分析引擎
python
cndes1 小时前
给Miniconda换源,让包下载更迅速
开发语言·python
Metaphor6921 小时前
使用 Python 添加、隐藏和删除 PDF 图层
python·pdf·图层
丨白色风车丨1 小时前
【Python 计算机视觉】基于 Dlib+OpenCV 实现实时人眼疲劳检测(闭眼预警)
python·opencv·计算机视觉
jump_jump2 小时前
深入理解 GraphQL:一条查询到底是怎么执行的
性能优化·api·graphql