Python中线程同步与线程锁

百般思念 提交于 2019-12-01 03:28:29

线程同步与线程锁

线程同步

概念
* 线程同步,线程间协同,通过某种技术,让一个线程访问某些数据时,其他线程不能访问这些数据,直到该线程完 成对数据的操作。

1.threading.Event对象

  • Event事件,是线程间通信机制中最简单的实现,使用一个内部的标记flag,通过flag的True或False的变化 来进行操作
名称 含义
event.set() 标记设置为True
event.clear() 标记设置为False
event.is_set() 标记是否为True
event.wait(timeout=None) 设置等待标记为True的时长,None为无限等待。等到返回True,未等到超时了返回False
  • 老板雇佣了一个工人,让他生产杯子,老板一直等着这个工人,直到生产了10个杯子
import threading import time import logging  logging.basicConfig(format="%(asctime)s %(threadName)s %(thread)s %(message)s",level=logging.INFO)  def worker(event:threading.Event,count = 10):     logging.info("我是worker工作线程")     cups = []     while True:         logging.info("制作了一个 cup")         time.sleep(0.2)         cups.append(1)         if len(cups)>=count:             event.set()             break     logging.info("制作完成:{}".format(cups))  def boss(event:threading.Event):     logging.info("我是boss")     event.wait()     logging.info("Good Job")  event = threading.Event() b = threading.Thread(target=boss,args=(event,)) w = threading.Thread(target=worker,args=(event,)) b.start() w.start() 

  • 使用同一个Event对象的标记flag。

  • 谁wait就是等到flag变为True,或等到超时返回False。不限制等待的个数。

  • wait的使用

from threading import Thread,Event import logging  logging.basicConfig(format="%(asctime)s %(threadName)s %(thread)s %(message)s",level=logging.INFO)  def worker(event:Event,interval:int):     while not event.wait(interval):         logging.info("没有等到。。")  e = Event() Thread(target=worker,args=(e,1)).start()  e.wait(5) e.set()  print("======end========") 

2.threading.Timer定时器,延迟执行

方法 含义
Timer.cancel() 取消定时器,(定时器为执行函数时可以取消,在函数执行中无法取消)
Time.start() 启动定时器
  • threading.Timer继承自Thread,这个类用来定义延迟多久后执行一个函数。
  • class threading.Timer(interval, function, args=None, kwargs=None)
    1. interval #多少时间后执行function函数
    2. function #需要执行的函数
  • start方法执行之后,Timer对象会处于等待状态,等待了interval秒之后,开始执行function函数的。
  • Timer是线程Thread的子类,Timer实例内部提供了一个finished属性,该属性是Event对象。
  • cancel方法,本质上 是在worker函数执行前对finished属性set方法操作,从而跳过了worker函数执行,达到了取消的效果。
from threading import Timer import logging import time  logging.basicConfig(format="%(asctime)s %(threadName)s %(thread)s %(message)s",level=logging.INFO)  def worker():     logging.info("in worker")     time.sleep(5)     logging.info("end in worker")  t = Timer(2,worker) t.setName("timer1") #设置线程名称 # t.cancel() #取消定时器后,定时器不在执行 t.start() # t.cancel() #取消定时器后,定时器不在执行 time.sleep(4) #等待4秒后,定时器已经开始执行 t.cancel() #当定时器执行后,无法取消  print("======end========") 

3.threading.Lock锁

锁(Lock):一旦线程获得锁,其他试图获取锁的线程将被阻塞等待。
锁:凡是存在共享支援争抢的地方都可以使用锁,从而保证只有一个使用者可以完全使用这个资源。

名称 含义
Lock.acquire(blocking=True,timeout=-1) 获取锁,获取成功返回True,否则返回False
当获取不到锁时,默认进入阻塞状态,直到获取到锁,后才继续。阻塞可以设置超时时间。非阻塞时,timeout禁止设置。如果超时依旧未获取到锁,返回False。
Lock.rease() 释放锁,可以从任何线程调用释放。
已上锁的锁,会被设置为unlocked。如果未上锁调用,会抛出RuntimeError异常。
import threading import sys import time  def print(*args):     sys.stdout.write(" ".join(map(str,args))+"\n")  def worker(lock):     print("worker start",threading.get_ident(),threading.current_thread().name)     lock.acquire()     print("worker over",threading.get_ident(),threading.current_thread().name)  lock = threading.Lock() lock.acquire() print(" -"*30) for i in range(5):     threading.Thread(target=worker,args=(lock,),name="w{}".format(i)).start()  print("- "* 30) while True:     time.sleep(0.1)     cmd = input(">>").strip()     if cmd == "r":         lock.release()     elif cmd == "q":         break     else:         print(threading.enumerate()) 


上例可以看出不管在哪一个线程中,只要对一个已经上锁的锁阻塞请求,该线程就会阻塞。
  • 加锁,解锁
    一般来说,加锁就需要解锁,但是加锁后解锁前,还要有一些代码执行,就有可能抛异常,一旦出现异常,锁是无 法释放,但是当前线程可能因为这个异常被终止了,这也产生了死锁

  • 加锁、解锁常用语句:

    1. 使用try…finally语句保证锁的释放
    2. with上下文管理,锁对象支持上下文管理
  • 计数器类,可加,可减。

import threading import sys import time  def print(*args):     sys.stdout.write(" ".join(map(str,args))+"\n")  class Counter:     def __init__(self):         self._val = 0         self.lock = threading.Lock()      @property     def value(self):         with self.lock:             return self._val      def inc(self):         with self.lock:             self._val += 1      def dec(self):         with self.lock:             self._val -= 1  def run(c:Counter,count=100):     for _ in range(count):         for i in range(-50,50):             if i <0:                 c.dec()             else:                 c.inc()  c = Counter() c1 = 10 #线程数 c2 = 1000 for i in range(c1):     threading.Thread(target=run,args=(c,c2)).start()  for k in threading.enumerate():     if k.name != "MainThread":         k.join()  print(c.value) 
  • 锁的应用场景
    锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候。

  • 使用锁的注意事项:

    1. 少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就成了串行,要么排队执行,要么争抢执行
      • 举例,高速公路上车并行跑,可是到了省界只开放了一个收费口,过了这个口,车辆依然可以在多车道 上一起跑。过收费口的时候,如果排队一辆辆过,加不加锁一样效率相当,但是一旦出现争抢,就必须 加锁一辆辆过。注意,不管加不加锁,只要是一辆辆过,效率就下降了。
    2. 加锁时间越短越好,不需要就立即释放锁
    3. 一定要避免死锁
  • 非阻塞锁的使用

import threading import sys import time  def print(*args):     sys.stdout.write(" ".join(map(str,args))+"\n")  def worker(lock:threading.Lock):     while True:         if lock.acquire(False):             print("do something.")             time.sleep(1)             lock.release()             break         else:             print("try again")             time.sleep(1)  lock = threading.Lock() for i in range(5):     threading.Thread(target=worker,name="w{}".format(i),args=(lock,)).start() 

4.可重入锁RLock

  • 可重入锁,是线程相关的锁
  • 线程A获得可重复锁,并可以多次成功获取,不会阻塞。最后要在线程A中做和acquire次数相同的release
import threading import sys import time  def print(*args):     sys.stdout.write(" ".join(map(str,args))+"\n")  def fib(num,rlock:threading.RLock):     with rlock:         if num<3:             return 1         return fib(num-1,rlock)+fib(num-2,rlock)  def work(num,rlock):     print(fib(num,rlock))  rlock = threading.RLock() for i in range(1,10):     threading.Thread(target=work,args=(i,rlock)).start() 

可重入锁
* 与线程相关,可在一个线程中获取锁,并可继续在同一线程中不阻塞多次获取锁
* 当锁未释放完,其它线程获取锁就会阻塞,直到当前持有锁的线程释放完锁
* 锁都应该使用完后释放。可重入锁也是锁,应该acquire多少次,就release多少次

5.Condition条件锁,等待通知

构造方法Condition(lock=None),可以传入一个Lock或RLock对象,默认是RLock。

名称 含义
Condition.acquire(self,*args) 获取锁
Condition.wait(self,timeout=None) 等待通知,timeout设置超时时间
Condition.notify(self,n=1) 唤醒至多指定数目个数的等待的线程,没有等待的线程就没有任何操作
Condition.notify_all(self) 唤醒所有等待的线程
  • 每个线程都可以通过Condition获取已把属于自己的锁,在锁中可以等待其他进程的同级锁的通知。当获取到同级锁的通知后,会停止等待。

  • 当使用Condition(lock=Lock())初始化锁时,锁只能一级等待,不能出现多级等待。

  • 简单示例:

import threading import time  def work(cond:threading.Condition):     with cond:         print("开始等待")         cond.wait()         print("等到了")  cond = threading.Condition() # cond = threading.Condition(threading.Lock()) threading.Thread(target=work,args=(cond,)).start() threading.Thread(target=work,args=(cond,)).start()  with cond:     with cond:         time.sleep(1)         print("开始释放二级等待")         print(cond.notifyAll())     time.sleep(2)     print("开始释放一级等待")     cond.notifyAll() 

  • 广播模式示例:
from threading import Thread,Condition,Lock import time import logging import random  logging.basicConfig(format="%(asctime)s %(threadName)s %(thread)s %(message)s",level=logging.INFO)  class Dispachter:     def __init__(self):         self.data = None         self.cond = Condition(lock=Lock())      #生成者     def produce(self,total):         for _ in range(total):             data = random.randint(1,100)             with self.cond:                 logging.info("生产了一个数据:{}".format(data))                 self.data = data                 self.cond.notify(1)             time.sleep(1) #模拟生成数据需要耗时1秒      #消费者     def consume(self):         while True:             with self.cond:                 self.cond.wait() #等待                 data = self.data                 logging.info("消费了一个数据 {}".format(data))                 self.data = None  d = Dispachter() p = Thread(target=d.produce,name="producer",args=(10,))  # 增加消费者 for i in range(5):     c = Thread(target=d.consume,name="consumer{}".format(i))     c.start()  p.start() 

上面例子中演示了生产者生产一个数据,就通知一个消费者消费。

  • Condition总结
    Condition用于生产者消费者模型中,解决生产者消费者速度匹配的问题。采用了通知机制,非常有效率。
  • 使用方式
    使用Condition,必须先acquire,用完了要release,因为内部使用了锁,默认使用RLock锁,最好的方式是使用 with上下文。
    消费者wait,等待通知。
    生产者生产好消息,对消费者发通知,可以使用notify或者notify_all方法。

6.therading.Semaphore信号量

和Lock很像,信号量对象内部维护一个倒计数器,每一次acquire都会减1,当acquire方法发现计数为0就阻塞请求 的线程,直到其它线程对信号量release后,计数大于0,恢复阻塞的线程。

名称 含义
Semaphore(value=1) 构造方法。value为初始信号量。value小于0,抛出ValueError异常
Semaphore.acquire(self,blocking=True,timeout=None) 获取信号量,技术器减1,即_value的值减少1。如果_value的值为0会变成阻塞状态。获取成功返回True
Semaphore.release(self) 释放信号量,计数器加1。即_value的值加1
Semaphore._value 信号量,当前信号量
  • 注意
    1. 计数器永远不会低于0,因为acquire的时候,发现是0,都会被阻塞。
    2. 信号量没有做超界限制
from threading import Semaphore  s =Semaphore(3) print(s._value) s.release() #会增加信号量 print(s._value) #可以看出没有做信号量上线控制 print("----------------") print(s.acquire()) print(s._value) print(s.acquire()) print(s._value) print(s.acquire()) print(s._value) print(s.acquire()) print(s._value) print(s.acquire()) #当信号量为0再次acquire会被阻塞 print("~~~~~~阻塞了吗?") print(s._value) 

  • 跨线程使用演示
from threading import Thread,Semaphore import time import logging  logging.basicConfig(format="%(asctime)s %(threadName)s %(thread)s %(message)s",level=logging.INFO)  #定义获取信号量 def worker(s:Semaphore):     while s.acquire():         logging.info("被执行了一次,获取一个信号量 _value={}".format(s._value))  #释放信号量 def cunn(s:Semaphore):     while True:         logging.info("释放一个信号量")         s.release()         time.sleep(1)  s = Semaphore(3) #创建3个线程获取信号量 for i in range(3):     Thread(target=worker,args=(s,),name="w{}".format(i)).start()  #开启一个线程释放信号量 Thread(target=cunn,args=(s,)).start() 

7.threading.BoundedSemaphore有界信号量

  • 有界信号量,不允许使用release超出初始值的范围,否则,抛出ValueError异常
名称 含义
BoundedSemaphore(value=1) 构造方法。value为初始信号量。value小于0,抛出ValueError异常
BoundedSemaphore.acquire(self,blocking=True,timeout=None) 获取信号量,技术器减1,即_value的值减少1。如果_value的值为0会变成阻塞状态。获取成功返回True
BoundedSemaphore.release(self) 释放信号量,计数器加1。即_value的值加1,超过初始化值会抛出异常ValueError。
BoundedSemaphore._value 信号量,当前信号量
from threading import BoundedSemaphore  bs = BoundedSemaphore(3) print(bs._value) bs.acquire() bs.acquire() bs.acquire() print(bs._value) bs.release() bs.release() bs.release() print(bs._value) bs.release() 

  • 应用举例
    连接池
    因为资源有限,且开启一个连接成本高,所以,使用连接池。
    一个简单的连接池
    连接池应该有容量(总数),有一个工厂方法可以获取连接,能够把不用的连接返回,供其他调用者使用。
from threading import Thread,BoundedSemaphore import time import logging import random  logging.basicConfig(format="%(asctime)s %(threadName)s %(thread)s %(message)s",level=logging.INFO)  #链接类 class Conn:     def __init__(self,name):         self.name = name  class Pool:     def __init__(self,count:int):         self.count = count         #池中放着链接备用         self.pool = [self._connect("conn-{}".format(i)) for i in range(count)]         self.bsemaphore = BoundedSemaphore(count)      #创建连接方法,返回一个连接对象     def _connect(self,conn_name):         return Conn(conn_name)      #获取一个链接     def get_conn(self):         self.bsemaphore.acquire()         self.pool.pop()         logging.info("从连接池拿走了一个连接~~~~~~~")      #归还一个连接     def return_conn(self,conn:Conn):         logging.info("归还了一个连接----------")         self.pool.append(conn)         self.bsemaphore.release()  def worker(pool:Pool):     conn = pool.get_conn()     logging.info(conn)     #模拟使用了一段时间     time.sleep(random.randint(1,5))     pool.return_conn(conn)  pool = Pool(3) for i in range(6):     Thread(target=worker,name="worker-{}".format(i),args=(pool,)).start() 


上例中,使用信号量解决资源有限的问题。
如果池中有资源,请求者获取资源时信号量减1,拿走资源。当请求超过资源数,请求者只能等待。当使用者用完 归还资源后信号量加1,等待线程就可以被唤醒拿走资源。
注意:这个连接池的例子不能用到生成环境,只是为了说明信号量使用的例子,连接池还有很多未完成功能。
  • 问题分析
  1. 边界问题
    • 假设一种极端情况,计数器还差1就归还满了,有三个线程A、B、C都执行了第一句,都没有来得及release,这时 候轮到线程A release,正常的release,然后轮到线程C先release,一定出问题,超界了,直接抛异常。 因此信号量,可以保证,一定不能多归还。
  2. 正常使用分析
    • 正常使用信号量,都会先获取信号量,然后用完归还。
    • 创建很多线程,都去获取信号量,没有获得信号量的线程都阻塞。能归还的线程都是前面获取到信号量的线程,其 他没有获得线程都阻塞着。非阻塞的线程append后才release,这时候等待的线程被唤醒,才能pop,也就是没有 获取信号量就不能pop,这是安全的。
    • 经过上面的分析,信号量比计算列表长度好,线程安全。
  • 信号量和锁
    1. 信号量,可以多个线程访问共享资源,但这个共享资源数量有限。
    2. 锁,可以看做特殊的信号量,即信号量计数器初值为1。只允许同一个时间一个线程独占资源。

总结

threading模块中的类

常用方法 含义
Event set(self) 将标记设置为True
clear(self) 将标记设置为False
is_set() 判断当前标记是否为True,是True返回True,否则返回False
相当于为返回当前标记
wait(self,timeout=None) 如果当前标记为True,立即返回True,如果当前标记为False,会产生一个阻塞,直到标记为True时返回True。timeout等待超时时间,默认为None表示无限等待。未等到超时了返回False
Time定时器,延迟执行 Timer(interval, function, args=None, kwargs=None) 实例化构造方法
1. interval #多少时间后执行function函数
2. function #需要执行的函数
cancel(self) 取消定时器
(定时器为执行函数时可以取消,在函数执行中无法取消)
start() 启动定时器
Lock锁 acquire(self,blocking=True,timeout=-1) 获取锁,获取成功返回True,否则返回False
当获取不到锁时,默认进入阻塞状态,直到获取到锁,后才继续。
阻塞可以设置超时时间。
非阻塞时,timeout禁止设置。如果超时依旧未获取到锁,返回False。
rease(self) 释放锁,可以从任何线程调用释放。
已上锁的锁,会被设置为unlocked。如果未上锁调用,会抛出RuntimeError异常。
RLock可重入锁 和Lock类似但是:
线程A获得可重复锁,并可以多次成功获取,不会阻塞。最后要在线程A中做和acquire次数相同的release
Condition Condition(self,lock=None) 构造方法,lock默认值为None表示使用RLock()锁,也可以自己传入为Lock()
acquire(self,*args) 获取锁
rease(self) 释放锁
wait(self,timeout=None) 等待通知,timeout设置超时时间。
注意:必须获取锁后才能等待通知,notify或notify_all可以发通知
notify(self,n=1) 唤醒至多指定数目个数的等待的线程,没有等待的线程就没有任何操作
notify_all(self) 唤醒所有等待的线程
Semaphore信号量 Semaphore(value=1) 构造方法。value为初始信号量。value小于0,抛出ValueError异常
acquire(self,blocking=True,timeout=None) 获取信号量,技术器减1,即_value的值减少1。
如果_value的值为0会变成阻塞状态。获取成功返回True
release(self) 释放信号量,计数器加1。即_value的值加1
`_value`属性 信号量,当前信号量
BoundedSemaphore有界信号量 BoundedSemaphore(value=1) 构造方法。value为初始信号量。value小于0,抛出ValueError异常
acquire(self,blocking=True,timeout=None) 获取信号量,技术器减1,即_value的值减少1。
如果_value的值为0会变成阻塞状态。获取成功返回True
release(self) 释放信号量,计数器加1。即_value的值加1,超过初始化值会抛出异常ValueError。
_value 信号量,当前信号量
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!