Python中的多线程指南|S2(同步)

2021年3月17日15:24:01 发表评论 607 次浏览

本文讨论了线程同步的概念。多线程用Python编程语言编写。

线程之间的同步

线程同步被定义为一种机制, 可确保两个或多个并发线程不会同时执行某些特定的程序段, 即关键部分.

关键部分是指程序中访问共享资源的部分。

例如, 在下图中, 3个线程尝试同时访问共享资源或关键部分。

Python中的多线程|S2(同步)1

并发访问共享资源可能导致比赛条件.

当两个或多个线程可以访问共享数据并且它们试图同时更改它们时, 就会发生竞争状态。结果, 变量的值可能是不可预测的, 并且取决于过程的上下文切换的时间而变化。

考虑下面的程序以了解竞争条件的概念:

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

在上面的程序中:

  • 两个线程t1和t2创建于主要任务函数和全局变量X设置为0。
  • 每个线程都有一个目标函数thread_task在其中增量函数被称为100000次。
  • 增量函数将增加全局变量X在每个通话中减1。

的预期最终价值X是200000但我们在10次迭代中得到的主要任务函数是一些不同的值。

这是由于线程同时访问共享变量而发生的X。价值的这种不可预测性X只不过是比赛条件.

下图显示了如何

比赛条件

发生在以上程序中:

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

请注意,

X

上图中的是12, 但由于竞赛条件, 结果是11!

因此, 我们需要一个工具来在多个线程之间进行适当的同步。

使用锁

穿线模块提供了锁上课以应对比赛条件。锁是使用信号操作系统提供的对象。

信号量是一个同步对象, 它控制并行编程环境中多个进程/线程对公共资源的访问。它只是操作系统(或内核)存储中指定位置的值, 每个进程/线程都可以检查然后更改。取决于找到的值, 进程/线程可以使用该资源, 或者将发现该资源已在使用中, 并且必须等待一段时间才能重试。信号量可以是二进制的(0或1), 也可以具有其他值。通常, 使用信号量的进程/线程会检查该值, 然后(如果它使用了资源)则更改该值以反映该值, 以便后续的信号量用户将知道等待。

锁该类提供以下方法:

  • 获取([阻止]):获取锁。锁可以是锁定的也可以是非锁定的。
    • 在将阻塞参数设置为的情况下调用true(默认设置), 直到锁解锁后, 线程执行才会被阻止, 然后将锁设置为已锁定并返回true.
    • 在将阻塞参数设置为的情况下调用false, 不会阻止线程执行。如果锁定已解锁, 则将其设置为锁定并返回true否则返回false立即。
  • 发布() :释放锁。
    • 锁锁定后, 将其重置为解锁状态, 然后返回。如果其他任何线程被阻塞, 等待锁解锁, 则允许其中一个线程继续进行。
    • 如果锁已解锁, 则线程错误被提出。

考虑下面给出的示例:

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 = 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中的多线程|S2(同步)3

这使我们到本教程系列的结尾

Python中的多线程

.

最后, 这里是多线程的一些优点和缺点:

优点:

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

缺点:

  • 随着线程数量的增加, 复杂性也随之增加。
  • 共享资源(对象, 数据)的同步是必要的。
  • 调试困难, 结果有时无法预测。
  • 可能导致饥饿的潜在死锁, 即某些线程可能设计不佳
  • 构造和同步线程需要占用大量CPU /内存。

如果发现任何不正确的地方, 或者想分享有关上述主题的更多信息, 请写评论。

注意怪胎!巩固你的基础Python编程基础课程和学习基础知识。

首先, 你的面试准备可通过以下方式增强你的数据结构概念:Python DS课程。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: