Python多线程/event
多线程-threading
python的thread模块是⽐较底层的模块, python的threading
模块是对thread做了⼀些包装的, 可以更加⽅便的被使⽤
1. 使⽤threading模块
单线程执⾏
import time
def saySorry():
print("亲爱的, 我错了, 我能吃饭了吗? ")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
saySorry()
运⾏结果: 打印了五次花了五秒
亲爱的, 我错了, 我能吃饭了吗?
亲爱的, 我错了, 我能吃饭了吗?
亲爱的, 我错了, 我能吃饭了吗?
亲爱的, 我错了, 我能吃饭了吗?
亲爱的, 我错了, 我能吃饭了吗? Process finished with exit code 0
多线程执⾏ :一起打印,花了一秒
#coding=utf-8
import threading
import time
def saySorry():
print("亲爱的, 我错了, 我能吃饭了吗? ")
time.sleep(1) if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=saySorry)
t.start() #启动线程, 即让线程开始执⾏
说明
1. 可以明显看出使⽤了多线程并发的操作, 花费时间要短很多
2. 创建好的线程, 需要调⽤ start() ⽅法来启动
2. 主线程会等待所有的⼦线程结束后才结束
#coding=utf-8
import threading
from time import sleep,ctime def sing():
for i in range(3):
print("正在唱歌...%d"%i)
sleep(1) def dance():
for i in range(3):
print("正在跳舞...%d"%i)
sleep(1) if __name__ == '__main__':
print('---开始---:%s'%ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
#sleep(5) # 屏蔽此⾏代码, 试试看, 程序是否会⽴⻢结束?
print('---结束---:%s'%ctime())
3. 查看线程数量
#coding=utf-8
import threading
from time import sleep,ctime def sing():
for i in range(3):
print("正在唱歌...%d"%i)
sleep(1) def dance():
for i in range(3):
print("正在跳舞...%d"%i)
sleep(1) if __name__ == '__main__':
print('---开始---:%s'%ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start() while True:
length = len(threading.enumerate())
print('当前运⾏的线程数为: %d'%length)
if length<=1:
break
sleep(0.5)
event
flag = threading.Event() # 这个对象里面的值默认是 False
flag.wait() # 查看 flag 里面的值,如果这个值是 false 的话,就在这里等待它变成 True ,然后再执行后面的代码,括号里面可以写参数,意思为 超时执行
flag.set() # 把 flag 里面的值改为 True
flag.clear() # 把 flag 里面的值改为 False
import threading
import time
import random def foo(n):
count = 1
while not event.is_set():
print("%s:I will connect Server.....%s"%(n, count))
event.wait(2)
count += 1 print("aha i got it.....") if __name__ == "__main__":
event = threading.Event() for i in range(5):
t = threading.Thread(target=foo, args=(i+1,))
t.start() time.sleep(100) event.set()
互斥锁
我们兴许会写出这样的代码,我们假设跑100个线程,但是这100个线程都会去访问某个公共资源(比如说下面的 num 这个全局变量),
并对该资源进行处理(num -= 1)
import time
import threading num = 100 def sub():
global num tmp = num
time.sleep(0.0001)
num = tmp-1 time.sleep(2) if __name__ == "__main__":
l = []
for i in range(100):
tb = threading.Thread(target=sub)
tb.start()
l.append(tb) for tb in l:
tb.join() print(num)
但是我们看下运行结果: 72
上面的运行结果通常为小于100,大于0
这是因为我们没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。
这种现象称为“线程不安全”。在开发过程中我们必须要避免这种情况,那怎么避免?这就用到了我们在综述中提到的互斥锁了。
互斥锁概念
Python编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。这个标记用来保证在任一时刻,只能有一个线程访问该对象。在Python中我们使用threading模块提供的Lock类。
我们对上面的程序进行整改,为此我们需要添加一个互斥锁变量lock = threading.Lock(),然后在争夺资源的时候之前我们会先抢占这把锁lock.acquire(),对资源使用完成之后我们在释放这把锁lock.release
def sub():
global num lock.acquire()
tmp = num
time.sleep(0.0001)
num = tmp-1
lock.release()
time.sleep(2) if __name__ == "__main__":
lock = threading.Lock() l = []
for i in range(100):
tb = threading.Thread(target=sub)
tb.start()
l.append(tb) for tb in l:
tb.join() print(num)
同步阻塞
当一个线程调用Lock对象的acquire()方法获得锁时,这把锁就进入“locked”状态。因为每次只有一个线程1可以获得锁,所以如果此时另一个线程2试图获得这个锁,该线程2就会变为阻塞状态。直到拥有锁的线程1调用锁的release()方法释放锁之后,该锁进入“unlocked”状态。线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
进一步考虑
通过对公共资源使用互斥锁,这样就简单的到达了我们的目的,但是如果我们又遇到下面的情况:
遇到锁嵌套的情况该怎么办,这个嵌套是指当我一个线程在获取临界资源时,又需要再次获取;
如果有多个公共资源,在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源;
上述这两种情况会直接造成程序挂起,即死锁,下面我们会谈死锁及递归锁(可重入锁)RLock。
所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,
若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,
这些永远在互相等待的进程称为死锁进程。 由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,
永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。
import time
import threading
import random lockA = threading.Lock()
lockB = threading.Lock() class MyThread(threading.Thread):
# 我这里并没有重写 __init__ 这个函数,意思为,继承父类的,反正我也没打算加些新的内容进去 def run(self):
self.foo()
self.bar() def foo(self):
global lockA
global lockB
lockA.acquire()
print("%s:I am from foo, and i have locakA"%self.name) time.sleep(random.random()) lockB.acquire()
print("%s:I am from foo, and i have locakB"%self.name) lockB.release() lockA.release() def bar(self):
global lockA
global lockB
lockB.acquire()
print("%s:I am from foo, and i have locakB" % self.name) time.sleep(random.random()) lockA.acquire()
print("%s:I am from foo, and i have locakA" % self.name) lockA.release() lockB.release() for i in range(10):
mt = MyThread()
mt.start()
代码中展示了一个线程的两个功能函数分别在获取了一个竞争资源之后再次获取另外的竞争资源,我们看运行结果:
Thread-1:I am from foo, and i have locakA
Thread-1:I am from foo, and i have locakB
Thread-1:I am from foo, and i have locakB
Thread-2:I am from foo, and i have locakA
可以看到,程序已经挂起在那儿了,这种现象我们就称之为”死锁“。
避免死锁主要方法就是:正确有序的分配资源,避免死锁算法中最有代表性的算法
或者我们可以使用一个叫做 递归锁 的东西
递归锁
RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。创建好的递归锁初始计数为0,每次被调用(acquire)的时候,它的计数就会加1, 每次释放(release)的时候,它的计数就会减1,只有它的计数为 0 的时候,才能被别的线程抢夺注意:如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的琐。这样的话,就可以解决死锁的问题了
import threading
import time
import random RLock = threading.RLock() class MyThread(threading.Thread):
# 我这里并没有重写 __init__ 这个函数,意思为,继承父类的,反正我也没打算加些新的内容进去 def run(self):
self.foo()
self.bar() def foo(self): RLock.acquire()
print("%s:I am from foo, and i have locakA"%self.name) time.sleep(random.random()) RLock.acquire()
print("%s:I am from foo, and i have locakB"%self.name) RLock.release() RLock.release() def bar(self): RLock.acquire()
print("%s:I am from foo, and i have locakB" % self.name) time.sleep(random.random()) RLock.acquire()
print("%s:I am from foo, and i have locakA" % self.name) RLock.release() RLock.release() for i in range(10):
mt = MyThread()
mt.start()
信号量semaphore
创建方法:threading.semaphore(5)
括号里面的数字为,最大运行线程数信号量,控制着对公共资源或者临界区的访问。
信号量维护着一个计数器,指定可同时访问资源或者进入临界区的线程数。每次有一个线程获得信号量时,计数器-1。若计数器为0,其他线程就停止访问信号量,直到另一个线程释放信号量。
import threading
import time
import random def foo(n):
sl.acquire()
print("I am come from foo....%s"%n)
time.sleep(random.random())
sl.release() if __name__ == "__main__":
sl = threading.Semaphore(5)
for i in range(100):
t1 = threading.Thread(target=foo, args=(i,))
t1.start()