python线程同步锁_python的Lock锁,线程同步

本文介绍了如何在Python中使用Lock、RLock、Condition、Semaphore和Barrier进行线程同步,包括加锁、解锁机制,以及它们在生产者消费者模型、资源限制和任务同步中的应用。着重讨论了死锁预防和使用场景的最佳实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、Lock锁

凡是存在共享资源争抢的地方都可以使用锁,从而保证只有一个使用者可以完全使用这个资源一旦线程获得锁,其他试图获取锁的线程将被阻塞

acquire(blocking=True,timeout=-1): 默认阻塞,阻塞可以设置超时时间,非阻塞时,timeout禁止设置,成功获取锁,返回True,否则返回False

releas() : 释放锁,可以从任何线程调用释放,已上锁的锁,会被重置为unlocked未上锁的锁上调用,抛出RuntimeError异常

例如:

订单要求生成1000个杯子,组织10个工人生产

举例1:importthreadingfrom threading importThread, Lockimporttimeimportlogging

FORAMT= '%(asctime)s %(threadName)s %(thread)d %(message)s'logging.basicConfig(format=FORAMT, level=logging.INFO)

cups=[]def worker(count=10):

logging.info("I'm working for u")

flag=FalsewhileTrue:if len(cups) >=count:

flag=True

time.sleep(0.001) #为了看出现场切换效果

if notflag:

cups.append(1)ifflag:breaklogging.info("{} finished.cups= {}".format(threading.current_thread().name, len(cups)))for _ in range(10): #开启10个线程

Thread(target=worker, args=(1000,)).start()

从上例的运行结果看出,多线程调度,导致了判断失效,多生成了杯子

如何修改,加锁

举例2:importthreadingfrom threading importThread, Lockimporttimeimportlogging

FORAMT= '%(asctime)s %(threadName)s %(thread)d %(message)s'logging.basicConfig(format=FORAMT, level=logging.INFO)

cups=[]

lock=Lock()def worker(count=10):

logging.info("I'm working for u")

flag=FalsewhileTrue:

lock.acquire()if len(cups) >=count:

flag=True#lock.release() # 这里释放锁合适吗?

time.sleep(0.001) #为了看出现场切换效果

if notflag:

cups.append(1)

lock.release()#这里释放锁对不对

ifflag:breaklogging.info("{} finished.cups= {}".format(threading.current_thread().name, len(cups)))for _ in range(10): #开启10个线程

Thread(target=worker, args=(1000,)).start()

假设第一句lock.release()合适,分析如下:

有一个时刻len(cups),正好是999,flag=True,释放锁,线程被打断,另一个线程判断发现是999,flag=True,可能线程被打断,可能另一个线程判断也是999,flag也设置为True

这三个线程只要继续执行到cups.append(1),一定会导致cups的长度超过1000

假设第二句lock.release()合适,分析如下:

在某一时刻len(cups),正好是999,flag=True,其他线程试图访问这段代码的线程都被阻塞获取

不到锁,直到当前线程安全的增加了一个数据,然后释放锁,其它线程有一个抢到锁,但发现

已经是1000了,只好break打印退出,所有其他线程接着都退出

二、加锁、解锁

一般来说加锁后还要一些代码实现,在释放锁之前还有可能抛异常,一旦出现异常所无法释放,但是当前线程可能因为这个异常被终止了,这就产生了死锁

加锁、解锁常用语句:

使用try...finally语句保证锁的释放

with上下文管理,锁对象支持上下文管理

importthreadingfrom threading importThread, LockimporttimeclassCounter:def __init__(self):

self._val=0

self.__lock =Lock()

@propertydefvalue(self):

with self.__lock:returnself._valdefinc(self):try:

self.__lock.acquire()

self._val+= 1

finally:

self.__lock.release()defdec(self):

with self.__lock:

self._val-= 1

def run(c:Counter, count=100):for _ inrange(count):for i in range(-50,50):if i <0:

c.dec()else:

c.inc()

c=Counter()

c1= 10c2= 100

for i inrange(c1):

Thread(target=run, args=(c,c2)).start()whileTrue:

time.sleep(1)if threading.active_count() == 1:print(threading.enumerate())print(c.value)else:print(threading.enumerate())

三、锁的应用场景

锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候,如果全部都是读取同一个共享资源需要锁吗,不需要,因为这时可以认为共享资源是不可变的,每一次读取它都是一样的值,所有不用加锁

使用锁的注意事项

少用锁,必要时用锁,使用了锁,多线程访问被锁的资源时,就成了串行,要么排队,要么争抢

加锁时间越短越好,不需要就立即释放锁

一定要避免死锁

1、可重入锁Rlock

可重入锁,是线程相关的锁,线程A获得可重复锁,并可以多次成功获取,不会阻塞,最后要在线程A中做和acquire次数相同的relea

importthreadingimporttime

lock=threading.RLock()print(lock.acquire())print('-------------------------')print(lock.acquire(blocking=False))print(lock.acquire())print(lock.acquire(timeout=3.55))print(lock.acquire(blocking=False))

lock.release()

lock.release()

lock.release()

lock.release()

lock.release()

四、Condition

构造方法Condition(lock=None),可以传入一个Lock或RLock对象,默认是RLock

Conditon用于生产者,消费者模型,为了解决生产者消费者速度匹配问题

acquire(*args) : 获取锁

wait(self,time=None) : 等待或超市

notify(n=1) : 唤醒至多指定数目个数的等待线程,没有等待的线程就没有任何操作

notif_all() : 唤醒所有等待的线程

举例1: 消费者消费速度大于生产者生成速度from threading importThread, Lock, Event, Conditionimporttime, randomimportlogging

FORAMT= '%(asctime)s %(threadName)s %(thread)d %(message)s'logging.basicConfig(format=FORAMT, level=logging.INFO)#此例只是为了演示,不考虑线程安全问题

classDispatcher:def __init__(self):

self.data=None

self.event= Event() #event只是为了使用方便,与逻辑无关

self.cond =Condition()defproduce(self, total):for _ inrange(total):

data= random.randint(0,100)

with self.cond:

logging.info(data)

self.data=data

self.cond.notify_all()#通知消费者消费,启动所有线程

self.event.wait(1) #模拟产生数据速度

self.event.set()defconsume(self):while notself.event.is_set():

with self.cond:#消费者等待

self.cond.wait()

logging.info("received {}".format(self.data))

self.data=None

self.event.wait(0.5) #模拟消费的速度

d=Dispatcher()

p= Thread(target=d.produce, args=(10,), name='prodcer')

c= Thread(target=d.consume, name='consumer')

c.start()

p.start()

消费者等待数据,如果生产者准备好了会通知消费者消费,省得消费者反复来查看数据是否就绪

Condition用于生产者消费者模型中,解决生产者消费者速度匹配的问题

采用了通知机制,非常有效率

使用方式,使用Condition,必须先acquire,用完了要release,因为内部使用了锁,默认使用了RLock锁,

最好方式是使用with上下文管理

消费者wait,等待通知,生产者生产好消息,对消费者发通知,可以使用notify或者notify_all方法

五、线程同步Barrier

有人翻译成栅栏,建设理解成屏障,可以想象成路障,道闸

Barrier(parties, action=None, timeout=None): 构建Barrier对象,指定参与方数目,timeout是wait方法未指定超时的默认值

n_waiting : 当前在屏障中等待的线程数

parties : 各方数,就是需要多少个等待

wait(timeout=None) : 等待通过屏障,返回0到线程数-1的整数,每个线程返回不同,如果wait方法设置了超时,并超时发送,屏障将处于broken状态

1、Barrier实例

broken :如果屏障处于打破的状态,返回True

abort() : 将屏障置于broken状态,等待中的线程或者调用等待方法的线程中都会抛出BrokenBarrierError异常,直到reset方法来恢复屏障

reset(): 恢复屏障,重新开始拦截

importthreadingimportlogging#输出格式定义

FORMAT = '%(asctime)-15s\t [%(threadName)s, %(thread)8d] %(message)s'logging.basicConfig(level=logging.INFO, format=FORMAT)defworker(barrier:threading.Barrier):

logging.info('waiting for {} threads'.format(barrier.n_waiting))try:

barrier_id=barrier.wait()

logging.info('after barrier {}'.format(barrier_id))exceptthreading.BrokenBarrierError:

logging.info('Broken Barrier')

barrier= threading.Barrier(3) #设置拦截线程数

for x in range(3):

threading.Thread(target=worker, name='worker-{}'.format(x), args=(barrier,)).start()

logging.info('started')

从运行结果看出:

所有线程冲到了Barrier前等待,直到到达parties的数目,屏障打开,所有线程停止等待,继续执行

再有线程wait,屏障就绪等到到达参数方数目

举例 ,赛马比赛所有马匹就位,开闸

举例2:importthreadingimportlogging#输出格式定义

FORMAT = '%(asctime)-15s\t [%(threadName)s, %(thread)8d] %(message)s'logging.basicConfig(level=logging.INFO, format=FORMAT)defworker(barrier:threading.Barrier):

logging.info('waiting for {} threads'.format(barrier.n_waiting))try:

barrier_id=barrier.wait()

logging.info('after barrier {}'.format(barrier_id))exceptthreading.BrokenBarrierError:

logging.info('Broken Barrier run')

barrier= threading.Barrier(3)for x in range(0,9):if x == 2:

barrier.abort()if x == 6:

barrier.reset()

threading.Event().wait(1)

threading.Thread(target=worker, name='worker-{}'.format(x), args=(barrier,)).start()

logging.info('started')

上例中等待了2个,屏障就被break了,waiting的线程抛了BrokenBarrierError异常

新wait的线程也抛异常,直到屏障恢复,才继续按照parties数目要求继续拦截线程

2、wait方法超时实例

如果wait方法超时发生,屏障将处于broken状态,直到reset

importthreadingimportlogging#输出格式定义

FORMAT = '%(asctime)-15s\t [%(threadName)s, %(thread)8d] %(message)s'logging.basicConfig(level=logging.INFO, format=FORMAT)defworker(barrier:threading.Barrier, i:int):

logging.info('waiting for {} threads'.format(barrier.n_waiting))try:

logging.info(barrier.broken)#是否broken

if i < 3:

barrier_id= barrier.wait(1) #超时后,屏障broken

else:if i == 6:

barrier.reset()#恢复屏障

barrier_id = barrier.wait(1)

logging.info('after barrier {}'.format(barrier_id))exceptthreading.BrokenBarrierError:

logging.info('Broken Barrier run')

barrier= threading.Barrier(3)for x in range(0,9):

threading.Event().wait(2)

threading.Thread(target=worker, name='worker-{}'.format(x), args=(barrier,x)).start()

logging.info('started')

3、Barrier应用

所有线程都必须初始化完成,才能继续工作,例如运行前加载数据,检查,如果这些工作没有完成,就开始运行,将不能正常工作

10个线程做10种工作准备,每个线程负责一种工作,只有这10个线程都完成后,才能继续工作,先完成的要等待后完成的线程

六、semaphore信号量

和Lock很像,信号量对内部维护一个到计数器,每一次acquire都会减1,

当acquire方法发现计数为0就阻塞请求的线程,直到其他线程对信号量release后,计数大于0,恢复阻塞线程

Semaphore(value=1): 构造方法,value小于0,抛出异常ValueError异常

acquire(blocking=True,time=None) : 获取信号量,计数器减1,获取成功返回True

release() : 释放信号量,计数器加1

计数器永远不会低于0,因为acquire的时候,发现是0,都会被阻塞

importthreadingimportloggingimporttime#输出格式定义

FORMAT = '%(asctime)-15s\t [%(threadName)s, %(thread)8d] %(message)s'logging.basicConfig(level=logging.INFO, format=FORMAT)defworker(barrier:threading.Semaphore):

logging.info('in sub thread')

logging.info(s.acquire())

logging.info('sub thread over')#信号量

s = threading.Semaphore(3)

logging.info(s.acquire())

logging.info(s.acquire())

logging.info(s.acquire())

threading.Thread(target=worker, args=(s,)).start()

time.sleep(2)

logging.info(s.acquire(False))

logging.info(s.acquire(timeout=3)) #阻塞

#释放

logging.info('released')

s.release()

应用举例:

实现一个简单的连接池,连接池应该有容量,有一个工厂方法可以获取连接,能够被不要的连接返回,供其他调用者使用importthreadingimportloggingimportrandom#输出格式定义

FORMAT = '%(asctime)-15s\t [%(threadName)s, %(thread)8d] %(message)s'logging.basicConfig(level=logging.INFO, format=FORMAT)classConn:def __init__(self, name):

self.name=namedef __repr__(self):returnself.nameclassPool:def __init__(self, count:int):

self.count=count#池中是连接对象列表

self.pool = [self._connect("conn-{}".format(x)) for x inrange(self.count)]

self.semaphore=threading.Semaphore(count)def_connect(self, conn_name):#返回一个名称

returnConn(conn_name)defget_conn(self):#从池中拿走一个连接

self.semaphore.acquire()

conn=self.pool.pop()returnconndefreturn_conn(self, conn:Conn):#向池中添加一个连接

self.pool.append(conn)

self.semaphore.release()#连接池初始化

pool = Pool(3)defworker(pool:Pool):

conn=pool.get_conn()

logging.info(conn)#模拟使用了一段时间

threading.Event().wait(random.randint(1,4))

pool.return_conn(conn)for i in range(6):

threading.Thread(target=worker, name="worker-{}".format(i), args=(pool,)).start()

上例中,使用信号量解决资源有限的问题

如果池中有资源,请求者获取资源时信号量减1,拿走资源,当请求超过资源数,请求者只能等待,

当使用者用完归还资源后信号量加1,等待线程拿到就可以唤醒拿走资源

1、信号量和锁

锁,只允许同一个时间一个线程独占资源,它是特殊的信号量,即信号量计数器初值为1

信号量,允许多个线程访问共享资源,但是这个共享资源数量有限

锁可以看做是特殊的信号量

2、数据结构和GIL

Queue :标准库queue模块,提供FIFO的Queue,LIFO的队列,优先队列

Queue类是线程安全的,适用于多线程间安全的交换数据,内部使用了Lock和Condition

3、GIL全局解释器锁

Cpython在解释器进程级别有一把锁,叫做GIL全局解释器

GIL保证CPython进程中,只有一个线程执行字节码,甚至是在多核CPU的情况下,也是如此

CPython中,IO密集型,由于线程阻塞,就会调度其他线程,CPU密集型,当前线程可能会连续的获得GIL,导致其他线程几乎无法使用CPU

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值