同步线程
线程同步可以定义为一种方法,借助该方法,我们可以确保两个或多个并发线程不会同时访问称为关键部分的程序段。另一方面,我们知道关键部分是访问共享资源的程序部分。因此,我们可以说同步是确保两个或多个线程不会通过同时访问资源而相互交互的过程。下图显示了四个线程试图同时访问程序的关键部分。
为了更清楚起见,假设两个或多个线程试图同时将对象添加到列表中。此操作无法成功结束,因为它要么会丢弃一个或所有对象,要么会完全破坏列表的状态。这里同步的作用是每次只有一个线程可以访问列表。
线程同步中的问题
我们在实现并发编程或应用同步原语时可能会遇到问题。在本节中,我们将讨论两个主要问题。问题是 −
- 死锁
- 竞争条件
竞争条件
这是并发编程中的主要问题之一。同时访问共享资源可能导致竞争条件。竞争条件可以定义为当两个或多个线程可以访问共享数据,然后尝试同时更改其值时发生的条件。因此,变量的值可能无法预测,并且会根据进程上下文切换的时间而变化。
示例
考虑此示例以了解竞争条件的概念 −
步骤 1 − 在此步骤中,我们需要导入线程模块 −
import threading
步骤 2 −现在,定义一个全局变量,比如 x,并将其值设为 0 −
x = 0
步骤 3 − 现在,我们需要定义 increment_global() 函数,该函数将在此全局函数 x 中执行 1 的递增 −
def increment_global(): global x x += 1
步骤 4 − 在此步骤中,我们将定义 taskofThread() 函数,该函数将调用increment_global() 函数指定次数;在我们的示例中,该次数为 50000 次 −
def taskofThread(): for _ in range(50000): increment_global()
步骤 5 − 现在,定义 main() 函数,在其中创建线程 t1 和 t2。这两个线程将在 start() 函数的帮助下启动,并等待它们在 join() 函数的帮助下完成其工作。
def main(): global x x = 0 t1 = threading.Thread(target= taskofThread) t2 = threading.Thread(target= taskofThread) t1.start() t2.start() t1.join() t2.join()
步骤 6 − 现在,我们需要给出要调用 main() 函数的迭代次数范围。这里,我们调用了 5 次。
if __name__ == "__main__": for i in range(5): main() print("x = {1} after Iteration {0}".format(i,x))
在下面的输出中,我们可以看到竞争条件的影响,因为每次迭代后 x 的值预计为 100000。但是,该值存在很大变化。这是由于线程对共享全局变量 x 的并发访问造成的。
输出
x = 100000 after Iteration 0 x = 54034 after Iteration 1 x = 80230 after Iteration 2 x = 93602 after Iteration 3 x = 93289 after Iteration 4
使用锁处理竞争条件
正如我们在上面的程序中看到的竞争条件的影响,我们需要一个同步工具,它可以处理多个线程之间的竞争条件。在 Python 中,<threading> 模块提供了 Lock 类来处理竞争条件。此外,Lock 类提供了不同的方法,借助这些方法,我们可以处理多个线程之间的竞争条件。这些方法如下所述 −
acquire() 方法
此方法用于获取,即阻塞锁。锁可以是阻塞的,也可以是非阻塞的,具体取决于以下 true 或 false 值 −
将值设置为 True −如果使用 True(默认参数)调用 acquire() 方法,则线程执行将一直被阻止,直到锁定被解锁。
将值设置为 False − 如果使用 False(非默认参数)调用 acquire() 方法,则线程执行将一直被阻止,直到将其设置为 true,即直到锁定。
release() 方法
此方法用于释放锁。以下是与此方法相关的一些重要任务 −
如果锁被锁定,则 release() 方法会将其解锁。它的工作是,如果多个线程被阻止并等待锁解锁,则只允许一个线程继续运行。
如果锁已解锁,则会引发 ThreadError。
现在,我们可以用 lock 类及其方法重写上述程序以避免竞争条件。我们需要使用 lock 参数定义 taskofThread() 方法,然后需要使用 acquire() 和 release() 方法来阻止和非阻止锁,以避免竞争条件。
示例
以下是 Python 程序示例,用于了解处理竞争条件的锁的概念 −
import threading x = 0 def increment_global(): global x x += 1 def taskofThread(lock): for _ in range(50000): lock.acquire() increment_global() lock.release() def main(): global x x = 0 lock = threading.Lock() t1 = threading.Thread(target = taskofThread, args = (lock,)) t2 = threading.Thread(target = taskofThread, args = (lock,)) t1.start() t2.start() t1.join() t2.join() if __name__ == "__main__": for i in range(5): main() print("x = {1} after Iteration {0}".format(i,x))
以下输出显示竞争条件的影响被忽略;因为 x 的值在每次迭代后现在都是 100000,这符合该程序的预期。
输出
x = 100000 after Iteration 0 x = 100000 after Iteration 1 x = 100000 after Iteration 2 x = 100000 after Iteration 3 x = 100000 after Iteration 4
死锁 − 哲学家就餐问题
死锁是设计并发系统时可能遇到的一个麻烦问题。我们可以借助哲学家就餐问题来说明这个问题,如下所示 −
Edsger Dijkstra 最初提出了哲学家就餐问题,这是并发系统最大问题之一死锁的著名例证之一。
在这个问题中,有五位著名的哲学家坐在圆桌旁,从碗里吃东西。五位哲学家可以使用五把叉子吃饭。然而,哲学家们决定同时使用两把叉子吃饭。
现在,哲学家有两个主要条件。首先,每个哲学家要么处于进食状态,要么处于思考状态;其次,他们必须先拿到两把叉子,即左叉和右叉。当五位哲学家同时拿到左叉时,问题就出现了。现在他们都在等待右叉空闲出来,但他们永远不会放弃叉子,直到他们吃完食物,而右叉永远也拿不到。因此,餐桌上就会出现死锁状态。
并发系统中的死锁
现在,如果我们看到,同样的问题也可能出现在我们的并发系统中。上述示例中的叉子将是系统资源,每个哲学家都可以代表竞争获取资源的过程。
使用 Python 程序的解决方案
可以通过将哲学家分为两种类型来找到此问题的解决方案 - 贪婪的哲学家和慷慨的哲学家。贪婪的哲学家主要会尝试拿起左边的叉子并等待,直到它在那里。然后,他会等待右边的叉子在那里,拿起它,吃掉然后放下。另一方面,慷慨的哲学家会尝试拿起左边的叉子,如果叉子不在那里,他会等待一段时间后再试一次。如果他们拿到了左边的叉子,那么他们会尝试拿右边的叉子。如果他们也拿到了右边的叉子,那么他们会吃掉并释放两个叉子。但是,如果他们拿不到右边的叉子,他们就会放开左边的叉子。
示例
以下 Python 程序将帮助我们找到哲学家就餐问题的解决方案 −
import threading import random import time class DiningPhilosopher(threading.Thread): running = True def __init__(self, xname, Leftfork, Rightfork): threading.Thread.__init__(self) self.name = xname self.Leftfork = Leftfork self.Rightfork = Rightfork def run(self): while(self.running): time.sleep( random.uniform(3,13)) print ('%s is hungry.' % self.name) self.dine() def dine(self): fork1, fork2 = self.Leftfork, self.Rightfork while self.running: fork1.acquire(True) locked = fork2.acquire(False) if locked: break fork1.release() print ('%s swaps forks' % self.name) fork1, fork2 = fork2, fork1 else: return self.dining() fork2.release() fork1.release() def dining(self): print ('%s starts eating '% self.name) time.sleep(random.uniform(1,10)) print ('%s finishes eating and now thinking.' % self.name) def Dining_Philosophers(): forks = [threading.Lock() for n in range(5)] philosopherNames = ('1st','2nd','3rd','4th', '5th') philosophers= [DiningPhilosopher(philosopherNames[i], forks[i%5], forks[(i+1)%5]) \ for i in range(5)] random.seed() DiningPhilosopher.running = True for p in philosophers: p.start() time.sleep(30) DiningPhilosopher.running = False print (" It is finishing.") Dining_Philosophers()
上述程序使用了贪婪哲学家和慷慨哲学家的概念。该程序还使用了 <threading> 模块的 Lock 类的 acquire() 和 release() 方法。我们可以在以下输出中看到解决方案 −
输出
4th is hungry. 4th starts eating 1st is hungry. 1st starts eating 2nd is hungry. 5th is hungry. 3rd is hungry. 1st finishes eating and now thinking.3rd swaps forks 2nd starts eating 4th finishes eating and now thinking. 3rd swaps forks5th starts eating 5th finishes eating and now thinking. 4th is hungry. 4th starts eating 2nd finishes eating and now thinking. 3rd swaps forks 1st is hungry. 1st starts eating 4th finishes eating and now thinking. 3rd starts eating 5th is hungry. 5th swaps forks 1st finishes eating and now thinking. 5th starts eating 2nd is hungry. 2nd swaps forks 4th is hungry. 5th finishes eating and now thinking. 3rd finishes eating and now thinking. 2nd starts eating 4th starts eating It is finishing.