python 进程线程同步锁、死锁与递归锁、协程和信号量
1、同步锁
# 锁通常被用来实现对共享资源的[同步]访问。 # 为每一个共享资源创建一个Lock对象, # 当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放), # 待资源访问完后,再调用release方法释放锁: import time import threading def addNum(): global num #在每个线程中都获取这个全局变量 #num-=1 # 获得锁锁定 lock.acquire() temp=num time.sleep(0.001) num =temp-1 # 对此公共变量进行-1操作 lock.release() # 释放锁,让别的线程继续用 num = 100 #设定一个共享变量 thread_list = [] # 增加线程同步锁 lock = threading.Lock() for i in range(100): t = threading.Thread(target=addNum) t.start() thread_list.append(t) for t in thread_list: #等待所有线程执行完毕 t.join() print('Result: ', num)
2、死锁现象
# 所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象, # 若无外力作用,它们都将无法推进下去。 # 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 # 解决方案:将所有锁改编成递归锁RLock # 在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。 # 这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。 # 直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁。 # 不要瞧不起最原始的串行,那是解决线程安全问题最原始最有效的方法 # RLock = threading.RLock() # RLock 代替所有mutex import threading import time mutexA = threading.Lock() mutexB = threading.Lock() class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.fun1() self.fun2() def fun1(self): mutexA.acquire() # 如果锁被占用,则阻塞在这里,等待锁的释放 print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time())) mutexB.acquire() print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time())) mutexB.release() mutexA.release() def fun2(self): mutexB.acquire() print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time())) time.sleep(0.2) mutexA.acquire() print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time())) mutexA.release() mutexB.release() if __name__ == "__main__": print("start---------------------------%s"%time.time()) for i in range(0, 10): my_thread = MyThread() my_thread.start()
执行结果:
start---------------------------1494312048.918175
I am Thread-1 , get res: ResA---1494312048.918385
I am Thread-1 , get res: ResB---1494312048.918402
I am Thread-1 , get res: ResB---1494312048.918414
I am Thread-2 , get res: ResA---1494312048.918524
经过递归锁清理死锁后:
import threading import time # mutexA = threading.Lock() # mutexB = threading.Lock() RLock = threading.RLock() class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.fun1() self.fun2() def fun1(self): RLock.acquire() # 如果锁被占用,则阻塞在这里,等待锁的释放 print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time())) RLock.acquire() print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time())) RLock.release() RLock.release() def fun2(self): RLock.acquire() print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time())) time.sleep(0.2) RLock.acquire() print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time())) RLock.release() RLock.release() if __name__ == "__main__": print("start---------------------------%s"%time.time()) for i in range(0, 10): my_thread = MyThread() my_thread.start()
3、线程之间通信:event
# 线程的一个关键特性是每个线程都是独立运行且状态不可预测。
# 如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。
# 为了解决这些问题,我们需要使用threading库中的Event对象。
# 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。
# 在 初始情况下,Event对象中的信号标志被设置为假。
# 如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。
# 一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。
# 如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行
event 常用方法
# event.isSet():返回event的状态值; # # event.wait():如果 event.isSet()==False将阻塞线程; # # event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; # # event.clear():恢复event的状态值为False。
# 可以考虑一种应用场景(仅仅作为说明),例如,我们有多个线程从Redis队列中读取数据来处理, # 这些线程都要尝试去连接Redis的服务,一般情况下,如果Redis连接不成功,在各个线程的代码中, # 都会去尝试重新连接。如果我们想要在启动时确保Redis服务正常,才让那些工作线程去连接Redis服务器, # 那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作:主线程中会去尝试连接Redis服务, # 如果正常的话,触发事件,各工作线程会尝试连接Redis服务。 # event 是如何通信的? import threading import time import logging logging.basicConfig(level=logging.DEBUG, format='(%(threadName)-10s) %(message)s',) #这样,我们就可以在等待Redis服务启动的同时,看到工作线程里正在等待的情况,redis重新分配 # def worker(event): # while not event.is_set(): # logging.debug('Waiting for redis ready...') # event.wait(2) # # # redis准备,连接redis服务并做事 # logging.debug('redis ready, and connect to redis server and do some work [%s]', time.ctime()) # time.sleep(1) def worker(event): logging.debug('Waiting for redis ready...') # 默认event为false,所以阻塞不执行,等待为真 event.wait() logging.debug('redis ready, and connect to redis server and do some work [%s]', time.ctime()) time.sleep(1) def main(): # 创建线程事件对象 readis_ready = threading.Event() # 创建2个线程 t1 = threading.Thread(target=worker, args=(readis_ready,), name='t1') t1.start() t2 = threading.Thread(target=worker, args=(readis_ready,), name='t2') t2.start() # 日志记录 logging.debug('first of all, check redis server, make sure it is OK, and then trigger the redis ready event') # 睡3秒 time.sleep(3) # simulate the check progress # 激活所有正在等待的线程 readis_ready.set() if __name__=="__main__": main()
但是在有些情况下:
# threading.Event的wait方法还接受一个超时参数,默认情况下如果事件一致没有发生, # wait方法会一直阻塞下去,而加入这个超时参数之后,如果阻塞时间超过这个参数设定的值之后, # wait方法会返回。对应于上面的应用场景,如果Redis服务器一致没有启动, # 我们希望子线程能够打印一些日志来不断地提醒我们当前没有一个可以连接的Redis服务, # 我们就可以通过设置这个超时参数来达成这样的目的:
#这样,我们就可以在等待Redis服务启动的同时,看到工作线程里正在等待的情况,redis重新分配
# def worker(event):
# is_set为真while not true 恰好不执行
# is_set为假while not false 恰好执行等待
# while not event.is_set():
# logging.debug('Waiting for redis ready...')
# event.wait(2)
#
# # redis准备,连接redis服务并做事
# logging.debug('redis ready, and connect to redis server and do some work [%s]', time.ctime())
# time.sleep(1)
4、协程单线程解决多线程安全问题
# 数据安全:为什么要锁,因为多线程存在矛盾,容易出现安全问题 # 协程是单线程,因此就不存在线程安全问题 import time """ 传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。 如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,
待消费者执行完毕后,切换回生产者继续生产,效率极高。 """ # 注意到consumer函数是一个generator(生成器): # 任何包含yield关键字的函数都会自动成为生成器(generator)对象 def consumer(): r = '' while True: # 3、consumer通过yield拿到消息,处理,又通过yield把结果传回; # yield指令具有return关键字的作用。然后函数的堆栈会自动冻结(freeze)在这一行。 # 当函数调用者的下一次利用next()或generator.send()或for-in来再次调用该函数时, # 就会从yield代码的下一行开始,继续执行,再返回下一次迭代结果。通过这种方式,迭代器可以实现无限序列和惰性求值。 n = yield r if not n: return print('[CONSUMER] ←← Consuming %s...' % n) time.sleep(1) r = '200 OK' def produce(c): # 1、首先调用c.next()启动生成器 next(c) n = 0 while n < 5: n = n + 1 print('[PRODUCER] →→ Producing %s...' % n) # 2、然后,一旦生产了东西,通过c.send(n)切换到consumer执行; cr = c.send(n) # 4、produce拿到consumer处理的结果,继续生产下一条消息; print('[PRODUCER] Consumer return: %s' % cr) # 5、produce决定不生产了,通过c.close()关闭consumer,整个过程结束。 c.close() if __name__ == '__main__': # 6、整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。 c = consumer() produce(c) ''' result: [PRODUCER] →→ Producing 1... [CONSUMER] ←← Consuming 1... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 2... [CONSUMER] ←← Consuming 2... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 3... [CONSUMER] ←← Consuming 3... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 4... [CONSUMER] ←← Consuming 4... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 5... [CONSUMER] ←← Consuming 5... [PRODUCER] Consumer return: 200 OK '''
5、协程单线程引入greenlet库
# 第三方库greenlet # greelet机制的主要思想是:生成器函数或者协程函数中的yield语句挂起函数的执行, # 直到稍后使用next()或send()操作进行恢复为止。可以使用一个调度器循环在一组生成器函数之间协作多个任务。 # greentlet是python中实现我们所谓的"Coroutine(协程)"的一个基础库. from greenlet import greenlet def test1(): print(12) gr2.switch() print(34) gr2.switch() def test2(): print(56) gr1.switch() print(78) gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch()
6、协程与gevent 第三方库
# Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。 # # gevent是第三方库,通过greenlet实现协程,其基本思想是: # 当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。 # 由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。 # # 由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成: import gevent import time def foo(): print("running in foo") gevent.sleep(2) print("switch to foo again") def bar(): print("switch to bar") gevent.sleep(5) print("switch to bar again") start=time.time() gevent.joinall( [gevent.spawn(foo), gevent.spawn(bar)] ) print(time.time()-start) # 当然,实际代码里,我们不会用gevent.sleep()去切换协程,而是在执行到IO操作时,gevent自动切换,代码如下: from gevent import monkey monkey.patch_all() import gevent from urllib import request import time def f(url): print('GET: %s' % url) resp = request.urlopen(url) data = resp.read() print('%d bytes received from %s.' % (len(data), url)) start=time.time() gevent.joinall([ gevent.spawn(f, 'https://itk.org/'), gevent.spawn(f, 'https://www.github.com/'), gevent.spawn(f, 'https://zhihu.com/'), ]) # f('https://itk.org/') # f('https://www.github.com/') # f('https://zhihu.com/') print(time.time()-start)
7、信号量:
# Semaphore管理一个内置的计数器, # 每当调用acquire()时内置计数器-1; # 调用release() 时内置计数器+1; # 计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。 # # 实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5): # 应用:连接池 # 思考:与Rlock的区别? import threading import time semaphore = threading.Semaphore(5) def func(): if semaphore.acquire(): print (threading.currentThread().getName() + ' get semaphore') time.sleep(2) semaphore.release() for i in range(20): t1 = threading.Thread(target=func) t1.start()
8、进程的调用
注意该模块是#multiprocessing
from multiprocessing import Process import os import time def info(name): print("name:",name) print('parent process:', os.getppid()) print('process id:', os.getpid()) print("------------------") time.sleep(1) def foo(name): info(name) if __name__ == '__main__': info('main process line') p1 = Process(target=info, args=('alvin',)) p2 = Process(target=foo, args=('egon',)) p1.start() p2.start() p1.join() p2.join() print("ending")