前言:
一旦接触到多线程编程,那么线程之间的同步就显得非常重要了。c++/c#/java等高级语言都有自己的线程库,当然也提供了线程同步的API接口。打个比方,在C++/QT中,线程的同步有以下几种方式:互斥锁、信号量、条件变量、读写锁等。基于不同语言的库所提供的线程同步的接口大同小异。总体来说,只要掌握了线程同步的基本思想,那么在每种语言中都可以使用的起来,不同的只是接口上的差异(例如命名、参数、返回值等等)
1. python中的线程同步
如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。
# 我们来看这样一个demo, 假如我们有100张火车票需要出售, 有2个窗口同时出售(相当于2个线程),
# 无论如何, 两个窗口都不可能出售票号相同的一张票.
import threading
g_tickets = 100
def fun1():
while True:
global g_tickets
if g_tickets > 0:
print('thread_1 sell ticket: ', g_tickets)
g_tickets -= 1
def fun2():
while True:
global g_tickets
if g_tickets > 0:
print('thread_2 sell ticket:', g_tickets)
g_tickets -= 1
t1 = threading.Thread(target=fun1, args=())
t2 = threading.Thread(target=fun2, args=())
t1.start()
t2.start()
t1.join()
t2.join()
通过程序运行结果发现, 票号为96的这张票被2个线程分别出售了一次
,如果是在真实的火车售票系统中出现这种问题,那麻烦就大了。
那为啥会有这种问题出现?
分析:
这是由于在多CPU的平台下,线程1和线程2在被创建后可以并发运行,全局变量g_tickets被卖到96号票的时候,此时线程1正在售卖,然后线程1还没来得及对g_tickets进行自减
,就切换到线程2了,很明显,此时线程2也对96号票进行了售卖
,于是造成了2个线程分别对96号票进行了售卖的这样情况
。上述问题出现的主要原因是两个线程同时访问了同一个全局变量:g_tickets
。为了避免这种问题的发生,就要求在多个 线程之间进行一个同步处理,保证在一个线程访问共享资源时,其他线程不能访问该共享资源
。对于本例来说,就是当一个线程在销售火车票的时候,其他线程在该段时间内不能访问同一种资源,本例就是全局变量g_tickets,必须等到前者完成火车票的售卖过程之后,其他线程才能访问该资源。这与我们上厕所一样,假如3个人共用一个厕所,其中有一个人在使用时,其他人必须依次排队等候,这样就不会造成冲突了,否则非打起来不可。
程序改进:
有了以上的分析之后,我们知道为了避免此种情况发生,必须进行线程同步。
为此我们引入 threading.Lock()函数,通过该lock()函数可以获取锁。
然后通过acquire()进行上锁,通过release()来释放锁。
将需要保护的代码放到acquire()...release()之间,就可以实现线程同步的效果
。
import threading
g_tickets = 100
def fun1():
while True:
lock.acquire() # 加锁
global g_tickets
if g_tickets > 0:
print('thread_1 sell ticket: ', g_tickets)
g_tickets -= 1
lock.release() # 释放锁
def fun2():
while True:
lock.acquire() # 加锁
global g_tickets
if g_tickets > 0:
print('thread_2 sell ticket: ', g_tickets)
g_tickets -= 1
lock.release() # 释放锁
lock = threading.Lock() #获取锁
t1 = threading.Thread(target=fun1, args=())
t2 = threading.Thread(target=fun2, args=())
t1.start()
t2.start()
t1.join()
t2.join()
此时,我们再来对96号票分析一下
。
假如此时线程1正在贩卖96号票,那么很明显,此时线程1对这个锁进行了上锁操作,并且正在执行售卖过程,但是还未自减;此时突然线程调度,切换到了线程2,线程2也想进行上锁操作,要想进行上锁操作,那必须先拿取到锁才行
,但是很明显,此时的锁被线程1给上锁了(占有了),还没有被线程1所释放,那么线程2只能苦苦的等待拿锁,阻塞在lock.acquire()
处等待线程1对锁进行释放,然后它可能有机会拿取到锁(不一定拿取到)
,然后进行上锁操作,进行售卖过程。这样一来,共享资源g_tickets就在线程1 和线程2之间实现了同步,也就是同一时间只能有一个线程操作它
。这样就不会出现之前的问题了。