线程同步
- 概念
- Event
- 练习
- 总结
- wait的使用
- 定时器 Timer/延迟执行
- 总结
- Lock
- 锁的基本使用
- 练习
- 加锁、解锁
- 加锁、解锁常用语句:
- 锁的应用场景
- 非阻塞锁使用
- 可重入锁RLock
- 可重入锁总结
- Condition
- Condition基本使用
- Condition总结
- semaphore 信号量
- release方法超界问题
- BoundedSemaphore类
- 应用举例
- 问题
- 1、边界问题分析
- 2、正常使用分析
- 信号量和锁
概念
- 线程同步,线程间协同,通过某种技术,让一个线程访问某些数据时,其他线程不能访问这些数据,直到该线程完成对数据的操作
Event
Event事件,是线程间通信机制中最简单的实现,使用一个内部的标记flag,通过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("I'm boss, waiting for U")
# 阻塞等待
event.wait()
logging.info('Good Job.')
def worker(event:Event, count=10):
logging.info("I'm worker for U")
cups = []
while True:
logging.info('make 1 cup')
time.sleep(0.5)
cups.append(1)
if len(cups) >= count:
event.set()
break
logging.info('I finished my job. cups={}'.format(cups))
event = Event()
b = Thread(target=boss, name='boss', args=(event,))
w = Thread(target=worker, name='worker', args=(event,))
b.start()
w.start()
# 执行结果
2019-06-09 10:25:33,955 boss 276616 I'm boss, waiting for U
2019-06-09 10:25:33,955 worker 276620 I'm worker for U
2019-06-09 10:25:33,957 worker 276620 make 1 cup
2019-06-09 10:25:34,458 worker 276620 make 1 cup
2019-06-09 10:25:34,958 worker 276620 make 1 cup
2019-06-09 10:25:35,461 worker 276620 make 1 cup
2019-06-09 10:25:35,962 worker 276620 make 1 cup
2019-06-09 10:25:36,463 worker 276620 make 1 cup
2019-06-09 10:25:36,964 worker 276620 make 1 cup
2019-06-09 10:25:37,464 worker 276620 make 1 cup
2019-06-09 10:25:37,965 worker 276620 make 1 cup
2019-06-09 10:25:38,466 worker 276620 make 1 cup
2019-06-09 10:25:38,966 worker 276620 I finished my job. cups=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
2019-06-09 10:25:38,966 boss 276616 Good Job.
总结
- 使用同一个Event对象的标记flag
- 谁wait就是等到flag变为True,或等到超时返回False。不限制等待的个数
wait的使用
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 worker(event:Event, interval:int):
while not event.wait(interval):
logging.info('do sth.')
e = Event()
Thread(target=worker, args=(e, 3)).start()
e.wait(10) # 等待
e.set()
print("====end====")
# 执行结果
2019-06-09 11:13:34,887 Thread-1 278980 do sth.
2019-06-09 11:13:37,888 Thread-1 278980 do sth.
2019-06-09 11:13:40,891 Thread-1 278980 do sth.
====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(4, worker)
t.setName('timer')
# t.cancel()
t.start()
# t.cancel()
while True:
print(threading.enumerate())
time.sleep(1)
# 执行结果
[<_MainThread(MainThread, started 277968)>, <Timer(timer, started 278872)>]
[<_MainThread(MainThread, started 277968)>, <Timer(timer, started 278872)>]
[<_MainThread(MainThread, started 277968)>, <Timer(timer, started 278872)>]
[<_MainThread(MainThread, started 277968)>, <Timer(timer, started 278872)>]
[<_MainThread(MainThread, started 277968)>, <Timer(timer, started 278872)>]
2019-06-09 11:18:06,947 timer 278872 in worker
[<_MainThread(MainThread, started 277968)>, <Timer(timer, started 278872)>]
[<_MainThread(MainThread, started 277968)>]
[<_MainThread(MainThread, started 277968)>]
······
- 上例代码工作线程早就启动了,只不过是在工作线程中延时了4秒才执行了worker函数
Timer是线程Thread的子类,Timer实例内部提供了一个finished属性,该属性是Event对象。cancel方法,本质上是在worker函数执行前对finished属性set方法操作,从而跳过了worker函数执行,达到了取消的效果
总结
- Timer是线程Thread的子类,就是线程类,具有线程的能力和特征
- 它的实例是能够延时执行目标函数的线程,在真正执行目标函数之前,都可以cancel它
- cancel方法本质使用Event类实现。这并不是说,线程提供了取消的方法
Lock
- 锁,一旦线程获得锁,其它试图获取锁的线程将被阻塞
- 锁:凡是存在共享资源争抢的地方都可以使用锁,从而保证只有一个使用者可以完全使用这个资源
名称 | 含义 |
acquire(blocking=True, timeout=-1) | 默认阻塞,阻塞可以设置超时时间。非阻塞时,timeout禁止设置 成功获取锁,返回True,否则返回False |
release() | 释放锁。可以从任何线程调用释放 已上锁的锁,会被重置为unlocked未上锁的锁上调用,抛RuntimeError异常 |
锁的基本使用
- 第三个print永久阻塞
import threading
lock = threading.Lock()
print(lock.acquire())
print(lock.acquire(timeout=2))
print(lock.acquire())
# 执行结果
True
False
Process finished with exit code -1
- 不阻塞,获取不到返回Fasle
import threading
lock = threading.Lock()
print(lock.acquire())
print(lock.acquire(timeout=2))
print(lock.acquire(False))
# print(lock.acquire(False,timeout=2)) ValueError
# 执行结果
True
False
False
非阻塞时不要设置timeout值,否则会抛ValueError错误
import threading
lock = threading.Lock()
print(lock.acquire())
print('-' * 30)
def worker(l:threading.Lock):
print('worker start',threading.current_thread().name)
l.acquire()
print('worker over',threading.current_thread().name)
for i in range(5):
threading.Thread(target=worker, args=(lock,), name="w{}".format(i)).start()
print('-' * 30)
while True:
cmd = input('>>').strip()
if cmd == 'r':
lock.release()
elif cmd == 'quit':
break
else:
print(threading.enumerate())
- 执行结果
True
------------------------------
worker start w0
worker start w1
worker start w2
worker start w3
worker start w4
------------------------------
>>r
>>worker over w0
r
>>worker over w1
[<_MainThread(MainThread, started 283276)>, <Thread(w2, started 282088)>, <Thread(w3, started 283576)>, <Thread(w4, started 283236)>]
>>r
>>worker over w2
r
>>worker over w3
r
>>worker over w4
quit
Process finished with exit code 0
上例可以看出不管在哪一个线程中,只要对一个已经上锁的锁阻塞请求,该线程就会阻塞。
练习
订单要求生产1000个杯子,组织10个工人生产。请忽略老板,关注工人生成杯子
import threading
from threading import Thread, Lock
import logging
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("I'm 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 finished my job. cups = {}'.format(len(cups)))
for _ in range(10):
Thread(target=worker, args=(1000,)).start()
- 上例中共有三处可以释放锁。只有第二出释放锁的位置正确
- 假设位置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
方法被执行,否则,就出现了死锁,得到锁的永远没有释放锁
加锁、解锁
- 一般来说,加锁就需要解锁,但是加锁后解锁前,还要有一些代码执行,就有可能抛异常,一旦出现异常,锁是无法释放,但是当前线程可能因为这个异常被终止了,这也产生了死锁。
加锁、解锁常用语句:
- 使用
try...finally
语句保证锁的释放 -
with
上下文管理,锁对象支持上下文管理
- 计数器类,可以加,可以减
import threading
from threading import Thread, Lock
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())
锁的应用场景
锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候
如果全部都是读取同一个共享资源不需要加锁,因为这时可以认为共享资源是不可变的,每一次读取它都是一样的值,所以不用加锁
使用锁的注意事项:
- 少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就成了串行,要么排队执行,要么争抢执行
- 举例,高速公路上车并行跑,可是到了省界只开放了一个收费口,过了这个口,车辆依然可以在多车道上一起跑。过收费口的时候,如果排队一辆辆过,加不加锁一样效率相当,但是一旦出现争抢,就必须加锁一辆辆过。注意,不管加不加锁,只要是一辆辆过,效率就下降了
- 加锁时间越短越好,不需要就立即释放锁
- 一定要避免死锁
不使用锁,有了效率,但是结果是错的
使用了锁,效率低下,但是结果是对的
但是为了保证数据的正确性,必要时还是得需要使用锁
非阻塞锁使用
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='woker={}'.format(i+1), args=(lock,)).start()
# 执行结果
2019-06-09 19:53:51,233 woker=1 301596 do something.
2019-06-09 19:53:51,234 woker=1 301596 try again
2019-06-09 19:53:51,234 woker=2 301600 try again
2019-06-09 19:53:51,234 woker=3 301604 try again
2019-06-09 19:53:51,235 woker=4 301608 try again
2019-06-09 19:53:51,239 woker=5 301612 try again
2019-06-09 19:53:52,234 woker=1 301596 try again
2019-06-09 19:53:52,235 woker=3 301604 try again
······
可重入锁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('-' * 30)
print(lock.acquire(blocking=False))
print(lock.acquire())
print(lock.acquire(timeout=3))
print(lock.acquire(blocking=False))
# print(lock.acquire(blocking=False, timeout=10)) # ValueError: can't specify a timeout for a non-blocking call
lock.release()
lock.release()
lock.release()
lock.release()
print('main thread {}'.format(threading.main_thread().ident))
print('lock in main thread {}'.format(lock))
lock.release()
# lock.release() # release多了抛RuntimeError异常
print('-' * 30)
# 主线程获取锁
print(lock.acquire(blocking=False)) # count = 1
# threading.Thread(target=lambda l:l.release(), args=(lock,)).start() # 跨线程抛RuntimeError异常
lock.release()
print('-' * 30)
# 测试主线程
print(lock.acquire()) # count = 1
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')
# l.release() # 不能多释放
print('+' * 30)
threading.Timer(2, sub, (lock,)).start()
print('in main thread, {}'.format(lock.acquire()))
lock.release()
time.sleep(3)
print('release lock in main thread~~~~~', end='\n\n')
lock.release() # count = 0
- 执行结果
True
------------------------------
True
True
True
True
main thread 304776
lock in main thread <locked _thread.RLock object owner=304776 count=1 at 0x000002158329CC38>
------------------------------
True
------------------------------
True
++++++++++++++++++++++++++++++
in main thread, True
release lock in main thread~~~~~
<Timer(Thread-1, started 304796)>: True
<Timer(Thread-1, started 304796)>: True
lock in sub thread <locked _thread.RLock object owner=304796 count=2 at 0x000002158329CC38>
release in sub 1
release in sub 2
可重入锁总结
- 与线程相关,可在一个线程中获取锁,并可继续在同一线程中不阻塞多次获取锁
- 当锁未释放完,其它线程获取锁就会阻塞,直到当前持有锁的线程释放完锁
- 锁都应该使用完后释放。可重入锁也是锁,应该acquire多少次,就release多少次
Condition
- 构造方法
Condition(lock=None)
,可以传入一个Lock或RLock对象,默认是RLock
名称 | 含义 |
acquire(*args) | 获取锁 |
wait(self, timeout=None) | 等待或超时 |
notify(n=1) | 唤醒至多指定数目个数的等待的线程,没有等待的线程就没有任何操作 |
notify_all() | 唤醒所有等待的线程 |
Condition基本使用
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
# 一个工人生产1000个杯子,有2个老板等到他生产为止
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.000001)
cups.append(1)
print("finishd.{} cups = {}".format(threading.current_thread().name, len(cups)))
# c.notify_all()
c.notify(2)
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()
# 执行结果
2019-06-09 20:29:59,499 boss1 301296 I'm waiting for U
I'm working worker
2019-06-09 20:29:59,499 boss2 304104 I'm waiting for U
finishd.worker cups = 1000
2019-06-09 20:30:00,718 boss1 301296 Good Job
2019-06-09 20:30:00,718 boss2 304104 Good Job
Condition用于生产者、消费者模型,为了解决生产者消费者速度匹配问题
- 下例只是为了演示,不考虑线程安全问题
from threading import Event, Thread, Condition
import logging
import random
FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
class Dispachter:
def __init__(self):
self.data = None
self.event = 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) # 模拟生产数据需要耗时1秒
def consume(self):
while not self.event.is_set():
with self.cond:
self.cond.wait()
data = self.data
logging.info('recieced {}'.format(data))
# self.data = NoNE
# self.event.wait(0.5) # 模拟消费速度
d = Dispachter()
p = Thread(target=d.produce, name='producer', args=(10,))
# 增加消费者
for _ in range(5):
c = Thread(target=d.consume, name='consumer')
c.start()
p.start()
- 执行结果
2019-06-09 20:40:45,322 producer 307312 64
2019-06-09 20:40:45,323 consumer 307292 recieced 64
2019-06-09 20:40:45,323 consumer 307300 recieced 64
2019-06-09 20:40:45,323 consumer 307308 recieced 64
2019-06-09 20:40:45,323 consumer 307304 recieced 64
2019-06-09 20:40:45,323 consumer 307296 recieced 64
2019-06-09 20:40:46,323 producer 307312 65
2019-06-09 20:40:46,323 consumer 307300 recieced 65
2019-06-09 20:40:46,323 consumer 307304 recieced 65
2019-06-09 20:40:46,323 consumer 307292 recieced 65
2019-06-09 20:40:46,323 consumer 307308 recieced 65
2019-06-09 20:40:46,323 consumer 307296 recieced 65
······
self.cond.notify_all()
# 发通知
修改为self.cond.notify(n=2)
打印结果为
2019-06-09 20:44:34,216 producer 307868 49
2019-06-09 20:44:34,217 consumer 302600 recieced 49
2019-06-09 20:44:34,217 consumer 305940 recieced 49
2019-06-09 20:44:35,218 producer 307868 24
2019-06-09 20:44:35,218 consumer 306812 recieced 24
2019-06-09 20:44:35,218 consumer 307840 recieced 24
2019-06-09 20:44:36,218 producer 307868 91
2019-06-09 20:44:36,218 consumer 307416 recieced 91
2019-06-09 20:44:36,218 consumer 302600 recieced 91
2019-06-09 20:44:37,218 producer 307868 7
2019-06-09 20:44:37,218 consumer 306812 recieced 7
2019-06-09 20:44:37,219 consumer 305940 recieced 7
······
这个例子,可以看到实现了消息的 一对多 ,这其实就是 广播模式
注:上例中,程序本身不是线程安全的,程序逻辑有很多瑕疵,但是可以很好的帮助理解Condition的使用和生产者消费者模型
Condition总结
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,都会被阻塞
from threading import Thread, Semaphore
import logging
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=5))
# 释放一个
logging.info('release one')
s.release()
#执行结果
2
2019-06-09 21:10:28,227 MainThread 308868 True
2019-06-09 21:10:28,228 MainThread 308868 True
2019-06-09 21:10:28,228 MainThread 308868 True
1
0
2019-06-09 21:10:28,230 Thread-1 307740 in worker thread
2019-06-09 21:10:30,244 MainThread 308868 False
2019-06-09 21:10:35,245 MainThread 308868 False
2019-06-09 21:10:35,245 MainThread 308868 release one
2019-06-09 21:10:35,245 Thread-1 307740 True
2019-06-09 21:10:35,245 Thread-1 307740 worker thread over
release方法超界问题
- 假设如果还没有acquire信号量,就release,会怎么样?
import logging
import threading
sema = threading.Semaphore(3)
logging.warning(sema.__dict__)
for _ in range(3):
sema.acquire()
logging.warning('~~~~~')
logging.warning(sema.__dict__)
for _ in range(4):
sema.release()
logging.warning(sema.__dict__)
for _ in range(3):
sema.release()
logging.warning('~~~~~')
logging.warning(sema.__dict__)
sema.acquire()
logging.warning('~~~~~')
logging.warning(sema.__dict__)
# 执行结果
WARNING:root:{'_cond': <Condition(<unlocked _thread.lock object at 0x0000017E1EC8B9B8>, 0)>, '_value': 3}
WARNING:root:~~~~~
WARNING:root:{'_cond': <Condition(<unlocked _thread.lock object at 0x0000017E1EC8B9B8>, 0)>, '_value': 0}
WARNING:root:{'_cond': <Condition(<unlocked _thread.lock object at 0x0000017E1EC8B9B8>, 0)>, '_value': 4}
WARNING:root:~~~~~
WARNING:root:{'_cond': <Condition(<unlocked _thread.lock object at 0x0000017E1EC8B9B8>, 0)>, '_value': 7}
WARNING:root:~~~~~
WARNING:root:{'_cond': <Condition(<unlocked _thread.lock object at 0x0000017E1EC8B9B8>, 0)>, '_value': 6}
从上例输出结果可以看出,竟然内置计数器达到了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(self.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就会抛异常的。如何解决?
1、加锁,在读写的地方加锁
2、使用信号量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(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(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()
# 执行结果
2019-06-09 21:29:42,950 worker-0 310848 get~~~~~~~~~~
2019-06-09 21:29:42,950 worker-0 310848 --------------
2019-06-09 21:29:42,951 worker-1 305300 get~~~~~~~~~~
2019-06-09 21:29:42,951 worker-0 310848 <__main__.Conn object at 0x0000017B291A35F8>
2019-06-09 21:29:42,951 worker-1 305300 --------------
2019-06-09 21:29:42,951 worker-2 306228 get~~~~~~~~~~
2019-06-09 21:29:42,951 worker-3 310728 get~~~~~~~~~~
2019-06-09 21:29:42,951 worker-1 305300 <__main__.Conn object at 0x0000017B291A3550>
2019-06-09 21:29:42,951 worker-2 306228 --------------
2019-06-09 21:29:42,952 worker-4 310832 get~~~~~~~~~~
2019-06-09 21:29:42,952 worker-5 308412 get~~~~~~~~~~
2019-06-09 21:29:42,952 worker-2 306228 <__main__.Conn object at 0x0000017B291A3438>
2019-06-09 21:29:44,952 worker-0 310848 return~~~~~~~~~
2019-06-09 21:29:44,952 worker-3 310728 --------------
2019-06-09 21:29:44,952 worker-3 310728 <__main__.Conn object at 0x0000017B291A35F8>
2019-06-09 21:29:44,953 worker-1 305300 return~~~~~~~~~
2019-06-09 21:29:44,953 worker-4 310832 --------------
2019-06-09 21:29:44,953 worker-4 310832 <__main__.Conn object at 0x0000017B291A3550>
2019-06-09 21:29:45,953 worker-2 306228 return~~~~~~~~~
2019-06-09 21:29:45,953 worker-5 308412 --------------
2019-06-09 21:29:45,953 worker-5 308412 <__main__.Conn object at 0x0000017B291A3438>
2019-06-09 21:29:46,953 worker-5 308412 return~~~~~~~~~
2019-06-09 21:29:47,953 worker-4 310832 return~~~~~~~~~
2019-06-09 21:29:48,953 worker-3 310728 return~~~~~~~~~
上例中,使用信号量解决资源有限的问题
如果池中有资源,请求者获取资源时信号量减1,拿走资源。当请求超过资源数,请求者只能等待。当使用者用完归还资源后信号量加1,等待线程就可以被唤醒拿走资源
- 注意:这个连接池的例子不能用到生成环境,只是为了说明信号量使用的例子,连接池还有很多未完成功能
问题
self.conns.append(conn) 这一句有哪些问题考虑?
1、边界问题分析
return_conn方法可以单独执行,有可能多归还连接,也就是会多release,所以,要用有界信号量
BoundedSemaphore类
这样用有界信号量修改源代码,保证如果多return_conn就会抛异常
self.pool.append(conn)
self.semaphore.release()
- 假设一种极端情况,计数器还差1就归还满了,有三个线程A、B、C都执行了第一句,都没有来得及release,这时候轮到线程A release,正常的release,然后轮到线程C先release,一定出问题,超界了,直接抛异常
- 因此信号量,可以保证,一定不能多归还
- 如果归还了同一个连接多次怎么办,重复很容易判断
这个程序还不能判断这些连接是不是原来自己创建的,这不是生成环境用的代码,只是简单演示
2、正常使用分析
- 正常使用信号量,都会先获取信号量,然后用完归还
- 创建很多线程,都去获取信号量,没有获得信号量的线程都阻塞。能归还的线程都是前面获取到信号量的线程,其他没有获得线程都阻塞着。非阻塞的线程append后才release,这时候等待的线程被唤醒,才能pop,也就是没有获取信号量就不能pop,这是安全的
- 经过上面的分析,信号量比计算列表长度好,线程安全
信号量和锁
- 信号量,可以多个线程访问共享资源,但这个共享资源数量有限
- 锁,可以看做特殊的信号量,即信号量计数器初值为1。只允许同一个时间一个线程独占资源