[Python]锁

目录

1. 什么是锁?

2. 互斥锁的使用

3. 为何要使用锁?

4. 可重入锁(RLock)

5. 饱受争议的GIL(全局锁)


1. 什么是锁?

在开发中, 可以理解为通行证。

当你对一段逻辑代码加锁时,意味着在同一时间有且仅能有一个线程在执行这段代码。

在 Python 中的锁可以分为两种:

  1. 互斥锁
  2. 可重入锁

2. 互斥锁的使用

来简单看下代码,学习如何加锁,获取钥匙,释放锁。

import threading

# 生成锁对象,全局唯一
lock = threading.Lock()

# 获取锁。未获取到会阻塞程序,直到获取到锁才会往下执行
lock.acquire()

# 释放锁,归还锁,其他人可以拿去用了
lock.release()

需要注意的是,lock.acquire() 和 lock.release()必须成对出现。否则就有可能造成死锁。可以使用关键字with来避免忘记在使用完锁之后释放

import threading

lock = threading.Lock()
with lock:
    # 这里写自己的代码
    pass

with 语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁。

3. 为何要使用锁?

定义两个函数,分别在两个线程中执行。这两个函数 共用 一个变量 n 。因为线程没有先后之分,在执行的过程中会被随机抢占资源,导致结果不准确。举个例子:1000人同时在买100张火车票,

假如第一个人正在买第一张票,突然接了个电话,就中断买票,窗口就在等他,然后另外的窗口也有人在买票,这时候看见票还没卖掉,于是就把第一张票买了,这时候第一个打完电话,继续买票,窗口就会把第一张票再卖一次,就会出现一票售两人的情况。

这个情况下,如果第一个人买票的时候把票数锁定,其他人都买不了,那么等买票结束之后才能继续操作,就会避免这个问题。

当然,避免这个这个问题还有另外一种方法,线程同步。这里先说加锁的方法

def job1():
    global n
    for i in range(10):
        n+=1
        print('job1',n)

def job2():
    global n
    for i in range(10):
        n+=10
        print('job2',n)

n=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()

看代码貌似没什么问题,执行下看看输出

job1 1
job1 2
job1 job2 13
job2 23
job2 333
job1 34
job1 35
job2
job1 45 46
job2 56
job1 57
job2
job1 67
job2 68 78
job1 79
job2
job1 89
job2 90 100
job2 110

两个线程共用一个全局变量n,当job1 执行三次 +1 操作时,job2就不管三七二十一 给n做了+10操作。两个线程是随机执行的。所以会看到输出当然也很乱。

使用锁之后的代码为

def job1():
    global n, lock
    # 获取锁
    lock.acquire()
    for i in range(10):
        n += 1
        print('job1', n)
    lock.release()


def job2():
    global n, lock
    # 获取锁
    lock.acquire()
    for i in range(10):
        n += 10
        print('job2', n)
    lock.release()

n = 0
# 生成锁对象
lock = threading.Lock()

t1 = threading.Thread(target=job1)
t2 = threading.Thread(target=job2)
t1.start()
t2.start()

由于job1的线程,率先拿到了锁,所以在for循环中,没有人有权限对n进行操作。当job1执行完毕释放锁后,job2这才拿到了锁,开始自己的for循环,执行结果如下

job1 1
job1 2
job1 3
job1 4
job1 5
job1 6
job1 7
job1 8
job1 9
job1 10
job2 20
job2 30
job2 40
job2 50
job2 60
job2 70
job2 80
job2 90
job2 100
job2 110

所以,加锁是为了对锁内资源(变量)进行锁定,避免其他线程篡改已被锁定的资源,以达到我们预期的效果。

下面是一个例子:

实现根据输入的数组(数组元素限制为1,2,3的随机排列)来打印 first, second , third, 要求无论数组元素如何排列,second只能在first 完成之后执行,third 只能在second完成之后执行。

import threading
from threading import Lock


class Foo():
    def __init__(self):
        # 创建firstJob,secondJob 的锁对象
        self.firstJobdone = Lock()
        self.secondJobdone = Lock()
        # 获取firstJob,secondJob 的锁
        self.firstJobdone.acquire()
        self.secondJobdone.acquire()

    def first(self):
        print("First")
        # 执行完成,释放锁
        self.firstJobdone.release()

    def second(self):
        # 验证firstJob的锁是否释放,在释放的情况下执行print操作
        with self.firstJobdone:
            print("Second")
            # 执行完成,释放掉secondJob的锁
            self.secondJobdone.release()

    def third(self):
        # 验证secondJob的锁是否释放,在释放的情况下执行print操作
        with self.secondJobdone:
            print("Third")

# 测试代码
thread_list = []
foo = Foo()

num = [1, 3, 2]
for i in num:
    if i == 1:
        thread1 = threading.Thread(target=foo.first)
        thread_list.append(thread1)
    elif i == 2:
        thread2 = threading.Thread(target=foo.second)
        thread_list.append(thread2)
    elif i == 3:
        thread3 = threading.Thread(target=foo.third)
        thread_list.append(thread3)
    else:
        print("error")

for i in range(len(num)):
    thread_list[i].start()

4. 可重入锁(RLock)

有时候在同一个线程中,我们可能会多次请求同一资源,俗称锁嵌套。

如果还是按照常规的做法,会造成死锁的。比如,下面这段代码,你可以试着运行一下。会发现并没有输出结果。

import threading

def main():
    n = 0
    lock = threading.Lock()
    with lock:
        for i in range(10):
            n += 1
            with lock:
                print(n)

t1 = threading.Thread(target=main)
t1.start()

是因为第二次获取锁(通行证)时,发现锁(通行证)已经被同一线程的人拿走了,拿东西总有个先来后到,别人拿走了,你要想用,你就得干等着,直到有人归还锁(通行证),假如别人一直不归还,那程序就会在这里一直阻塞。

上面的代码中,使用了嵌套锁,在锁还没有释放的时候,又再一次请求锁,这就当然会造成死锁了。

那么如何解决这个问题呢?

threading模块除了提供Lock锁之外,还提供了一种可重入锁RLock,专门来处理这个问题。

import threading

def main():
    n = 0
    # 生成可重入锁对象
    lock = threading.RLock()
    with lock:
        for i in range(10):
            n += 1
            with lock:
                print(n)

t1 = threading.Thread(target=main)
t1.start()

执行之后

1
2
3
4
5
6
7
8
9
10

需要注意的是,可重入锁(RLock),只在同一线程里放松对锁(通行证)的获取,意思是,只要在同一线程里,程序就当你是同一个人,这个锁就可以复用,其他的话与Lock并无区别。

5. 饱受争议的GIL(全局锁)

在第一章的时候,我就和大家介绍到,多线程和多进程是不一样的。

多进程是真正的并行,而多线程是伪并行,实际上他只是交替执行。

是什么导致多线程,只能交替执行呢?是一个叫GILGlobal Interpreter Lock,全局解释器锁)的东西。

什么是GIL呢?

任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

需要注意的是,GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。而Python解释器,并不是只有CPython,除它之外,还有PyPyPsycoJPythonIronPython等。

在绝大多数情况下,我们通常都认为 Python == CPython,所以也就默许了Python具有GIL锁这个事。

都知道GIL影响性能,那么如何避免受到GIL的影响?

  • 使用多进程代替多线程。
  • 更换Python解释器,不使用CPython

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值