📜  Python中的多线程 |第 2 组(同步)

📅  最后修改于: 2022-05-13 01:55:00.527000             🧑  作者: Mango

Python中的多线程 |第 2 组(同步)

本文讨论了Python编程语言中多线程情况下线程同步的概念。

线程之间的同步

线程同步被定义为一种机制,它确保两个或多个并发线程不会同时执行某些称为临界区的特定程序段。

例如,在下图中,3 个线程尝试同时访问共享资源或临界区。

对共享资源的并发访问会导致竞争条件

考虑下面的程序来理解竞争条件的概念:

import threading
  
# global variable x
x = 0
  
def increment():
    """
    function to increment global variable x
    """
    global x
    x += 1
  
def thread_task():
    """
    task for thread
    calls increment function 100000 times.
    """
    for _ in range(100000):
        increment()
  
def main_task():
    global x
    # setting global variable x as 0
    x = 0
  
    # creating threads
    t1 = threading.Thread(target=thread_task)
    t2 = threading.Thread(target=thread_task)
  
    # start threads
    t1.start()
    t2.start()
  
    # wait until threads finish their job
    t1.join()
    t2.join()
  
if __name__ == "__main__":
    for i in range(10):
        main_task()
        print("Iteration {0}: x = {1}".format(i,x))

输出:

Iteration 0: x = 175005
Iteration 1: x = 200000
Iteration 2: x = 200000
Iteration 3: x = 169432
Iteration 4: x = 153316
Iteration 5: x = 200000
Iteration 6: x = 167322
Iteration 7: x = 200000
Iteration 8: x = 169917
Iteration 9: x = 153589

在上面的程序中:

  • main_task函数中创建了两个线程t1t2 ,并将全局变量x设置为 0。
  • 每个线程都有一个目标函数thread_task ,其中增量函数被调用100000次。
  • increment函数将在每次调用中将全局变量x增加 1。

x的预期最终值为 200000,但我们在main_task函数的 10 次迭代中得到一些不同的值。

这是由于线程对共享变量x的并发访问而发生的。 x值的这种不可预测性只不过是竞争条件

下图显示了上述程序中竞争条件如何发生:

请注意,上图中x的期望值为 12,但由于竞争条件,结果为 11!因此,我们需要一个工具来在多个线程之间进行适当的同步。

使用锁

threading模块提供了一个Lock类来处理竞争条件。锁定是使用操作系统提供的信号量对象实现的。

Lock类提供以下方法:

  • 获取([阻塞]):获取锁。锁可以是阻塞的或非阻塞的。
    • 当阻塞参数设置为True (默认值)调用时,线程执行被阻塞,直到锁被解锁,然后 lock 设置为锁定并返回True
    • 当阻塞参数设置为False时,线程执行不会被阻塞。如果锁被解锁,则将其设置为已锁定并返回True否则立即返回False
  • release() :释放锁。
    • 当锁被锁定时,将其重置为解锁,然后返回。如果任何其他线程被阻塞等待锁被解锁,则只允许其中一个线程继续。
    • 如果锁已解锁,则会引发ThreadError

考虑下面给出的示例:

import threading
  
# global variable x
x = 0
  
def increment():
    """
    function to increment global variable x
    """
    global x
    x += 1
  
def thread_task(lock):
    """
    task for thread
    calls increment function 100000 times.
    """
    for _ in range(100000):
        lock.acquire()
        increment()
        lock.release()
  
def main_task():
    global x
    # setting global variable x as 0
    x = 0
  
    # creating a lock
    lock = threading.Lock()
  
    # creating threads
    t1 = threading.Thread(target=thread_task, args=(lock,))
    t2 = threading.Thread(target=thread_task, args=(lock,))
  
    # start threads
    t1.start()
    t2.start()
  
    # wait until threads finish their job
    t1.join()
    t2.join()
  
if __name__ == "__main__":
    for i in range(10):
        main_task()
        print("Iteration {0}: x = {1}".format(i,x))

输出:

Iteration 0: x = 200000
Iteration 1: x = 200000
Iteration 2: x = 200000
Iteration 3: x = 200000
Iteration 4: x = 200000
Iteration 5: x = 200000
Iteration 6: x = 200000
Iteration 7: x = 200000
Iteration 8: x = 200000
Iteration 9: x = 200000

让我们试着一步一步理解上面的代码:

  • 首先,使用以下命令创建一个Lock对象:
    lock = threading.Lock()
    
  • 然后,锁定作为目标函数参数传递:
    t1 = threading.Thread(target=thread_task, args=(lock,))
      t2 = threading.Thread(target=thread_task, args=(lock,))
    
  • 在目标函数的关键部分,我们使用lock.acquire()方法应用锁。一旦获得锁,在使用lock.release()方法释放锁之前,没有其他线程可以访问临界区(这里是增量函数)。
    lock.acquire()
      increment()
      lock.release()
    

    正如您在结果中看到的,每次x的最终值都是 200000(这是预期的最终结果)。

这是下面给出的图表,描述了上述程序中锁的实现:

这将我们带到Python中的多线程系列教程的结尾。
最后,这里有几个多线程的优点和缺点:

好处:

  • 它不会阻止用户。这是因为线程是相互独立的。
  • 由于线程并行执行任务,因此可以更好地利用系统资源。
  • 增强了多处理器机器上的性能。
  • 多线程服务器和交互式 GUI 专门使用多线程。

缺点:

  • 随着线程数量的增加,复杂性也会增加。
  • 共享资源(对象、数据)的同步是必要的。
  • 调试困难,结果有时无法预测。
  • 导致饥饿的潜在死锁,即某些线程可能无法以糟糕的设计提供服务
  • 构造和同步线程是 CPU/内存密集型的。