首页 > 编程语言 >python并发与并行(三) ———— 利用Lock防止多个线程争用同一份数据

python并发与并行(三) ———— 利用Lock防止多个线程争用同一份数据

时间:2024-08-31 14:24:34浏览次数:12  
标签:count thread python Lock self counter how 线程


了解到全局解释器锁(GIL)的效果之后,许多Python新手可能觉得没必要继续在代码里使用互斥锁(mutual-exclusion lock,mutex)了。既然GIL让Python线程没办法平行地运行在多个CPU核心上,那是不是就意味着它同时还会自动保护程序里面的数据结构,让我们不需要再加锁了?在列表与字典等结构上面测试过之后,有些人可能真的以为是这样的。

其实并非如此。GIL起不到这样的保护作用。虽说同一时刻只能有一条Python线程在运行,但这条线程所操纵的数据结构还是有可能遭到破坏,因为它在执行完当前这条字节码指令之后,可能会被Python系统切换走,等它稍后切换回来继续执行下一条字节码指令时,当前的数据或许已经与实际值脱节了,因为中途切换进来的其他线程可能更新过这个值。所以,多个线程同时访问同一个对象是很危险的。每条线程在操作这份数据时,都有可能遭到其他线程打扰,因此数据之中的固定关系或许已经被别的线程破坏了,这会令程序陷入混乱状态。

我们用一个程序来模拟传感器采集数据,然后使用多线程来统计最终传感器采集到的值。

这个Python代码示例主要演示了多线程环境下的并发问题,并使用了threading.Barrier来确保所有线程在同一时间点开始执行。代码的核心目的是展示在没有适当同步机制的情况下,多个线程同时更新共享资源(在这个例子中是一个简单的计数器)可能会导致数据不一致。

from threading import Barrier
BARRIER = Barrier(5)
from threading import Thread

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self, offset):
        self.count += offset

def worker(sensor_index, how_many, counter):
   # Barrier类,它允许一组线程在某个点上互相等待,直到所有线程都到达这个点(即达到指定的线程数量),然后它们才可以继续执行。
   # BARRIER.wait()确保所有线程在开始计数之前都已经准备好,这样更容易触发并发问题(因为线程几乎同时开始更新共享资源)。
    BARRIER.wait()
    for _ in range(how_many):
  
        counter.increment(1)

how_many = 10**5
counter = Counter()

threads = []
for i in range(5):
    thread = Thread(target=worker,
                    args=(i, how_many, counter))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

expected = how_many * 5
found = counter.count
print(f'Counter should be {expected}, got {found}')

Output:

Counter should be 500000, got 370684

由于这个示例没有使用任何锁或同步机制来保护对Counter实例的并发访问,因此你很可能会发现实际计数值与期望值不符。这就是所谓的“竞态条件”(race condition),它发生在多个线程同时访问和修改共享数据时。在实际应用中,为了避免这种情况,通常会使用锁(如互斥锁、读写锁等)或其他同步机制(如条件变量、信号量等)来确保数据的一致性和正确性。

Python解释器会保证多个线程可以公平地获得执行机会,或者说,保证每条线程所分配到的执行时间大致相等。为了实现这种效果,它会及时暂停某条线程,并且把另一条线程切换过来执行。然而问题是,我们并不清楚它具体会在什么时候暂停线程,万一这条线程正在执行的是一项本来不应该中断的原子操作(atomic operation),就会造成我们例子中这种错误的结果。

Counter对象的increment方法看上去很简单,工作线程在调用这个方法时,相当于是在执行下面这样一条语句:

counter.count += 1

然而,在对象的属性上面执行+=操作,实际上需要分成三个小的步骤。也就是说,Python解释器会把这一条语句分成三个语句来之行:

value = getattr(counter, 'count')
result = value + 1
setattr(counter, 'count', result)

这三个步骤本来应该一次执行完才对,但是Python系统有可能在任意两步之间,把当前这条线程切换走,这就导致这条线程在切换回来后,看到的是个已经过时的value值,它把这个过时的值通过setattr赋给Counter对象的count属性,从而使统计出来的样本总数偏小。

下面我们模拟一下多线程切换时的状态:

# Running in Thread A
value_a = getattr(counter, 'count')
# Context switch to Thread B
value_b = getattr(counter, 'count')
result_b = value_b + 1
setattr(counter, 'count', result_b)
# Context switch back to Thread A
result_a = value_a + 1
setattr(counter, 'count', result_a)

线程A在执行了第一步之后,还没来得及执行第二步,就被线程B打断了。等到线程B把它的三个步骤执行完毕后,线程A才重新获得执行机会。这时,它并不知道count已经被线程B更新过了,它仍然以为自己在第一步里读取到的那个value_a是正确的,于是线程A就给value_a加1并将结果(也就是result_a)赋给count属性。这实际上把线程B刚刚执行的那一次递增操作覆盖掉了。上面的传感器采样总数之所以出错,也正是这个原因所致。

为了避免数据争用,Python在内置的threading模块里提供了一套健壮的工具。其中最简单也最有用的是一个叫作Lock的类,它相当于互斥锁(mutex)。

通过这样的锁,我们可以确保多条线程有秩序地访问Counter类的count属性,使得该属性不会遭到破坏,因为线程必须先获取到这把锁,然后才能操纵count,而每次最多只能有一条线程获得该锁。下面,用with语句来实现加锁与解锁.

在这个修改后的代码中,我们引入了一个带有锁机制的计数器LockingCounter,以确保在多线程环境下对计数器进行安全地更新。下面是对修改后代码的详细解释:

from threading import Lock
from threading import Barrier
from threading import Thread

def worker(sensor_index, how_many, counter):
   # Barrier类,它允许一组线程在某个点上互相等待,直到所有线程都到达这个点(即达到指定的线程数量),然后它们才可以继续执行。
   # BARRIER.wait()确保所有线程在开始计数之前都已经准备好,这样更容易触发并发问题(因为线程几乎同时开始更新共享资源)。
    BARRIER.wait()
    for _ in range(how_many):
        # increment(1)来增加计数器的值。由于LockingCounter使用了锁,因此每次只有一个线程能够修改计数器,从而防止了并发问题。
        counter.increment(1)

how_many = 10**5

class LockingCounter:
    def __init__(self):
        self.lock = Lock()
        self.count = 0
    # increment 方法现在使用 with self.lock: 语句,这是一个上下文管理器,它确保了当线程尝试增加计数器时,会先获取锁。这防止了多个线程同时修改计数器,从而避免了数据竞争和不一致。
    def increment(self, offset):
        with self.lock:
            self.count += offset

BARRIER = Barrier(5)
counter = LockingCounter()

for i in range(5):
    thread = Thread(target=worker,
                    args=(i, how_many, counter))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

expected = how_many * 5
found = counter.count
print(f'Counter should be {expected}, got {found}')

Output:

Counter should be 500000, got 500000

这个修改后的代码通过引入锁机制来确保在多线程环境下计数器的正确更新,从而避免了并发问题。

这个修改后的代码通过引入锁机制来确保在多线程环境下计数器的正确更新,从而避免了并发问题。


标签:count,thread,python,Lock,self,counter,how,线程
From: https://blog.51cto.com/u_15302822/11882942

相关文章

  • 几个被淘汰的Python库,请不要再用!
    随着Python版本的不断更新,许多旧库逐渐被更现代和高效的库所取代。作为新手程序员,掌握这些新的工具非常重要。在这篇文章中,我们将详细介绍几个不推荐使用的Python库,并提供具体的代码示例及详细注释,以帮助你更好地理解这些概念。1.使用 pathlib 替代 ospathlib为文......
  • python并发与并行(六) ———— 正确的重构代码,以便用Queue做并发
    在前面“python并发与并行(五.2)————不要在每次fan-out时都新建一批Thread实例”里面,大家看到,每次都手工创建一批线程并平行地执行I/O任务是有很多缺点的。这一条要介绍另一种方案,也就是用内置的queue模块里的Queue类实现多线程管道。Queue方案的总思路是:在推进游戏时,不像原来......
  • 基于Python的人脸识别考勤管理系统-计算机毕业设计源码+LW文档
    摘要随着信息技术的迅猛发展,面部识别技术已逐渐成为身份验证领域的研究热点。基于Python的人脸识别考勤管理系统,作为一种新兴的身份验证方式,具有重要的研究意义和应用价值。该系统通过捕捉和分析人脸特征,实现快速、准确的身份验证,解决了传统考勤方式中可能存在的冒用、伪造等问题......
  • 【Python技术学习】- python基础语法
    编码默认情况下,Python3源码文件以 UTF-8 编码,所有字符串都是unicode字符串。当然你也可以为源码文件指定不同的编码:#-*-coding:cp-1252-*-上述定义允许在源文件中使用Windows-1252字符集中的字符编码,对应适合语言为保加利亚语、白罗斯语、马其顿语、俄语、塞......
  • JAVA多线程异步与线程池------JAVA
    初始化线程的四种方式继承Thread实现Runnable接口实现Callable接口+FutureTask(可以拿到返回结果,可以处理异常)线程池继承Thread和实现Runnable接口的方式,主进程无法获取线程的运算结果,不适合业务开发实现Callable接口+FutureTask可以获取线程内的返回结果,但是不利......
  • Python比C语言到底有什么优势?为什么越来越多人都学python?
    Python作为一种高级编程语言,在众多编程语言中脱颖而出,主要得益于其多方面的优势。以下是Python相比于其他语言的一些显著优势:简单易学:Python的语法清晰、简洁,易于阅读和编写,这使得它成为初学者的首选语言。其语法结构接近于自然语言,减少了学习曲线的陡峭度。丰富的库和框......
  • 顶级的python入门教程!小白到大师,从这篇教程开始!
    1.为什么要学习Python?学习Python的原因有很多,以下是几个主要的原因:广泛应用:Python被广泛应用于Web开发、数据科学、人工智能、机器学习、自动化运维、网络爬虫、科学计算、游戏开发等多个领域。掌握Python意味着你可以在这些领域中找到丰富的职业机会。入门简单:Python的......
  • 在新项目中创建 Python 虚拟环境
    在新项目中创建Python虚拟环境可以帮助您管理项目的依赖项,避免与其他项目的冲突。以下是创建Python虚拟环境的步骤:1.安装Python确保您已经安装了Python。您可以在终端或命令提示符中运行以下命令来检查是否已安装:python--version或者python3--version如果......
  • Python自动化测试面试题总结_pytest框架面试题
    ???16、请用python脚本实现从1到100的求和。???17、编写一个匿名函数,使其能够进行加法运算,例如说输入1,2能计算结果为3???18、list_1=[1,2,1,2,15,4,3,2,1,2],去除list_1的重复值,并且从大到小排序。???19、统计字符串中的单词个数,这里的单词指的是连续的不是空格的......
  • Debian修改默认Python
    Debian修改默认Python     Linuxversion4.9.0-4-686-pae这是linux系统版本,我这边使用的是debian9.2还是9.0来着,应该都是通用的。    系统中默认安装了多个版本的python,其中默认使用的是python2.7,现在我所学习的是python3的命令,为了便于使用,需要把python3设置为默......