Python多线程编程初步探索(二)
多个线程
参考博文:https://www.jianshu.com/p/e50b9e4ce5aa
7. 关于同步、锁的问题
真正的多线程在操作系统底层是如何实现的呢?——GIL(全局解释器锁)。无论你启多少个线程,你有多少个cpu, Python在执行一个进程的时候会淡定的在同一时刻只允许一个线程运行。所以,python是无法利用多核CPU实现多线程的。 这样,python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。详见博文[https://www.jianshu.com/p/e50b9e4ce5aa],这部分不再讨论。
(1)当两个并行的线程同时要访问或修改一个数据时,怎么办呢?是谁来修改?这就关乎同步的问题了。
先看代码:
import time
import threading
def addNum():
global num # 在每个线程中都获取这个全局变量
# ''' 实验1
# num -= 1
# time.sleep(1)
# ''' 实验1
# ''' 实验2
# temp = num
# time.sleep(1)
# num = temp - 1 # 对此公共变量进行-1操作
# ''' 实验2
# ''' 实验3
# temp = num
# time.sleep(0.0001)
# num = temp - 1 # 对此公共变量进行-1操作
# ''' 实验3
# ''' 实验4
lock.acquire() # 获取这把锁
temp = num
time.sleep(0.01)
num = temp - 1
lock.release() # 释放这把锁
time.sleep(5)
# ''' 实验4
num = 100 # 设定一个共享变量
thread_list = []
lock=threading.Lock() #创建一把锁
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
# 如果这里紧跟t.join(),则start一个线程后,该线程随即阻塞主线程。
# 主线程上该线程运行完毕后,主线程方能进入下一次循环开启新线程。这里有join的话就不能实现多线程。
print(threading.enumerate())
thread_list.append(t)
for t in thread_list: # 等待所有线程执行完毕
t.join() # 这里的join与上面的情况就不同了。此时多个线程(如100个)已经启动了,对于线程列表中的每个线程依次阻塞主线程。
# 某一时间只有1个线程阻塞了主线程,或者说,主线程等待所有线程结束后方才结束。
# print(threading.enumerate())
print('Result: ', num)
对于实验1,100个线程几乎同时运行函数,但肯定是有先后的,但不管谁先谁后,不管是谁只要运行一次num -= 1,num就会减小1.最终结果为0.
对于实验2,100个线程同时竞争运行函数,100个线程启动的时间很快,相差时间估计小于0.0001秒,大家都在很短的时间内获得num=100,都交给各自的临时变量temp,然后几乎同时进入睡眠(1s的时间)。睡眠时间较长,在这个时间里,所有线程均已获得num的值,而且num并没有被任何一个线程更改。
第一个线程竞争到的肯定率先醒来速度极快计算完,num = temp - 1=99,线程2醒来时仍然从上面携带的100同样计算num=99,…,大家算的结果都是99。
对于实验3,各线程的睡眠时间变得很短,当线程1醒来时,for循环还未执行完毕,即线程1开始修改num时还有其他线程刚刚启动,此时其他线程首次获得的num值已经被线程1修改为99.而num不断有线程修改,而且不断有线程要初次获得这个值。此时,0.0001秒内究竟有几个线程在睡眠,又有几个线程在修改,每次都不确定,结果呈现了很大的不确定性。这就是多线程同时修改数据导致的不一致性。
为此,需要对访问、修改该数据的过程进行上锁。即同一时间只能有一个线程进行这一段的操作。大家都来了的话,就要排队。这就是实验4.“上锁的作用是这个线程未结束其他线程无法竞争,只能等,使这一段成为一个串行,运行时间为0.0001s*100次。”那么多线程变为一个串行后,还有用吗?当然,因为串行仅在这一段,其他部分(time.sleep(5))仍然是并行的。如果不采用多线程,则执行这100次功能,就需要(0.0001s+5s)100,。而多线程的总时间约为0.0001s100+5s。
综上,采用多线程执行同一函数,通过lock的方式可以避免对共享数据修改造成的混乱。经过以上这些内容,我们可以轻松的通过创建多线程的方式,加快程序的运行时间:通过类继承创建新线程,新线程中的run来执行同一或不同函数,并向函数传递各自的实参,对于共享数据采用global的方式及lock,实现对共享数据的修改。下面是一个综合实例:
import time
import threading
global num # 在每个线程中都获取这个全局变量,每个线程都要用这个数据并可能对其修改
num = 100 # 设定一个共享变量的初值
def addNum(name, change_num, delay):
global num # 在每个线程中都获取这个全局变量,每个线程都要用这个数据并可能对其修改
lock.acquire() # '''上锁
temp = num
time.sleep(0.0001)
num = temp - change_num
lock.release() # '''释放这把锁,
print("The current thread is " + name) # 这一段不再需要上锁,因为这不是共同内容,这是各线程自己的事
print("The current number is" + str(num)) # 这一段不再需要上锁,因为这不是共同内容,这是各线程自己的事
time.sleep(delay) # 这一段不再需要上锁,因为这不是共同内容,这是各线程自己的事
class MyThread(threading.Thread): # 线程类继承式创建
def __init__(self, name, change_num, delay):
threading.Thread.__init__(self)
self.name = name
self.change_num = change_num
self.delay = delay
def run(self): # 调用同一个函数,并赋予各自的实参
addNum(self.name, self.change_num, self.delay)
thread_list = []
lock = threading.Lock() # 创建一把锁
for i in range(100): # 创建100个线程,同时运行
if (i % 2 == 0):
a = 1
b = 4
else:
a = 0
b = 5 # 所有线程并发,系统总的耗时大体取决于delay最大者。
t = MyThread(str(i), a, b) # 创建线程,每个线程可以同时调用一个函数,并且向函数传递各自的实参
t.start() # 线程启动,随即运行run
print(threading.enumerate()) # 看一下当下的线程有哪些(观察后台的情况)
thread_list.append(t)
for t in thread_list: # 主线程要等待所有线程执行完毕,方可结束
t.join()
print('Result: ', num) # 由于仅偶数线程修改num值,故最终结果为50.
(2)上锁、开锁使得数据免于混乱,各线程对于数据的操作有了一个排队的秩序。其实锁这个概念很形象,大家都要访问一个房间,必须排队、上锁、开锁,而不能一拥而上。但是有时候上锁这个手段缺可能导致死锁。见下述代码:
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()
首先,线程1启动执行run,经过fun1后进入fun2,获得B锁开始睡眠,其他线程启动执行run,进入fun1获得A锁等待B锁,线程1醒来后等待A锁。这样便形成一个死锁环,线程1拥有B需要等A,其他线程拥有A需要等B。这个概念其实和动力学仿真中的代数环雷同。究其原因,是因为数据访问过程存在一个外环(房间的外间)和一个内环(内间)的结构,有人进入了外间在等内间的锁开,持有内间锁的人却在屋外等待外间开锁。
解决方法为采用可重入锁,RLock。“这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:”
import threading
import time
Rlock = threading.RLock() # 创建一个递归锁
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
self.func1()
self.func2()
def func1(self):
Rlock.acquire() # 如果锁被占用,则阻塞在这里,等待锁的释放 counter = 1
print('I am %s ,get res: %s --- %s ' % (self.name, 'ResA', time.time()))
Rlock.acquire() # counter = 2
print('I am %s ,get res: %s --- %s ' % (self.name, 'ResB', time.time()))
Rlock.release() # counter = 1
Rlock.release() # counter = 0
def func2(self):
Rlock.acquire() # counter = 1
print('I am %s ,get res: %s --- %s ' % (self.name, 'ResB', time.time()))
time.sleep(0.2)
Rlock.acquire() # counter = 2
print('I am %s ,get res: %s --- %s ' % (self.name, 'ResA', time.time()))
Rlock.release() # counter = 1 #释放这把锁
Rlock.release() # counter = 0
if __name__ == '__main__':
print('start ----------- %s' % time.time())
for i in range(0, 10):
mt = MyThread()
mt.start()
其实RLock就是一种锁中锁,或者说是一把锁,这把锁可以提供子锁,counter记录了这种锁中锁的层数。各线程要么获得这把锁中锁,要么等待,即外间和内间的锁同一时间只能被一人所有。这样就避免了死锁。