Python多线程和多进程(三) 线程同步之条件变量

本系列文章目录
展开/收起
  • Python多线程和多进程(一) GIL锁和使用Thread创建多线程
  • Python多线程和多进程(二) 线程同步之互斥锁和重入锁
  • Python多线程和多进程(三) 线程同步之条件变量
  • Python多线程和多进程(四) 线程同步之信号量
  • Python多线程和多进程(五) 多线程管理——线程池
  • Python多线程和多进程(六) 多进程编程和同步


同步方式2:条件变量

首先,条件变量必须要配合互斥锁使用,因为条件变量是一种多线程竞争的共享资源。
通过条件变量可以实现等待和通知的机制。

最基本的使用方式为:

cond = Condition() # 创建一个条件变量
cond.acquire()   # 给条件变量上锁

cond.wait()     # 等待,会阻塞下面的代码执行,当其他线程调用notify的时候才会被唤醒
do_something()   
cond.notify()    # 通知和唤醒其他使用了条件变量cond的线程
cond.release()

 

其中:
cond.acquire()  其实就是用一个互斥锁上锁,相当于 lock.acquire()
cond.wait()     会干三件事:1.会释放互斥锁,让其他使用了相同条件变量的线程可以拿到锁来运行。2.进入阻塞休眠状态,让出CPU。3.当本线程的wait()被其他线程notify唤醒后,wait()会重新拿到锁。
cond.notify()    通知其他线程,唤醒其他线程的wait()。
cond.release()  释放互斥锁

wait()和release()必须在锁内执行。如果不获取锁就直接调用wait()和release()就会报错


PS:使用 
with cond:
    if my_condition:
        cond.wait()
    do_something()
    cond.notify()
    
和 
cond.acquire()
if my_condition:
    cond.wait()
do_something()
cond.notify()
cond.release()

是一样的。

使用with就相当于在代码外面包了一层锁。


例子1:控制多线程有序执行
有两个人A,B进行对话,分别说1~6这几个数字,要求A先说,A说完B才能说,B说完A才能再说
A:1
B:2
A:3
B:4
A:5
B:6

from threading import Condition,Thread

cond = Condition()
a_say = [1,3,5]
b_say = [2,4,6]

class A(Thread):
  def __init__(self,cond,say):
    super(A,self).__init__(name="A")
    self.cond = cond
    self.say = say

  def run(self):
    self.cond.acquire()
    for i in range(len(self.say)):
      print("%s say %d" % (self.name,self.say.pop(0)))
      self.cond.notify() # A说完就要通知B,让B开始说

      if len(self.say):
        self.cond.wait()  # A说完就不能在说,而是等待B说完,等B通知到A,A才能继续说

    self.cond.release()


class B(Thread):
  def __init__(self,cond,say):
    super(B,self).__init__(name="B")
    self.cond = cond
    self.say = say

  def run(self):
    self.cond.acquire()
    for i in range(len(self.say)):
      self.cond.wait()  # 一开始是A先说而不是B先说,所以一开始B是处于等待状态
      print("%s say %d" % (self.name,self.say.pop(0)))
      self.cond.notify() # B说完就要通知A,让A继续说

    self.cond.release()

if __name__=="__main__":
  a = A(cond,a_say)
  b = B(cond,b_say)

  b.start()    # 必须让b线程先启动,a后启动,如果a先启动,那么a会在b没有执行wait()的情况下执行notify(),所以这个notify()通知相当于无效。之后a执行wait().b也执行wait()双方都处于等待,于是这个进程就卡住
  a.start()


    
    

这里使用了一个条件变量来实现


当然,我们也可以使用多个互斥锁来实现多线程的有序执行,前面互斥锁的例子中已经展示。

 


例子2:用互斥锁,条件变量和列表实现一个线程安全的队列:
 

# coding=utf-8

from threading import Thread
from threading import Lock,Condition
import random

class ThreadSafeQueue:
  def __init__(self,max_size=0,blocking=True,timeout=None): # 默认队列没有限制最大空间
    self.max_size=max_size
    self.blocking=blocking # 默认等待阻塞
    self.timeout=timeout  # 默认等待时间无限长
    self.lock = Lock()
    self.cond = Condition(lock=self.lock) # 这个条件变量所使用的锁是自定义的互斥锁,而不使用Condition内部定义的重入锁
    self.queue = []

  def size(self):   # self.queue是线程共享资源,所有关于self.queue的使用都要加锁,包括查和改
    self.lock.acquire()
    size = len(self.queue)
    self.lock.release()

    return size

  def batch_push(self,items):
    if not isinstance(items,list):
      items=list(items)
    for item in items:
      self.push(item)

  def push(self,item):
    self.cond.acquire()
    while self.max_size>0 and len(self.queue)>=self.max_size:
      if self.blocking:
        res = self.cond.wait(timeout=self.timeout)  # 如果超过timeout还没被唤醒,则返回False
       
        if not res:
          self.cond.release()
          return False
      else:
        self.cond.release()
        return False

    self.queue.append(item)
    self.cond.notify()
    self.cond.release()


    return True

  def pop(self):
    self.cond.acquire()
    while len(self.queue)<=0:
      if self.blocking:
        res=self.cond.wait(timeout=self.timeout)
       
        if not res:
          self.cond.release()
          return False
      else:
        self.cond.release()
        return False

    item = self.queue.pop()
    self.cond.notify()     # 通知生产者可以继续生产
    self.cond.release()

    return item

  def get(self,index):
    self.lock.acquire()
    try:
      item = self.queue[index]
    except:
      item=None
    self.lock.release()

    return item

# 生产者
def produce(q,n):
  for i in range(100000):
    q.push(i)
    print("Thread %d push %d" % (n,i))

def consumer(q,n):
  count_none = 0 # 如果q.pop()阻塞次数大于10则停止while循环
  while True:
    item = q.pop()
    if item is False:
      count_none+=1
    else:
      count_none=0
      print("Thread %d pop %d" % (n,item))

    if count_none>=10:
      break


# 测试
if __name__=="__main__":
  queue = ThreadSafeQueue(1000)    # 测试阻塞队列,结果是,消费者消费完所有产品后阻塞等待新产品生产,一直处于等待状态
  # queue = ThreadSafeQueue(1000,timeout=1)    # 测试阻塞队列,结果是,消费者消费完所有产品后阻塞等待新产品生产,阻塞10次后自动跳出循环
  # queue = ThreadSafeQueue(1000,blocking=False)  # 测试非阻塞队列,结果是,生产者由于多次被阻塞而放弃了很多次生产产品,消费者消费完所有产品后直接结束

  # 创建两个生产者线程,一个消费者线程,使得生产产品的速度比消费产品的速度快,这样消费产品不会等待,而生产产品会等待
  t1 = Thread(target=produce,args=(queue,1))
  t2 = Thread(target=produce,args=(queue,2))
  t3 = Thread(target=consumer,args=(queue,3))
  t1.start()
  t2.start()
  t3.start()
  t1.join()
  t2.join()
  t3.join()
 

  
    
    


下面说一下Condition的底层是怎么实现的:
1.实例化 Condition 的时候,Condition的__init__会生成一个 RLock 重入锁,这个锁用于保护条件变量对象的使用。我们叫这把锁为 R

2.执行wait()前必须执行cond.acquire()对条件变量上锁,上的锁就是 R;
  执行wait() 的时候,wait()做了这么几件事:
  2-1. wait()会创建一个互斥锁,我们把这个互斥锁叫做X,并对X调用acquire()上锁: X.acquire(),之后将锁X放到一个双向队列 Q 中。
  2-2. wait()释放锁 R,这样其他线程才能获取锁R并执行一些任务代码
  2-3. wait()在释放 R之后,会再对X上一次锁,X.acquire() ; 由于连续对X上两次锁,所以会发生死锁,这样wait()进入阻塞状态。
  所以 条件变量的wait() 是通过死锁的方式来实现阻塞等待的功能的!!
  
  2-4.wait()的锁X被其他线程的notify()释放后,会重新对R上锁,锁X就再也不会被使用。下一次调用wait()的时候会生成一把新的X锁
  
3.其他线程获取到锁R,并执行一些任务代码,之后执行notify()唤醒之前那个线程的wait()
  notify()做了这么几件事:
  3-1. 从队列 Q 头部弹出锁 X,释放锁X。通过释放锁X实现唤醒wait()的。之后这个锁X就永远不会被用到了
  
  
总结: Condition的实现用了两把锁:
__init__()时创建的重入锁R 和 wait()是创建的互斥锁X
R用于保护条件变量和一些共享变量的线程安全
X不用于线程安全,而是用于制造死锁达成阻塞效果

R会重复使用,X是一次性使用,每次会生成新的X


下面贴出 Condition中__init__,wait()和notify()的源码:
 

class Condition:
 

  def __init__(self, lock=None):
    if lock is None:
      lock = RLock()   # 创建一个重入锁 R。如果手动传入 lock 则使用用户传入的lock。
    self._lock = lock

    self.acquire = lock.acquire
    self.release = lock.release
   
    try:
      self._release_save = lock._release_save
    except AttributeError:
      pass
    try:
      self._acquire_restore = lock._acquire_restore
    except AttributeError:
      pass
    try:
      self._is_owned = lock._is_owned
    except AttributeError:
      pass
    self._waiters = _deque()

 

  def wait(self, timeout=None):
    if not self._is_owned():
      raise RuntimeError("cannot wait on un-acquired lock")
    waiter = _allocate_lock()    #### 互斥锁 X ####
    waiter.acquire()        #### 对 X 上锁 ####
    self._waiters.append(waiter)  #### 将 X 添加到队列 Q ####
    saved_state = self._release_save()   #### 释放锁 R ####
    gotit = False
    try:  
      if timeout is None:
        waiter.acquire()    #### 对互斥锁 X 第二次上锁,达成死锁,实现了阻塞 ####
        gotit = True
      else:
        if timeout > 0:
          gotit = waiter.acquire(True, timeout)
        else:
          gotit = waiter.acquire(False)
      return gotit
    finally:
      self._acquire_restore(saved_state)   #### X被释放后,对R重新上锁 ####
      if not gotit:
        try:
          self._waiters.remove(waiter)
        except ValueError:
          pass

 
  def notify(self, n=1):
    if not self._is_owned():
      raise RuntimeError("cannot notify on un-acquired lock")
    all_waiters = self._waiters
    waiters_to_notify = _deque(_islice(all_waiters, n))
    if not waiters_to_notify:
      return
    for waiter in waiters_to_notify:
      waiter.release()    #### 释放锁 X ####
      try:
        all_waiters.remove(waiter)  #### 将锁X从队列Q中弹出####
      except ValueError:
        pass

 


    

本文转载自: 张柏沛IT技术博客 > Python多线程和多进程(三) 线程同步之条件变量

你可能感兴趣的:(python多线程与多进程,多线程,并发编程,python)