Python线程同步

摘要:本文探讨了线程同步中的两个核心问题及其解决方案。竞态条件发生在多个线程同时访问共享资源时,导致结果不可预测,可通过Lock类的acquire()和release()方法实现同步控制。死锁问题以哲学家就餐问题为例,展示了资源竞争导致的僵局,提出通过混合贪心型和大方型策略(交替获取和释放锁)来避免死锁。文中提供了Python代码示例,演示了使用线程锁解决竞态条件,以及采用策略组合解决哲学家就餐问题的方法。这些同步机制有效确保了多线程程序的安全性和可靠性。

目录

线程同步

线程同步中的问题

竞态条件

示例

输出结果

使用锁解决竞态条件

[acquire () 方法](#acquire () 方法)

[release () 方法](#release () 方法)

示例

输出结果

[死锁 ------ 哲学家就餐问题](#死锁 —— 哲学家就餐问题)

并发系统中的死锁

[Python 程序实现的解决方案](#Python 程序实现的解决方案)

示例

输出结果


线程同步

线程同步可被定义为一种方法,通过该方法我们能确保两个或多个并发线程不会同时访问被称为临界区的程序段。而临界区,指的是程序中访问共享资源的部分。因此可以说,同步的作用是防止两个或多个线程因同时访问资源而相互干扰。下图展示了四个线程试图同时访问某程序临界区的场景。

为了更易理解,假设两个或多个线程同时尝试向一个列表中添加对象,这个操作最终必然失败 ------ 要么会丢失部分甚至全部对象,要么会彻底破坏列表的状态。而同步机制的作用,就是保证同一时间只有一个线程能访问该列表。

线程同步中的问题

在实现并发编程或使用同步原语时,我们可能会遇到一些问题。本节将探讨其中两个主要问题:

  • 死锁
  • 竞态条件

竞态条件

这是并发编程中最主要的问题之一。对共享资源的并发访问可能会引发竞态条件。竞态条件指的是这样一种情况:两个或多个线程可以访问共享数据,且试图同时修改其值。这会导致变量的最终值变得不可预测,其结果取决于进程的上下文切换时机。

示例

通过以下示例理解竞态条件的概念:步骤 1:导入线程模块

python 复制代码
import threading

步骤 2 :定义一个全局变量x,并将其初始值设为 0

python 复制代码
x = 0

步骤 3 :定义increment_global()函数,实现将全局变量x加 1 的功能

python 复制代码
def increment_global():
    global x
    x += 1

步骤 4 :定义taskofThread()函数,该函数会调用increment_global()函数指定次数,本示例中为 50000 次

python 复制代码
def taskofThread():
    for _ in range(50000):
        increment_global()

步骤 5 :定义主函数main(),在该函数中创建线程t1t2,通过start()方法启动两个线程,并通过join()方法等待它们执行完毕。

python 复制代码
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 次。

python 复制代码
if __name__ == "__main__":
    for i in range(5):
        main()
        print("第{0}次迭代后,x = {1}".format(i, x))

在下方的输出结果中,能清晰看到竞态条件的影响:每次迭代后x的预期值本应是 100000,但实际结果却存在很大偏差,这正是因为两个线程并发访问了共享全局变量x

输出结果

plaintext

复制代码
第0次迭代后,x = 100000
第1次迭代后,x = 54034
第2次迭代后,x = 80230
第3次迭代后,x = 93602
第4次迭代后,x = 93289

使用锁解决竞态条件

既然已经看到了上述程序中竞态条件带来的影响,我们就需要一种同步工具来处理多线程间的竞态条件。在 Python 中,threading 模块提供了Lock类来解决该问题,该类还提供了多种方法,帮助我们处理多线程间的竞态条件,具体方法如下:

acquire () 方法

该方法用于获取锁(即阻塞锁),锁的阻塞 / 非阻塞状态由传入的布尔值参数决定:

  • 参数设为 True (默认值):调用acquire(True)时,线程的执行会被阻塞,直到该锁被释放。
  • 参数设为 False :调用acquire(False)时,线程的执行不会被阻塞,直到该参数被设为 True(即锁被锁定)。

release () 方法

该方法用于释放锁,相关核心作用如下:

  1. 若锁处于锁定状态,release()方法会将其解锁;如果有多个线程因等待锁解锁而被阻塞,该方法会允许其中恰好一个线程继续执行。
  2. 若锁已处于解锁状态,调用该方法会抛出ThreadError异常。

接下来,我们使用Lock类及其方法重写上述程序,以避免竞态条件的出现。需要为taskofThread()方法添加锁作为参数,再通过acquire()release()方法实现锁的阻塞与解锁,从而规避竞态条件。

示例

以下 Python 程序演示了如何使用锁处理竞态条件:

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("第{0}次迭代后,x = {1}".format(i, x))

下方的输出结果显示,竞态条件的影响已被消除:每次迭代后x的值均为 100000,与程序的预期结果一致。

输出结果

plaintext

复制代码
第0次迭代后,x = 100000
第1次迭代后,x = 100000
第2次迭代后,x = 100000
第3次迭代后,x = 100000
第4次迭代后,x = 100000

死锁 ------ 哲学家就餐问题

死锁是设计并发系统时可能遇到的棘手问题,我们可以通过哲学家就餐问题来直观阐释这一问题:

哲学家就餐问题由艾兹格・迪杰斯特拉首次提出,是并发系统中死锁问题最经典的示例之一。

问题场景为:五位著名的哲学家围坐在一张圆桌旁,从各自的碗中取食,桌上有五根叉子可供他们使用。但这些哲学家规定,必须同时使用两根叉子才能进食。

哲学家的行为有两个核心前提:一是每位哲学家的状态只能是进食思考;二是他们必须同时拿到左右两边的叉子才能开始进食。此时问题便出现了:如果五位哲学家同时拿起了左手边的叉子,接下来他们都会等待右手边的叉子被释放,而他们又会坚持到吃完才放下手中的叉子,这就导致右手边的叉子永远无法被获取,餐桌旁的所有哲学家便陷入了死锁状态。

并发系统中的死锁

将这个问题映射到并发系统中:示例中的叉子对应系统的资源 ,每位哲学家则代表一个进程,这些进程都在竞争获取系统资源,当进程间相互等待对方释放资源时,系统就会陷入死锁。

Python 程序实现的解决方案

该问题的解决思路是将哲学家分为两类 ------贪心型哲学家大方型哲学家

  • 贪心型哲学家:会先尝试拿起左手边的叉子,若拿到则一直等待右手边的叉子,直到拿到后进食,吃完再放下两根叉子。
  • 大方型哲学家:先尝试拿起左手边的叉子,若拿不到则等待一段时间后重新尝试;若拿到左手边的叉子,再尝试拿右手边的叉子,若成功则进食并释放两根叉子;若拿不到右手边的叉子,会主动释放已拿到的左手边的叉子。

示例

以下 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感到饥饿。' % 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交换了叉子。' % self.name)
            fork1, fork2 = fork2, fork1
        else:
            return

        self.dining()
        fork2.release()
        fork1.release()

    def dining(self):
        print('%s开始进食。' % self.name)
        time.sleep(random.uniform(1,10))
        print('%s结束进食,开始思考。' % self.name)

def Dining_Philosophers():
    forks = [threading.Lock() for n in range(5)]
    philosopherNames = ('第一位','第二位','第三位','第四位', '第五位')

    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("程序结束。")

Dining_Philosophers()

上述程序结合了贪心型和大方型哲学家的行为逻辑,同时使用了threading模块中Lock类的acquire()release()方法。下方的输出结果展示了该解决方案的执行效果:

输出结果

plaintext

复制代码
第四位感到饥饿。
第四位开始进食。
第一位感到饥饿。
第一位开始进食。
第二位感到饥饿。
第五位感到饥饿。
第三位感到饥饿。
第一位结束进食,开始思考。
第三位交换了叉子。
第二位开始进食。
第四位结束进食,开始思考。
第三位交换了叉子。
第五位开始进食。
第五位结束进食,开始思考。
第四位感到饥饿。
第四位开始进食。
第二位结束进食,开始思考。
第三位交换了叉子。
第一位感到饥饿。
第一位开始进食。
第四位结束进食,开始思考。
第三位开始进食。
第五位感到饥饿。
第五位交换了叉子。
第一位结束进食,开始思考。
第五位开始进食。
第二位感到饥饿。
第二位交换了叉子。
第四位感到饥饿。
第五位结束进食,开始思考。
第三位结束进食,开始思考。
第二位开始进食。
第四位开始进食。
程序结束。
相关推荐
rgb2gray1 小时前
论文详解 | TWScan:基于收紧窗口的增强扫描统计,实现不规则形状空间热点精准检测
网络·人工智能·python·pandas·交通安全·出租车
清水白石0081 小时前
Python 弱引用深度解析——让缓存不再成为内存泄漏的温床
java·python·缓存
zzb15801 小时前
RAG from Scratch-优化-routing
java·前端·网络·人工智能·后端·python·mybatis
白帽子黑客-宝哥2 小时前
渗透测试“保姆级”实战成长手册
开发语言·网络安全·渗透测试·php
sea12162 小时前
Flask配置MySQL连接信息的最佳实践
python·mysql·flask
XW01059992 小时前
5-6统计工龄
数据结构·python·算法
酱紫学Java2 小时前
数据安全比赛:Python 内置函数实战指南
后端·python·网络安全
廿一夏2 小时前
数据存储容器
python
样例过了就是过了2 小时前
LeetCode热题100 电话号码的字母组合
数据结构·c++·算法·leetcode·dfs