概念
线程同步,线程间协同,通过某种技术,让一个线程访问某些数据时,其他线程不能访问这些数据,直到该线程完成对数据的操作
Event
Event事件,是线程间通信机制中最简单的实现,使用一个内部的标记flag,通过flag的True或者False的变化来进行操作
名称 | 含义 |
---|---|
set() | 标记设置为True |
clear() | 标记设置为False |
is_set() | 标记是否为True |
wait(timeout=None) | 设置等待标记为True的时长,None为无限等待,等到l返回True,未等到超时返回False |
用法:
event = Event() 创建Event全局对象(多个线程使用)
event.wait() 阻塞等待(多个线程可用)
event.set() 设置,一旦set,flag就变为Ture,阻塞等待就不等了
event.wait(1) 等1秒后,执行后面的代码吗(暂停1秒)
from threading import Thread,Event
import time
import logging
event = Event() #创建Event全局对象
FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT,level=logging.INFO)
def boss(event:Event):
logging.info("I'am boss,waiting for you " )
event.wait() # 等待工人把事情做完(阻塞等待)
logging.info('OK,good job')
def worker(event:Event,count=10):
logging.info("I'am working")
cups = []
while True:
time.sleep(0.5)
cups.append(1)
if len(cups) >= count:
event.set()
break
logging.info("I'am finished my job,cups={}".format(count))
b = Thread(target=boss,name='boss',args=(event,))
w = Thread(target=worker,name='worker',args=(event,))
b.start()
w.start()
------------------------------------------
2019-06-07 11:07:25,660 boss 11180 I'am boss,waiting for you
2019-06-07 11:07:25,660 worker 18188 I'am working
2019-06-07 11:07:30,666 worker 18188 I'am finished my job,cups=10
2019-06-07 11:07:30,667 boss 11180 OK,good job
- Event使用的总结:
- 使用同一个Event对象的标记flag。谁wait就是等到flag变为True,或等到超时返回False。不限制等待的个数
定时器 Timer/延迟执行
- threadingTimer继承自Thread,这个类用来定义延迟多久后执行一个函数
- class threading.Time(interval,function,args=None,kwargs=None)
- start方法执行之后,Timer对象会处于等待状态,等待了interval秒之后,开始执行函数function
import threading
import logging
import time
FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(level=logging.INFO, format=FORMAT)
def worker():
print(threading.enumerate())
logging.info('working')
time.sleep(0.5)
logging.info('finished the job')
t = threading.Timer(2,worker)
#t.cancel() 可以取消执行t,但是必须是在start之前取消执行
t.start()
time.sleep(1)
print('a')
time.sleep(1)
print('b')
time.sleep(1)
print('c')
-----------------------------------------
a
b
2019-06-07 11:37:41,789 Thread-1 12128 working
[<_MainThread(MainThread, started 7764)>, <Timer(Thread-1, started 12128)>]
2019-06-07 11:37:42,289 Thread-1 12128 finished the job
c
上列说明,工作线程被暂停时,主线程仍然继承工作
- Timer是线程Thread的子类,就是线程类,具有线程的能力和特征。
- 它的实例是能够延时执行目标函数的线程,在真正执行目标函数之前,都可以cancel它。cancel方法本质使用Event类实现。这并不是说,线程提供了取消的方
Lock
- 锁,一旦线程获得锁,其他试图获取锁的线程将被阻塞
- 锁,凡是存在共享资源争抢的地方,都可以使用锁,从而保证只有一个使用者可以完全使用这个资源
名称 | 含义 |
---|---|
acquire(blocking=True,timeout=-1) | 默认阻塞,阻塞可以设置超时时间,非阻塞时,timeout禁止设置,成功获取锁,返回True,否则返回False |
release() | 释放锁,可以从任何线程调用释放,已经上锁的锁会被重置unlocked,未上锁的锁上调用,抛RuntimeError异常 |
用法
lock = threading.Lock() #建立锁对象
lock.acquire() # 加锁
lock.release() # 解锁
lock.acquire(blocking=True),阻塞
lock.acquire(blocking=False),拿到锁,但是不阻塞,继续运行后面代码
lock.acquire(timeout=1),拿到锁,阻塞1秒后,继续运行后面代码
import threading
import time
count = []
def worker(n):
a = 0
for i in range(n):
a +=1
while len(count) < n:
time.sleep(0.01)
count.append(1)
print('over',len(count),threading.current_thread().name,a)
for i in range(5):
threading.Thread(target=worker,name='worker{}'.format(1+i),args=(100,)).start()
-------------------------------------------------------------------
over 100 worker3 100
over 101 worker2 100
over 102 worker1 100
over 103 worker5 100
over 104 worker4 100
上例的运行结果看出,多线程调度,导致了判断失效,count在递增到100的时候,最后多递增了.全局变量count在多线程运行时,各个线程之间有干扰,反而局部变量a却不存在干扰.可以用锁解决问题,如下
import threading
import time
lock = threading.Lock() #建立锁对象
count = []
def worker(n):
a = 0
for i in range(n):
a +=1
lock.acquire() #加锁
while len(count) < n:
time.sleep(0.01)
count.append(1)
print('over',len(count),threading.current_thread().name,a)
lock.release() #离开后必须解锁,释放CPU资源
for i in range(5):
threading.Thread(target=worker,name='worker{}'.format(1+i),args=(100,)).start()
-------------------------------------------------------------------
over 100 worker1 100
over 100 worker2 100
over 100 worker3 100
over 100 worker4 100
over 100 worker5 100
锁使得并行转换为串行,一旦线程获得锁,此线程将独占资源,其他试图获取锁的线程将全部被阻塞
加锁,解锁
一般来说,加锁就需要解锁,但是加锁后解锁前,中间还有一段代码需要执行,就有可能抛异常,一旦出现异常,锁是无法释放,但是当前线程可能因为这个异常被终止,这就产生了死锁.
加锁,解锁的常用语句:
1,使用try…finaly语句保证锁的释放
2,with上下文管理,锁对象支持上下文管理 (使用with的方法是:with后面是锁对象即可,进入with时,自动运行__enter__,进行acquire加锁,中间执行语句块,结束运行__exit__时,自动进行release解锁)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yQiSPPKS-1570784781338)(https://i.loli.net/2019/06/07/5cfa4c3169db212677.png)]
import threading
from threading import Thread
import time
lock = threading.Lock()
class Counter:
def __init__(self):
self._val = 0
self.lock = lock
@property
def value(self):
with self.lock: #with语法,会调用with后的对象的__enter__方法(即会运行acquire()方法,)
return self._val #退出with上下文管理时,会运行__exit__(即会运行release()方法)
def inc(self):
try:
self.lock.acquire()
self._val += 1
finally:
self.lock.release()
def dec(self):
try:
self.lock.acquire()
self._val -= 1
finally:
self.lock.release()
def run(c:Counter,count =100000):
for _ in range(count):
for i in range(-50,50):
if i < 0:
c.dec()
else:
c.inc()
c = Counter()
c1 = 10
c2 = 1000
thread_list=[]
for i in range(c1):
t = Thread(target=run,args=(c,c2))
t.start()
thread_list.append(t)
print(c .value) #此处打印不正确
for i in thread_list:
i.join() #等到耗时最长的工作线程结束时,主线程才继续执行后面的代码,后面的结果才能确保正确
print(c .value)
#最后打印也可以按下面改 确保工作线程全部结束,只剩主线程时,就可以拿值了
while True:
if threading.active_count() == 1:
print(c.value)
break
-----------------------------------------------------------------
-175 #不正确的结果
0
锁的应用场景
- 锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候,(如果访问的是类似常量的不需要锁)
- 使用锁的注意事项:
- 少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就成了串行,要么排队执行,要么争抢执行。
- 加锁时间越短越好,不需要就立即释放
- 锁一定要避免死锁
可重入锁
可重入锁,是线程相关的锁
线程A获得可重复锁,并且可以多次成功获取,不会阻塞,最后在线程A中做和acquire次数相同的release
用法:
lock = threading.Rlock() #建立锁对象
lock.acquire(blocking=False)
lock.acquire()
代码块
lock.release()
lock.release()
- 可重入锁
- 与线程相关,可在一个线程中获取,并可继续在同一线程中不阻塞多次获取
- 当锁未释放完,其它线程获取锁就会阻塞,直到当前持有锁的线程释放完锁
- 锁都应该使用完后释放,可重入锁也是锁,应该acquire多少次,就release多少次
Condition
构造方法Condition(lock=None),可以传入一个Lock或RLock对象,默认是RLock
名称 | 含义 |
---|---|
acquire(*args) | 获取锁 |
wait(self,timeout=None) | 等待或超时,等待notify的通知(唤醒) |
notify(n=1) | 唤醒至多指定数目的等待线程,没有等待的线程就没有任何操作 |
notefy_all() | 唤醒所有等待的线程 |
用法:
cond = threading.Condition()
cond.acquire() #阻塞
cond.wait()
cond.notify(1),# 单播,只唤醒一个线程
cond.notefy(n) # 多播,唤醒n个线程
cond.notefy_all() # 广播,唤醒所有线程
- Condition总结
- Condition用于生产者消费者模型中,解决生产者消费者速度匹配的问题。采用了通知机制,非常有效率。
- 使用Condition,必须先acquire,用完了要release,因为内部使用了锁,默认使用RLock锁,最好的方式是使用with上下文。
- 消费者wait,等待通知。生产者生产好消息,对消费者发通知,可以使用notify或者notify_all方法
condition使用上下文
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IGG5uQLL-1570784781339)(https://i.loli.net/2019/06/07/5cfa635274cb224835.png)]
from threading import Event, Thread, Condition
import logging
import random
FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
# 此处不考虑线程安全问题
class Dispachter:
def __init__(self):
self.data = None
self.event = Event()
self.cond = Condition()
def produce(self, total):
for _ in range(total):
data = random.randint(1, 100)
with self.cond:
logging.info(data)
self.data = data
self.cond.notify_all() #一旦产生数据,立马通知所有阻塞的线程,唤醒等待的线程
self.event.wait(1)
def consume(self):
while not self.event.is_set():
with self.cond:
self.cond.wait() #等待cond.notify的唤醒,一旦唤醒,马上运行下面的代码
data = self.data
logging.info('recieved {}'.format(data))
d = Dispachter()
p = Thread(target=d.produce, name='producer', args=(10,))
for i in range(5):
c = Thread(target=d.consume, name='consumer')
c.start()
p.start()
semaphore 信号量
和 Lock很像,信号量对象内部维护一个倒计数器,每一次acquire,都会减一,当acquire方法发现计算器为0时,就阻塞请求的线程,直到其它线程对信号量release后,计算器大于0,此时才恢复阻塞的线程
名称 | 含义 |
---|---|
Semaphore(valure=1) | 构造方法,value小于0,抛ValueError异常 |
acquire(blocking=True,timeout=None) | 获取信号量,计算器减1,获取成功返回True |
rlease() | 释放信号量,计算器加1 |
计算器永远不会小于0,因为acquire的时候,发现是0,都会被阻塞.
from threading import Thread,Semaphore
def worker(s:Semaphore):
pass
#总信号量
s = Semaphore(2)
print(s._value)
print('A',s.acquire()) #此处A位置拿到一个信号量,计数减1
Thread(target=worker,args=(s,)).start() # 启动其他工作线程
print('B',s.acquire(False)) #此处B位置拿到一个信号量,计数减1,剩余0
print('C',s.acquire(timeout=2)) #此处C位置拿不到信号量,总共2个,被前面的拿完了
# 释放一个信号量
print('D','release one')
s.release() # 此处释放一个信号量,计数加1,当Semaphore对象执行release时,内部会执行一下Condition.notify_all
print('E',s.acquire(timeout=2)) # 此处又可以被拿到一个信号链
-----------------------------------------------------------------------
2
A True
B True
C False
D release one
E True
- release方法超界问题
- 假设如果还没有acquire信号量,就release,release就会有超界的问题存在,需要解决
BoundedSemaphore类
边界问题分析
每个线程的release方法可以单独执行,有可能多归还连接,也就是会多release,所以,要用有界信号量BoundedSemaphore类,防止超界
有界的信号量,不允许使用release超出初始值的范围,否则,抛出ValueError异常,BoundedSemaphore类源码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ckFIAZ1J-1570784781340)(https://i.loli.net/2019/06/08/5cfb355d6ead797795.png)]
链接池
一个简单的连接池
连接池应该有容量(总数),有一个工厂方法可以获取连接,能够把不用的连接返回,供其他调用者使用
import random
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)-8d %(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('con-{}'.format(i))for i in range(self.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(2) #初始化链接池
def worker(pool:Pool):
conn = pool.get_conn() # 链接池中,总共2个信号量,同时获取(执行)的只有2个线程
logging.info(conn)
#模拟使用了一段时间
time.sleep(random.randint(1,5))
pool.return_conn(conn) # 还回信号量,阻塞的线程才有机会获得
for i in range(5):
threading.Thread(target=worker,name='worker{}'.format(i),args=(pool,)).start()
-----------------------------------------------------------------------------------------
2019-06-08 14:41:39,978 worker0 8236 get---------------
2019-06-08 14:41:39,979 worker0 8236 ----------------------
2019-06-08 14:41:39,979 worker0 8236 <__main__.Conn object at 0x00000224C39824E0>
2019-06-08 14:41:39,979 worker1 7344 get---------------
2019-06-08 14:41:39,979 worker1 7344 ----------------------
2019-06-08 14:41:39,979 worker1 7344 <__main__.Conn object at 0x00000224C39823C8>
2019-06-08 14:41:39,979 worker2 8308 get--------------- #阻塞
2019-06-08 14:41:39,980 worker3 7948 get--------------- #阻塞
2019-06-08 14:41:39,980 worker4 7604 get--------------- #此处阻塞,必须等到其他线程return还回一个信号量才能继承
2019-06-08 14:41:41,980 worker0 8236 return ----------------- #一旦有线程还回信号量,阻塞的线程马上获得信号量
2019-06-08 14:41:41,980 worker2 8308 ----------------------
2019-06-08 14:41:41,980 worker2 8308 <__main__.Conn object at 0x00000224C39824E0>
2019-06-08 14:41:44,980 worker1 7344 return -----------------
2019-06-08 14:41:44,981 worker3 7948 ----------------------
2019-06-08 14:41:44,981 worker3 7948 <__main__.Conn object at 0x00000224C39823C8>
2019-06-08 14:41:45,980 worker2 8308 return -----------------
2019-06-08 14:41:45,980 worker4 7604 ----------------------
2019-06-08 14:41:45,980 worker4 7604 <__main__.Conn object at 0x00000224C39824E0>
2019-06-08 14:41:49,981 worker4 7604 return -----------------
2019-06-08 14:41:49,983 worker3 7948 return -----------------
- 上例中,使用信号量解决资源有限的问题。如果池中有资源,请求者获取资源时信号量减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,只允许同一个事件是一个线程独占资源.