Python多线程同步编程

概念

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

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,只允许同一个事件是一个线程独占资源.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值