Python入门(二十八)——线程“锁”复习(一般锁.Lock()、死锁、递归锁.RLock()、信号量.Semaphore(n)、条件变量同步.Condition())

本文详细介绍了Python中的线程同步概念,包括为何需要锁、如何使用Lock避免数据混乱、死锁的产生及解决、递归锁RLock的使用、Semaphore信号量在资源限制场景的应用,以及Condition条件变量在等待特定条件满足时的线程同步策略。通过实例解析,深入理解Python多线程中的同步问题。
摘要由CSDN通过智能技术生成

1. 为什么要有锁

先看一个例子:

# Author:AD
# Date:2020/3/7
import threading, time

sum = 0  # 以下是对公共变量的修改
class MyThread(threading.Thread):
    def __init__(self, arg):
        threading.Thread.__init__(self)
        self.arg = arg

    def run(self):
        start = time.time()
        global sum
        for i in range(self.arg): #(100000000):
            sum += i
        end = time.time()
        print(self, sum, end-start)

t1 = MyThread(10000000)
t2 = MyThread(10000000)
t1.start()
t2.start() 
print('主线程也在跑')

运行结果:发现结果混乱,是因为有可能在某一次执行的时候共同拿到了同一个全局变量
在这里插入图片描述

2. 加入Lock()

建立锁对象
mutex = threading.Lock()
加锁
mutex.acquire()
释放锁
mutex.release()

# Author:AD
# Date:2020/3/7
import threading, time

sum = 0  # 以下是对公共变量的修改
class MyThread(threading.Thread):
    def __init__(self, arg):
        threading.Thread.__init__(self)
        self.arg = arg

    def run(self):
        start = time.time()
        global sum
        for i in range(self.arg): #(100000000):
            # 在对全局变量修改的地方加锁即可
            mutex.acquire()
            sum += i
            mutex.release()
        end = time.time()
        print(self, sum, end-start)

t1 = MyThread(10000000)
t2 = MyThread(10000000)
mutex = threading.Lock()  # 同一把锁在同一时刻只能被一个线程获取,其他会在acquire处阻塞
t1.start()
t2.start()
print('主线程也在跑')

运行结果:
在这里插入图片描述

上述方式导致计算密集型式的加锁,内部一直在不停的加锁解锁,耗时十倍,还可以按照如下方式加锁:

'''高效率加锁方式'''
import threading, time

sum = 0  # 以下是对公共变量的修改
class MyThread(threading.Thread):
    def __init__(self, arg):
        threading.Thread.__init__(self)
        self.arg = arg

    def run(self):
        start = time.time()
        global sum
        # 在对全局变量修改的地方加锁即可
        mutex.acquire()
        for i in range(self.arg): #(100000000)
            sum += i
        mutex.release()
        end = time.time()
        print(self, sum, end-start)

t1 = MyThread(10000000)
t2 = MyThread(10000000)
mutex = threading.Lock()  # 同一把锁在同一时刻只能被一个线程获取,其他会在acquire处阻塞
t1.start()
t2.start()
print('主线程也在跑')

在这里插入图片描述

3. 产生死锁:

import threading, time


mutex1 = threading.Lock()
mutex2 = threading.Lock()

def foo1():
    mutex1.acquire()
    time.sleep(0.5)
    mutex2.acquire()
    time.sleep(0.5)
    mutex2.release()
    mutex1.release()
    print('foo1 over')

def foo2():
    mutex2.acquire()
    time.sleep(0.5)
    mutex1.acquire()
    time.sleep(0.5)
    mutex1.release()
    mutex2.release()
    print('foo2 over')

t1 = threading.Thread(target=foo1, args=())
t2 = threading.Thread(target=foo2, args=())
t1.start()
t2.start()
print('main flow')

会发现两把锁互相锁定,程序阻塞

4. 递归锁RLock解决死锁

两个特点:

  • 一个计数器(可多层加锁)
  • 一把锁(谁拿谁是释放)

递归锁内部有计数机制,会判断锁了几次,直到某个线程将递归锁技术清零才会释放。

一般可以这样写:

mutex = threading.RLock()
with mutex:

pass

上述with代码等同于加锁再解锁,递归锁是最常用的,多见于谁拿谁释放,并未见到解决死锁的例子,

以下利用银行账户实例进行递归锁的理解(其实用一般锁也可以)

# Author:AD
# Date:2020/3/9
import time

import threading

class Account:
    def __init__(self, _id, balance):
        self.id = _id
        self.balance = balance
        self.lock = threading.RLock()

    def withdraw(self, amount):

        with self.lock:
            self.balance -= amount

    def deposit(self, amount):
        with self.lock:
            self.balance += amount


    def drawcash(self, amount):#lock.acquire中嵌套lock.acquire的场景

        with self.lock:
            interest=0.05
            count=amount+amount*interest

            self.withdraw(count)


def transfer(_from, to, amount):

    #锁不可以加在这里 因为其他的其它线程执行的其它方法在不加锁的情况下数据同样是不安全的
     _from.withdraw(amount)

     to.deposit(amount)

     #_from.drawcash(amount)



alex = Account('alex',1000)
yuan = Account('yuan',1000)

t1=threading.Thread(target = transfer, args = (alex,yuan, 100))
t1.start()

t2=threading.Thread(target = transfer, args = (yuan,alex, 200))
t2.start()

t1.join()
t2.join()

print('>>>', 'alex', alex.balance)
print('>>>', 'yuan', yuan.balance)

在这里插入图片描述
可以发现每个对象都有一把锁,每次操作对象内的字段的时候进行上锁,无论多少个线程进行操作都会保证数据的安全。

如果把函数中的取现放开会发现下面的结果:
在这里插入图片描述
而将Rlock换为Lock会发现在取现的时候一般锁锁一次,再遇到第二次就会阻塞。

实验曲线不加两把锁也是正常的,也就是局部变量并无所谓。

5. 信号量 .Semaphore

线程中,信号量主要是用来维持有限的资源,使得在一定时间使用该资源的线程只有指定的数量, 爬虫、数据库等使用

与递归锁的区别:

  • 递归锁在某线程进行加锁的过程中层层封闭,知道该线程将所有的锁都释放,其他线程才能进行操作
  • 而信号量规定了最大线程数目,可以同时锁定多少线程
  • 信号量并不是用来维护数据安全的

.BoundedSemaphore(value=maxconnections) 与 .Semaphore区别:
前者.release次数超过value会抛异常,后者不会。

完全可以用以下方案来解决这个问题:

maxconnections = 3
sema = threading.BoundedSemaphore(value=maxconnections)
with sema:
 	pass

'''上述代码与下等同'''
sema.acquire()
pass
sema.release()

在这里插入图片描述
爬虫控制演示:

import threading
import time
import random

sites = ["https://www.baidu.com/", "https://github.com/Fiz1994", "https://stackoverflow.com/",
         "https://www.sogou.com/",
         "http://english.sogou.com/?b_o_e=1&ie=utf8&fr=common_index_nav&query="] * 20
sites_index = 0
maxconnections = 3
pool_sema = threading.BoundedSemaphore(value=maxconnections)

class MyThread(threading.Thread):
    def run(self):
        with pool_sema:
            global sites_index, sites
            url = str(sites[sites_index])
            k = random.randint(1, 10)
            print(self, "爬去: " + url + " 需要时间 : " + str(k))
            sites_index += 1
            time.sleep(k)
            print(self, '退出 ', url)


for i in range(10):
    MyThread().start()

在这里插入图片描述

6. 条件变量同步.Condition()

有一类线程需要满足条件之后才能够继续执行,Python提供了threading.Condition 对象用于条件变量线程的支持,它除了能提供RLock()或Lock()的方法外,还提供了 wait()、notify()、notifyAll()方法。

lock_con=threading.Condition([Lock/Rlock]): 锁是可选选项,不传人锁,对象自动创建一个RLock()。

wait():条件不满足时调用,线程会释放锁并进入等待阻塞
notify():条件创造后调用,通知等待池激活一个线程,并从acquire处开始执行
notifyAll():条件创造后调用,通知等待池激活所有线程。

  • acquire(timeout)
    调用Condition类关联的Lock/RLock的acquire()方法。

  • release()
    调用Condition类关联的Lock/RLock的release()方法。

  • wait(timeout)
    1)线程挂起,直到收到一个notify通知或者等待时间超出timeout才会被唤醒;
    2)注意:wait()必须在已获得Lock的前提下调用,否则会引起RuntimeError错误。

  • notify(n=1)
    1)唤醒在Condition的waiting池中的n(参数n可设置,默认为1)个正在等待的线程并通知它,受到通知的线程将自动调用acquire()方法尝试加锁;
    2)如果waiting池中有多个线程,随机选择n个唤醒;
    3)必须在已获得Lock的前提下调用,否则将引发错误。

  • notify_all()
    唤醒waiting池中的等待的所有线程并通知它们。

import threading
from time import sleep
import random

# 商品
product = 1500
# 条件变量
con = threading.Condition(threading.Lock())


# 生产者类
# 继承Thread类
class Producer(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        global product
        while True:
            # 如果获得了锁
            if con.acquire():
            # if 1:
                # con.acquire()
                # 处理产品大于等于500和小于500的情况
                if product >= 500:
                    # 如果大于等于500,Producer不需要额外操作,于是挂起
                    print(self.name + " >500")
                    con.wait()
                    #con.notify()
                else:
                    product += 50
                    message = self.name + " produced 50 products."
                    print(message)
                    # 处理完成,发出通知告诉Consumer
                    con.notify()
                # 释放锁
                con.release()
                sleep(5)


# 消费者类
# 继承Thread类
class Consumer(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        global product
        while True:
            # 如果获得了锁
            if con.acquire():
            # if 1:
                #con.acquire()
                # 处理product小于等于100和大于100的两种情况
                if product <= 100:
                    # 如果小于等于100,Consumer不需要额外操作,于是挂起
                    print(self.name + " <100")
                    con.wait()
                    #con.notify()
                else:
                    product -= 10
                    message = self.name + " consumed 10 products."
                    print(message)
                    # 处理完成,发出通知告诉Producer
                    con.notify()
                # 释放锁
                n = random.random()
                sleep(n)
                con.release()
                n = random.randint(1,5)
                sleep(n)



def main():
    # 创建两个Producer
    for i in range(2):
        p = Producer('【Producer】-%d' % i)
        p.start()
    # 创建三个Consumer
    for i in range(3):
        c = Consumer('Consumer-%d' % i)
        c.start()




if __name__ == '__main__':
    main()

在这里插入图片描述
改进了网上的一个例子,实验发现以下结论

  • wait释放锁,并进入wait池
  • 不管是wait释放的锁还是release释放的锁,对于所有激活的线程,如果运行到acquire语句得话都存在竞争抢锁的关系
  • 可能notify的话会激活wait池里面的线程进行抢锁
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值