2.1 多线程:锁机制

1.python的多线程

多线程是提高效率的一种有效方式,但是由于 CPython 解释器中存在 GIL 锁,因此 CPython 中的多线程只能使用单核。也就是说 Python 的多线程是宏观的多线程,而微观上实际依旧是单线程。

2.多个线程同时修改全局变量

进行global 声明并对即修改即可。

import threading
import time

num = 0


def test1(nums):
    global num
    for i in range(nums):
        num += 1
    print("test1----num=%d" % num)


def test2(nums):
    global num
    for i in range(nums):
        num += 1
    print("test2----num=%d" % num)


def main():
    t1 = threading.Thread(target=test1, args=(100000,))
    t2 = threading.Thread(target=test2, args=(100000,))

    t1.start()
    t2.start()

    # t1.join()
    # t2.join()
    time.sleep(5)
    print("main-----num=%d" % num)

if __name__ == "__main__":
    main()

线程 1 到 CPU 中执行代码 num+=1 的时候,其实这一句代码要被拆分为 3 个步骤来执行:

  • 第一步:获取 num 的值;
  • 第二步:把获取的值 +1 操作;
  • 第三步:把第二步获取的值存储到 num 中;

在 CPU 中执行这三步的时候,并不能保证这三部一定会执行结束,再去执行线程 2 中的代码。

因为这是多线程的,所以 CPU 在处理两个线程的时候,是采用雨露均沾的方式,可能在线程一刚刚将 num 值 +1 还没来得及将新值赋给 num 时,就开始处理线程二了,因此当线程二执行完全部的 num+=1 的操作后,可能又会开始对线程一的未完成的操作,而此时的操作停留在了完成运算未赋值的那一步,因此在完成对 num 的赋值后,就会覆盖掉之前线程二对 num 的 +1 操作。

要解决这个问题就需要 锁。

3.互斥锁

当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

互斥锁为资源引入一个状态——锁定/非锁定。

某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能改变,直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

import threading

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

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

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

需要注意的是,lock.acquire() 和 lock.release() 必须成对出现。否则就有可能造成死锁。使用with语句上下文管理器来加锁可以避免死锁的出现:with 语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁。

import threading

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

互斥锁解决资源竞争:
此时输出的结果是没有问题的。互斥锁也会引发一个问题,就是死锁。

import threading
import time

num = 0
# 创建一个互斥锁,默认是没有上锁的
mutex = threading.Lock()

def test1(nums):
    global num
    mutex.acquire()
    for i in range(nums):
        num += 1
    mutex.release()
    print("test1----num=%d"%num)

def test2(nums):
    global num
    mutex.acquire()
    for i in range(nums):
        num += 1
    mutex.release()
    print("test1----num=%d" % num)

def main():
    t1 = threading.Thread(target=test1,args=(1000000,))
    t2 = threading.Thread(target=test2,args=(1000000,))

    t1.start()
    t2.start()
    time.sleep(2)
    print("main-----num=%d" % num)

if __name__ == "__main__":
    main()

4. 死锁

当多个线程几乎同一 时间的去修改某个共享数据的时候就需要我们进行同步控制,线程同步能够保证多个线程安全的访问竞争资源,最简单的就是引入互斥锁 Lock、可重入锁 RLock。这两种类型的锁有一点细微的区别。

像下面这种情况,就容易出现死锁。互相锁住了对方,又在等对方释放资源。

import threading  
 #Lock对象  
lock = threading.Lock()
#A 线程
lock.acquire(a)
lock.acquire(b)

#B 线程
lock.acquire(b)
lock.acquire(a)

当线程调用 lock 对象的 acquire() 方法时,lock 就会进入锁住状态,如果此时另一个线程想要获得这个锁,该线程就会变为阻塞状态,因为每次只能有一个线程能够获得锁,直到拥有锁的线程调用 lock 的 release() 方法释放锁之后,线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行状态。

上面这种情况比较容易被发现,还有一种情况不太容易被发现,**调用其他加锁函数,也可能造成死锁。**如下:

def add(lock):
    global total
    for i in range(100000):
        lock.acquire()
        task()
        total += 1
        lock.release()

def task():
    lock.acquire()
    # do something
    lock.release()

(1)避免死锁的方法:

  • 程序设计上尽量避免
  • 添加超时时间
  • 使用 try…except…finally 语句处理异常、保证锁的释放
  • with 语句上下文管理,锁对象支持上下文管理。只要实现了__enter__和__exit__魔术方法的对象都支持上下文管理。

5. 可重入锁

RLock 允许在同一线程中被多次 acquire ,如果出现 Rlock ,那么 acquire 和 release 必须成对出现,即调用了 i 次 acquire ,必须调用 i 次的 release 才能真正释放所占用的锁。
需要注意的是,可重入锁( RLock ),只在同一线程里放松对锁(通行证)的获取,意思是,只要在同一线程里,程序就当你是同一个人,这个锁就可以复用,其他的话与 Lock 并无区别。

import threading  
 #RLock对象  
rLock = threading.RLock() 
rLock.acquire()  
#在同一线程内,程序不会堵塞。
rLock.acquire()  
rLock.release()  
rLock.release()

6.锁的应用场景

独占锁: 锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候。
共享锁: 如果共享资源是不可变的值时,所有线程每一次读取它都是同一样的值,这样的情况就不需要锁。

7.使用锁的注意事项

  • 少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就变成了串行,要么排队执行,要么争抢执行。
  • 加锁时间越短越好,不需要就立即释放锁。
  • 一定要避免死锁。

8.示例

10个工人生产100杯子的例子, 当做到99个杯子时,10个工人都发现还少一个。不加锁的话,10个工人都去做了一个,一共做了109个,超出了100个,就发生了不可预期的结果。临界线判断失误,多生产了杯子。解决方法就可以用锁,来解决资源争抢。当一个人看杯子数量时,就上锁,其它人只能等着,看完杯子后发现少一个就把这最后一个做出来,然后数量加一,解锁,其他人再看到已经有100个杯子时,就可以停止工作。

加锁的时机非常重要:看杯子数量时加锁,增加数量后释放锁。

# Lock
import logging
import threading
import time

logging.basicConfig(level=logging.INFO)

# 10 -> 100cups
cups = []
lock = threading.Lock()


def worker(lock: threading.Lock, task=100):
    while True:
        if lock.acquire(False):
            count = len(cups)

            time.sleep(0.1)

            if count >= task:
                lock.release()
                break
            logging.info(count)

            cups.append(1)
            lock.release()
            logging.info("{} make 1........ ".format(threading.current_thread().name))
    logging.info("{} ending=======".format(len(cups)))


for x in range(10):
    threading.Thread(target=worker, args=(lock, 100)).start()

在使用了锁以后,虽然保证了结果的准确性,但是性能下降了很多。

一般来说加锁以后还要有一些功能实现,在释放之前还有可能抛异常,一旦抛出异常,锁是无法释放,但是当前线程可能因为这个异常被终止了,这就产生了死锁。所以使用 try…except…finally 语句处理异常、保证锁的释放。

9.读写锁

在某些情况下,与常规锁相比,读写锁可以提高程序的性能,但实现起来更复杂,并且通常使用更多资源来跟踪读者的数量。

决定使用哪种类型的互斥锁,一般的经验法则是:当线程从共享数据中读取远多于写入时,使用读写器锁,例如某些类型的数据库应用程序。如果程序的大多数线程都在写,不建议使用读写锁。
读写锁安装方式:

pip install -i https://pypi.douban.com/simple readerwriterlock


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tony Einstein

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值