同步线程(Synchronizing Threads)

优质
小牛编辑
129浏览
2023-12-01

线程同步可以被定义为一种方法,借助于该方法,我们可以确保两个或更多并发线程不同时访问称为临界区的程序段。 另一方面,正如我们所知,临界区是访问共享资源的程序的一部分。 因此,我们可以说同步是通过同时访问资源来确保两个或多个线程不相互连接的过程。 下图显示了四个线程同时尝试访问程序的关键部分。

同步

为了更清楚,假设有两个或更多线程试图同时在列表中添加对象。 此行为无法导致成功结束,因为它将丢弃一个或所有对象,否则将完全破坏列表的状态。 这里同步的作用是一次只有一个线程可以访问列表。

线程同步中的问题

在实现并发编程或应用同步原语时,我们可能会遇到问题。 在本节中,我们将讨论两个主要问题。 问题是 -

  • Deadlock
  • Race condition

比赛条件

这是并发编程中的主要问题之一。 对共享资源的并发访问可能导致竞争条件。 竞争条件可以定义为当两个或多个线程可以访问共享数据然后尝试同时更改其值时发生的条件。 因此,变量的值可能是不可预测的,并且取决于过程的上下文切换的定时而变化。

例子 (Example)

考虑这个例子来理解竞争条件的概念 -

Step 1 - 在这一步中,我们需要导入线程模块 -

import threading

Step 2 - 现在,定义一个全局变量,比如x,以及它的值为0 -

x = 0

Step 3 - 现在,我们需要定义increment_global()函数,它将在此全局函数x中增加1 -

def increment_global():
   global x
   x += 1

Step 4 - 在这一步中,我们将定义taskofThread()函数,该函数将调用increment_global()函数指定的次数; 对于我们的例子它是50000次 -

def taskofThread():
   for _ in range(50000):
      increment_global()

Step 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()

Step 6 - 现在,我们需要给出我们想要调用main()函数的迭代次数。 在这里,我们称它为5次。

if __name__ == "__main__":
   for i in range(5):
      main()
      print("x = {1} after Iteration {0}".format(i,x))

在下面显示的输出中,我们可以看到竞争条件的影响,因为每次迭代后x的值预计为100000.但是,值有很多变化。 这是由于线程并发访问共享全局变量x。

输出 (Output)

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() method

该方法用于获取,即阻止锁定。 锁定可以是阻塞或非阻塞,具体取决于以下真值或假值 -

  • With value set to True - 如果使用True(默认参数)调用acquire()方法,则会阻止线程执行,直到解锁为止。

  • With value set to False - 如果使用False调用acquire()方法(这不是默认参数),则在将其设置为true(即直到锁定)之前,不会阻止线程执行。

release() method

此方法用于释放锁定。 以下是与此方法相关的一些重要任务 -

  • 如果锁被锁定,那么release()方法将解锁它。 它的工作是,如果多个线程被阻塞并等待锁被解锁,则只允许一个线程继续进行。

  • 如果锁已经解锁,它将引发ThreadError

现在,我们可以使用lock类及其方法重写上述程序,以避免竞争条件。 我们需要使用lock参数定义taskofThread()方法,然后需要使用acquire()和release()方法来阻止和非阻塞锁以避免竞争条件。

例子 (Example)

以下是了解用于处理竞争条件的锁概念的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,这符合该程序的期望。

输出 (Output)

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程序解决方案

通过将哲学家分为两类 - greedy philosophersgenerous philosophers可以找到这个问题的解决方案。 主要是一个贪婪的哲学家将尝试拿起左叉并等到它在那里。 然后他会等待正确的叉子在那里,捡起来吃,然后把它放下。 另一方面,一个慷慨的哲学家会试图拿起左叉,如果不存在,他会等一段时间再试一次。 如果他们得到左叉,那么他们会尝试找到合适的叉子。 如果他们也会获得正确的叉子,那么他们就会吃掉并释放两个叉子。 但是,如果他们不能获得正确的分叉,那么他们将释放左分叉。

例子 (Example)

以下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()方法。 我们可以在以下输出中看到解决方案 -

输出 (Output)

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.