python多线程同步---信号量 Semaphore

python多线程编程中的Semaphore, 它内部维护了一个计数器,每一次acquire操作都会让计数器减1,每一次release操作都会让计数器加1,当计数器为0时,任何线程的acquire操作都不会成功,Semaphore确保对资源的访问有一个上限, 这样,就可以控制并发量

1. 内容回顾

在多线程的系列里,我已经讲解了Lock,RLock,Condition,在讲解信号量之前,不妨先做一个简单的内容回顾

1.1 Lock

对一个变量进行加1操作,远比我们以为的要复杂,从内存到寄存器,执行加1,写回内存,这就引申出了多线程情况下对共享资源的访问的问题。有些操作是线程安全的,有些操作是线程不安全的,对于那些线程不安全的操作,我们必须为这个操作加上一把锁,来确保同一个时刻,只能有一个线程进行操作,就像加1的操作,要等到彻底执行完,也就是写回到内存后才允许其他线程进行操作

1.2 RLock

RLock的作用和Lock一样,不同的地方在于,它可以被一个线程多次请求,只要你保证每一个acquire对应一个release就可以保证线程不会发生死锁,它比Lock更加安全,尤其当程序复杂时,推荐你使用RLock

1.3 Condition

Condition,也称作条件变量,它也可以实现多线程对共享资源的互斥访问,相比于Lock,RLock,它更加灵活,其灵活之处在于它提供了wait操作和notify操作。

假设这样一个场景,你有2个生产者线程,10个消费者线程,当商品数量大于10个的时候,生产者不再生产,商品数量为0时,不再消费,如果是用RLock来做线程间互斥,当商品数量为0时,按道理说消费者线程是不进行任何消费活动的,但是,这些消费者线程仍然在拼命的尝试来获得锁,得到锁以后,发现商品数量为0,于是再释放掉锁,紧接着去争抢锁。

如果是用Condition来做呢,消费者发现商品数量为0时,可以进行wait操作,此时,线程进入等待状态,再没有被唤醒之前,它是不会去争抢锁的,极端的情况是商品为0时,10个消费者都进入到wait状态,而这时,生产者获得锁,生产了商品,然后进行notify操作,去唤醒一个线程,当然这次唤醒的可能是另一个生产者,不过没关系,总会又一次唤醒的是消费者线程。

condition和Lock,RLock相比较,在生产者和消费者模型中,避免了不必要的对锁的争抢,更加高效的调用线程资源。

2. 信号量

来谈一谈Semaphore,它内部维护了一个计数器,每一次acquire操作都会让计数器减1,每一次release操作都会让计数器加1,当计数器为0时,任何线程的acquire操作都不会成功,Semaphore确保对资源的访问有一个上限, 这样,就可以控制并发量。

如果使用Lock,RLock,那么只能有一个线程获得对资源的访问,但现实中的问题并不总是这样,假设这样一个场景,一个线程安全的操作,同一个时刻可以允许两个线程进行,如果太多了效率会降低,那么Lock,Rlock,包括Condition就不适合这种场景。

我这里举一个不恰当的例子(因为我自己没这么干过,但有助于你理解)。假设你写了一个多线程爬虫,起10个线程去爬页面,10个线程访问过于频繁了,目标网站对你采取反爬措施。但你把线程数量降到2两个就没问题了。那么对于这个场景,你仍然可以启动10个线程,只是向目标网站发送请求的这个操作,你可以用Semaphore来控制,使得同一个时刻只有两个线程在请求页面

3. 示例代码

import threading
import time
semaphore = threading.Semaphore(2)


def worker(id):
    print('thread {id} acquire semaphore'.format(id=id))
    semaphore.acquire()
    print('thread {id} get semaphore do something'.format(id=id))
    time.sleep(2)
    semaphore.release()
    print('thread {id} release semaphore'.format(id=id))


for i in range(10):
    t = threading.Thread(target=worker, args=(i, ))
    t.start()

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案