有没有在生活中思考或者见别人说过这个问题,在银行自助取款机取钱的时候,如果同时按下取一百元和手机微信支付一百元(假定银行卡中只有一百元),那样会不会既支付成功又能取出钱呢,答案当然是不可能,这就涉及到在多线程的环境下访问同一资源的问题,这样会引发线程不安全的可能,下面我们来学习一下问题的根源和解决方式。
1. 临界资源问题
首先我们来了解一下什么是临界资源,多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。
还有一个名词叫临界区,每个进程中访问临界资源的那段代码称为临界区。显然,若能保证诸进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。
我们通过下面的例子来了解一下临界资源的共用:
12345678910111213141516171819202122232425262728293031323334 | import time import threading class Apple: def __init__( self ): self .apple_number = 6 #定义6个苹果,每个苹果有一个标号 def get_apple_number( self ): return self .apple_number def sell_apple( self ): time.sleep( 2 ) #当前线程休眠,会阻塞当前的线程,这个时间提供给用户付款,完成付款后执行售出操作 print ( '第%d个苹果已卖' % self .apple_number) self .apple_number - = 1 apple = Apple() def thread_one(): global apple while True : query_apple_number = ap.get_apple_number() #查询是否还有苹果 if query_apple_number > 0 : #苹果数量大于0就执行一次出售操作 ap.sell_apple() else : break def thread_two(): global apple while True : query_apple_number = ap.get_apple_number() if query_apple_number > 0 : ap.sell_apple() else : break if __name__ = = '__main__' : apple_one = threading.Thread(target = thread_one()) apple_one.start() apple_one.join() apple_two = threading.Thread(target = thread_two()) apple_two.start() |
输出结果为:
123456 | 第 6 个苹果已卖 第 5 个苹果已卖 第 4 个苹果已卖 第 3 个苹果已卖 第 2 个苹果已卖 第 1 个苹果已卖 |
我们前面定义了一个类,类中存在着查询苹果和售出苹果的方法,然后调用这个类来创建一个对象,然后创建2个线程体来执行买苹果操作,最后在主程序中创建2个线程去执行指令,通过输出结果我们可以看出,两个线程共用了临界资源,我们在输出结果的时候可能会出现不一致,这就源于多个线程共享数据的缘故,因此多线程对临界资源的使用可能会导致数据出现误差。
2. 多线程同步
上面我们说到多个线程使用同一资源的时候可能会引发数据的不一致,所以我们要引入一种互斥机制来解决这个问题这种互斥机制帮助我们在任一时刻只能由一个线程访问同一资源,即使后面排队的线程出现了堵塞,锁定机制仍然不会被解除,其余线程仍然无法进行资源的访问。
举个例子来说,当多个人排队去书店看同一本书的时候,第一个人拿到书之后,该书就到了第一个人的手上,此时我们就给这本书加了一个虚拟的锁,其他读者只有等待,直到第一个人结束阅读。在Python总我们使用threading模块中的Lock类来给线程加锁,Lock的对象有两种状态,默认是未锁定,还有一种锁定,可以通过acquire()方法锁定和release()方法解锁。
操作系统中有一道很经典的读者和写者问题,多线程进行可以进行同时读,允许多个读者进行阅读,但是只允许一个写者在写作,写者进行时禁止阅读。
看下面的代码:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 | import time import threading from threading import Semaphore import random writerminute = Semaphore( 1 ) # 添加计数器 readerminute = Semaphore( 1 ) # 添加计数器 readercount = 0 sleept = 1 def reader(i): print ( '读者' + str (i) + ' 等待阅读\n' , end = '') readerminute.acquire() # 计数器+1 global readercount if readercount = = 0 : writerminute.acquire() # 计数器+1 readercount + = 1 # 阅读人数+1 readerminute.release() # 计数器-1 print ( '读者' + str (i) + ' 正在阅读\n' , end = '') time.sleep(sleept) print ( '读者' + str (i) + ' 结束阅读\n' , end = '') readerminute.acquire() readercount - = 1 # 读完-1 if readercount = = 0 : writerminute.release() readerminute.release() def writer(i): print ( '写者' + str (i) + ' 等待去写\n' , end = '') writerminute.acquire() # +1 print ( '写者' + str (i) + ' 正在写\n' , end = '') time.sleep(sleept) # 读 print ( '写者' + str (i) + ' 完成写作\n' , end = '') writerminute.release() # 结束-1 if __name__ = = '__main__' : list = [] for i in range ( 8 ): list .append(random.randint( 0 , 1 )) print ( list ) # 创建了一个人数为8的列表,1为读者,0为写者。 # 首位优先进行阅读或写作,后续等待。 rindex = 1 windex = 1 for i in list : if i = = 0 : t = threading.Thread(target = reader, args = (rindex,)) rindex + = 1 t.start() else : t = threading.Thread(target = writer, args = (windex,)) windex + = 1 t.start() |
运行结果如下:
12345678910111213141516171819202122232425 | [ 1 , 0 , 0 , 1 , 1 , 1 , 0 , 0 ] 写者 1 等待去写 写者 1 正在写 读者 1 等待阅读 读者 2 等待阅读 写者 2 等待去写 写者 3 等待去写 写者 4 等待去写 读者 3 等待阅读 读者 4 等待阅读 写者 1 完成写作 读者 1 正在阅读 读者 2 正在阅读 读者 3 正在阅读 读者 4 正在阅读 读者 2 结束阅读 读者 3 结束阅读 读者 1 结束阅读 读者 4 结束阅读 写者 2 正在写 写者 2 完成写作 写者 3 正在写 写者 3 完成写作 写者 4 正在写 写者 4 完成写作 |
在这个问题上,我们首先对读者函数进行理解,首先当第一个线程开始的时候,读者开始阅读,进入一个函数判断,如果当前没有读者在读,首先把写者计数器给锁定, 然后进入阅读,读完之后判断是否还有读者在读,如果没有人在读,就解开锁定。然后 是写者函数,最后在主程序中,我们通过随机数产生八个数字,把1看成读者,0看成 写者,第一个进入队列的首先进行操作,后续的全部排队,如果第一个是写者,那么后 面的所有人都不能进行操作,只有等待,如果第一个是读者,那么后面的人先进行等待, 然后是读者优先进行阅读。
3. 总结
本节的内容理解起来比较抽象,结合着操作系统中的相关理念能更好的进行理解,读者和写者是一个比较经典的问题,还有哲学家进餐、一家人吃水果等问题都是线程同步的相关内容,多线程同步的时候使用Lock对象能保证线程同步的时候信息是安全的,也就是多个线程使用同一资源的时候,会确保资源的正确使用。