Python -- 线程同步

概念

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

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方法被执行,否则,就出现了死锁,得到锁的永远没有释放锁

加锁、解锁

  • 一般来说,加锁就需要解锁,但是加锁后解锁前,还要有一些代码执行,就有可能抛异常,一旦出现异常,锁是无法释放,但是当前线程可能因为这个异常被终止了,这也产生了死锁。
加锁、解锁常用语句:
  1. 使用try...finally语句保证锁的释放
  2. 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。只允许同一个时间一个线程独占资源
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值