线程同步
概念
线程同步,线程间协同,通过某种技术,让一个线程访问某些数据时,其他线程不能访问这些数据,直到该线程完成对数据的操作
Event
Event事件,是线程间通信机制中最简单的实现,使用一个内部的标记falg,通过flag的True或False的变化来进行操作
名称 | 含义 |
---|---|
set() | 标记设置为True |
clear() | 标记设置为False |
is_set() | 标记是否为True |
wait(timeout=None) | 设置等待标记为True的时长,None为无限等待,等到返回True,未等到超时了返回False |
练习
老板雇佣了一个工人,让他生产杯子,老板一直等着这个工人,直到生产了10个杯子
from threading import Event,Thread
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
def boss(event:Event):
logging.info('boss')
event.wait()
logging.info('good')
def worker(event:Event,count=10):
logging.info('working')
cups = []
while True:
logging.info('make 1 cup')
time.sleep(0.1)
cups.append(1)
if len(cups) >= count:
event.set()
break
logging.info(cups)
event = Event()
b = Thread(target=boss, name='boss', args=(event,))
w = Thread(target=worker, name='worker', args=(event,))
b.start()
w.start()
总结
使用同一个Event对象标记flag
谁wait就是等到flag变为True,或等到超时返回False.不限制等待的个数
wait的使用
from threading import Event,Thread
import logging
FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
def worker(event:Event,interval):
while not event.wait(interval):
logging.info('do sth')
event = Event()
Thread(target=worker, name='worker', args=(event,3))
event.wait(5)
event.set()
print('===end===')
定时器Timer/延迟执行
threading.Timer继承自Thread,这个类用来定义延迟多久后执行一个函数
class threading.Timer(interval,function,args=None,kwargs=None)
start方法执行之后,Timer对象会处于等待状态,等待了interval秒之后,开始执行function函数的
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
def worker():
logging.info('in worker')
time.sleep(2)
t = threading.Timer(3,worker)
t.setName('timer')
#t.cancel()
t.start()
#t.cancel()
while True:
print(threading.enumerate())
time.sleep(1)
上例代码工作线程早就启动了,只不过是在工作线程中掩饰4秒才执行worker函数
Timer是线程Tread的子类,Timer实例内部提供了一个finshed属性,该属性是Event对象.cancal方法,本质上是在worker函数执行前对finshed属性set方法操作,从而跳过了worker函数执行,达到了取消的效果
总结
Timer是线程Thread的子类,就是线程类,具有线程的能力和特征。
它的实例是能够延时执行目标函数的线程,在真正执行目标函数之前,都可以cancel它。
cancel方法本质使用Event类实现。这并不是说,线程提供了取消的方法。
Lock
Lock
锁,一旦线程获得锁,其它试图获取锁的线程将被阻塞
锁:凡是存在共享资源争抢的地方都可以使用锁,从而保证只有一个使用者可以完全使用这个资源
名称 | 含义 |
---|---|
acquire(blocking=True,timeout=1 | 默认阻塞,阻塞可以设置超时时间.非阻塞时,timeout禁止设置.成功获取锁,返回True,否则返回False |
release() | 释放锁.可以从任何线程调用释放.以上锁的锁,会被重置为unlocked未上锁的锁上调用,抛RuntimeError异常 |
锁的基本使用
import threading
lock = threading.Lock()
lock.acquire()
print('-'*30)
# threading.Timer(5, lambda l: l.release(), (lock,)).start()
# lock.release()
def worker():
print('worker start', threading.current_thread().name)
lock.acquire()
print('worker over', threading.current_thread().name)
for i in range(10):
threading.Thread(target=worker,name='{}'.format(i)).start()
print('-'*30)
while True:
cmd = input('>>').strip()
if cmd =='r':
lock.release()
elif cmd == 'quit':
break
else:
print(threading.enumerate())
上例可以看出不管在哪一个线程中,只要对一个已经上锁的锁阻塞请求,该线程就会阻塞
练习
订单要求生产1000个杯子,组织10个工人生产,请忽略老板,关注工人生成杯子
from threading import Event,Thread
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
cups = []
def worker(count=10):
logging.info('working')
while len(cups) < count:
time.sleep(0.001)
cups.append(1)
logging.info('I finished my job. cups = {}'.format(len(cups)))
for i in range(1,11):
t = Thread(target=worker,name='worker-{}'.format(i),args=(100,))
t.start()
从上例的运行结果看出,多线程调度,导致了判断失效
上例的锁实现
from threading import Thread,Lock
import threading
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
cups = []
lock = Lock()
def worker(count=10):
logging.info('working')
flag = False
while True:
lock.acquire() #获取锁
if len(cups) >= count:
flag = True
#lock.release() # 1
time.sleep(0.001) #为了看出线程切换效果
if not flag:
cups.append(1)
lock.release() # 2 这里释放锁
if flag:
break
#lock.release() # 3
logging.info('I finshed. cups = {}'.format(len(cups)))
for _ in range(10):
Thread(target=worker,args=(1000,)).start()
思考
上面代码中,共有3处可以释放锁.请问,放在何处合适?
假设位置1的lock.release()合适,分析如下:
有一个时刻,在某一个线程中len(cups)正好是999,flag=True,释放锁,正好线程被打断。另一个线程判断发现 也是999,flag=True,可能线程被打断。可能另外一个线程也判断是999,flag也设置为True。这三个线程只要继 续执行到cups.append(1),一定会导致cups的长度超过1000的.
假设位置2的lock.release()合适,分析如下:
在某一个时刻len(cups),正好是999,flag=True,其它线程试图访问这段代码的线程都阻塞获取不到锁,直到当 前线程安全的增加了一个数据,然后释放锁。其它线程有一个抢到锁,但发现已经1000了,只好break打印退出。 再其它线程都一样,发现已经1000了,都退出了。 所以位置2 释放锁 是正确的。 但是我们发现锁保证了数据完整性,但是性能下降很多。
上例中位置3,if flag:break是为了保证位置2的release方法被执行,否则,就出现了死锁,得到锁的永远没有释放锁.
计算器类,可以加 可以减
from threading import Thread,Lock
import threading
import time
class Counter:
def __init__(self):
self._val = 0
@property
def value(self):
return self._val
def inc(self):
self._val += 1
def dec(self):
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 = 10
for i in range(c1):
Thread(target=run,args=(c,c2)).start()
print(c.value)
c1取10 100 1000看看
c2取10 100 1000看看
self._val += 1或self._val-=1在线程执行的时候,有可能被打断
加锁,解锁
一般来说,加锁就需要解锁,但是加锁后解锁前,还要有一些代码执行,就有可能抛异常,一旦出现异常,锁无法释放,但是当前线程可能因为这个异常被终止了,这也产生了死锁
加锁 解锁常用语句:
- 使用try…finally语句保证锁的释放
- with上下文管理,锁对象支持上下文管理
改造Couter类,如下
from threading import Thread,Lock
import threading
import time
class Counter:
def __init__(self):
self._val = 0
self.__lock = Lock()
@property
def value(self):
with self.__lock:
return self._val
def inc(self):
try:
self.__lock.acquire()
self._val += 1
finally:
self.__lock.release()
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):
Thread(target=run,args=(c,c2)).start()
while True:
time.sleep(1)
if threading.active_count() == 1:
print(threading.enumerate())
print(c.value)
break
else:
print(threading.enumerate())
print(c.value)这一句在主线程中,很早就执行了,退出条件是,只剩下主线程的时候
这样的改造后,代码可以保证最后得到的value值一定是0
锁的应用场景
锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候
如果全部都是读取同一个共享资源需要锁吗?
不需要。因为这时可以认为共享资源是不可变的,每一次读取它都是一样的值,所以不用加锁
使用锁的注意事项:
-
少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就成了串行,要么排队执行,要么争抢执行
举例,高速公路上车并行跑,可是到了省界只开放了一个收费口,过了这个口,车辆依然可以在多车道 上一起跑。过收费口的时候,如果排队一辆辆过,加不加锁一样效率相当,但是一旦出现争抢,就必须 加锁一辆辆过。注意,不管加不加锁,只要是一辆辆过,效率就下降了。 -
加锁时间越短越好,不需要就立即释放锁
-
一定要避免死锁
不使用锁,有了效率,但是结果是错的
使用了锁,效率但是结果是对的
我们还是要正确的结果
非阻塞锁使用
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
lock = threading.Lock()
def worker(l:threading.Lock):
while True:
flag = l.acquire(False)
if flag:
logging.info('do something') #为了显示效果,没有释放锁
else:
logging.info('try again')
time.sleep(1)
for i in range(5):
threading.Thread(target=worker, name='worker={}'.format(i),args=(lock,)).start()
可重入锁RLock
可重入锁,是线程相关的锁
线程A获得可重复锁,并可以多次获取,不会阻塞.最后要在线程A中做和acquire次数相同的release
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
lock = threading.RLock()
print(lock.acquire())
print(lock.acquire())
print(lock.acquire())
print(lock.acquire())
lock.release()
lock.release()
lock.release()
lock.release()
# lock.release() #release多了抛异常
print('main thread {}'.format(threading.main_thread().ident))
print('lock in main thread {}'.format(lock))
print(lock.acquire(blocking=False))
lock.release()
print('-'*30)
print(lock.acquire())
def sub(l):
print('{}: {}'.format(threading.current_thread(), l.acquire())) # 阻塞
print('{}: {}'.format(threading.current_thread(), l.acquire()))
print('lock in sub thread {}'.format(lock))
l.release()
print('release in sub 1')
l.release()
print('release in sub 2')
threading.Timer(2,sub,(lock,)).start()
print('in main thread, {}'.format(lock.acquire())) # count = 2
lock.release()
time.sleep(5)
print('release lock in main thread~~~~~~~~', end='\n\n')
lock.release() # count = 0
可重入锁
- 与线程相关,可在一个线程中获取锁,并可继续在同一线程中不阻塞多次获取锁
- 当锁未释放完,其它线程获取锁就会阻塞,直到当前持有锁的线程释放完锁
- 锁都应该使用完后释放。可重入锁也是锁,应该acquire多少次,就release多少次
Condition
构造方法Condtion(lock=None),可以传入一个Lock或RLock对象,默认是RLock
acquire(*args) | 获取锁 |
---|---|
wait(self,timeout=None) | 等待或超时 |
notify(n=1) | 唤醒至多指定数目的等待的线程,没有等待的线程就没有任何操作 |
notify_all() | 唤醒所有等待的线程 |
Condition基本使用
import logging
import threading
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
def boss(c:threading.Condition):
logging.info("I'm waiting for U")
with c:
c.wait()
logging.info("Good Job")
def worker(c:threading.Condition,count=1000):
print("I'm working {}".format(threading.current_thread().name))
with c:
cups = []
while len(cups) < count:
time.sleep(0.001)
cups.append(1)
print("finishd.{} cups = {}".format(threading.current_thread().name,len(cups)))
c.notify_all()
c.notify(1)
cond = threading.Condition()
b1 = threading.Thread(target=boss, args=(cond,), name='boss1')
b2 = threading.Thread(target=boss, args=(cond,), name='boss2')
b1.start()
b2.start()
w = threading.Thread(target=worker, args=(cond,), name='worker')
w.start()
Condition用于生产者 消费者模型,为了解决生产者消费者速度匹配问题
先看一个例子,消费者消费速度大于生产者生产速度
Condtion总结
Condition用于生产者消费者模型中,解决生产者消费者速度匹配的问题。
采用了通知机制,非常有效率。
使用方式
使用Condition,必须先acquire,用完了要release,因为内部使用了锁,默认使用RLock锁,最好的方式是使用with上下文
消费者wait,等待通知
生产者生产好消息,对消费者发通知,可以使用notify活着notify_all方法.
semaphore信号量
和Lock很像,信号量对象内部维护一个倒计数器,每一次acquire都会减1,当acquire方法发现计数为0就阻塞请求 的线程,直到其它线程对信号量release后,计数大于0,恢复阻塞的线程。
名称 | 含义 |
---|---|
Semaphore(value=1) | 构造方法.value小于0,抛ValueError异常 |
acquire(blocking=True,timeout=None) | 获取信号量,计数器减1,获取成功返回True |
release() | 释放信号量,计数器加1 |
计数器永远不会低于0,因为acquire的时候,发现是0,都会被阻塞
import logging
from threading import Thread,Semaphore
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
def worker(s:Semaphore):
logging.info("in worker thread")
logging.info(s.acquire())
logging.info('worker thread over')
#信号量
s = Semaphore(3)
logging.info(s.acquire())
print(s._value)
logging.info(s.acquire())
print(s._value)
logging.info(s.acquire())
print(s._value)
Thread(target=worker,args=(s,)).start()
time.sleep(2)
logging.info(s.acquire(False))
logging.info(s.acquire(timeout=3))
logging.info("abc")
s.release()
release方法超界
import logging
import threading
sema = threading.Semaphore(3)
logging.warning(sema.__dict__)
for i in range(3):
sema.acquire()
logging.warning('~~~~~~~~')
logging.warning(sema.__dict__)
for i in range(4):
sema.release()
logging.warning(sema.__dict__)
for i in range(3):
sema.acquire()
logging.warning('~~~~~~~~')
logging.warning(sema.__dict__)
sema.acquire()
logging.warning('~~~~~~~~')
logging.warning(sema.__dict__)
从上例输出结果可以看出,竟然内置计数器达到了4,这样实际上超出我们的最大值,需要解决这个问题.
BoundedSemaphore类
有界的信号量,不允许使用release超出初始值的范围,否则,抛出ValueError异常。
将上例的信号量改成有界的信号量试一试
应用举例
连接池
因为资源有限,且开启一个连接成本高,所以,使用连接池
一个简单的连接池
连接池应该有容量(总数),有一个工厂方法可以获取连接,能够把不用的连接返回,供其他调用者使用.
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)]
def _connect(self,conn_name):
return Conn(conn_name)
def get_conn(self):
if len(self.pool)>0:
return self.pool.pop()
def return_conn(self,conn:Conn):
self.pool.append(conn)
真正的连接池的实现比上面的例子要复杂的多,这里只是简单的一个功能的实现.
本例中,get_conn()方法在多线程的时候有线程安全问题。
假设池中正好有一个连接,有可能多个线程判断池的长度是大于0的,当一个线程拿走了连接对象,其他线程再来 pop就会抛异常的。如何解决?
加锁,在读写的地方加锁
使用信号量Semaphore
import random
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
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.semaphore = threading.Semaphore(count)
def _connect(self,conn_name):
#创建连接的方法,返回一个连接对象
return Conn(conn_name)
def get_conn(self):
#从池中拿走一个连接
logging.info('get---------')
self.semaphore.acquire()
logging.info('------------')
return self.pool.pop()
def return_conn(self,conn:Conn):
#向池中返回一个连接对象
logging.info('return------------')
self.pool.append(conn)
self.semaphore.release()
#初始化连接池
pool = Pool(3)
def worker(pool:Pool):
conn = pool.get_conn()
logging.info(conn)
#模拟使用了一段时间
time.sleep(random.randint(1,5))
pool.return_conn(conn)
for i in range(6):
threading.Thread(target=worker, name='worker-{}'.format(i), args=(pool,)).start()
上例中,使用信号量解决资源有限的问题。
如果池中有资源,请求者获取资源时信号量减1,拿走资源。当请求超过资源数,请求者只能等待。当使用者用完 归还资源后信号量加1,等待线程就可以被唤醒拿走资源。
注意:这个连接池的例子不能用到生成环境,只是为了说明信号量使用的例子,连接池还有很多未完成功能
问题
self.conns.append(conn) 这一句有哪些问题考虑?
一. 边界问题分析
return_conn方法可以单独执行,有可能多归还连接,也就是会多release,所以,要用有界信号量 BoundedSemaphore类。
这样用有界信号量修改源代码,保证如果多return_conn就会抛异常
self.pool.append(conn)
self.semaphore.release()
假设一种极端情况,计数器还差1就归还满了,有三个线程A、B、C都执行了第一句,都没有来得及release,这时 候轮到线程A release,正常的release,然后轮到线程C先release,一定出问题,超界了,直接抛异常。 因此信号量,可以保证,一定不能多归还。
如果归还了同一个连接多次怎么办,重复很容易判断。
这个程序还不能判断这些连接是不是原来自己创建的,这不是生成环境用的代码,只是简单演示。
二.正常使用分析
正常使用信号量,都会先获取信号量,然后用完归还。
创建很多线程,都去获取信号量,没有获得信号量的线程都阻塞。能归还的线程都是前面获取到信号量的线程,其 他没有获得线程都阻塞着。非阻塞的线程append后才release,这时候等待的线程被唤醒,才能pop,也就是没有 获取信号量就不能pop,这是安全的。
经过上面的分析,信号量比计算列表长度好,线程安全.
信号量和锁
信号量,可以多个线程访问共享资源,但这个共享资源数量有限。
锁,可以看做特殊的信号量,即信号量计数器初值为1。只允许同一个时间一个线程独占资源。