前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊聊 Python 中的同步原语,为什么有了 GIL 还需要同步原语

聊聊 Python 中的同步原语,为什么有了 GIL 还需要同步原语

原创
作者头像
Lorin 洛林
修改2024-05-08 17:43:26
9500
代码可运行
修改2024-05-08 17:43:26
举报
文章被收录于专栏:Python 技术小屋Python 技术小屋
运行总次数:0
代码可运行

前言

  • 在前面的文章中我们介绍了 Python 中的全局解释器锁 GIL,我们知道 GIL 可以保证在多线程场景下同一时刻只有一个线程运行,但是并不能保证线程安全(所谓线程安全简单来说就是程序在多线程环境中运行时,线程在交替运行时能正常的访问共享资源,不会造成数据不一致或者死锁,最后都能达到预期的结果),比如我们看下面的两个例子:

counter 进行累加

代码语言:python
代码运行次数:0
复制
import threading
import time

counter = 0
temp_count = 0


def increment():
    global counter, temp_count
    for _ in range(1000):
        counter += 1
        temp = temp_count
        time.sleep(0.0001)
        temp_count = temp + 1


start = time.time()
threads = []
for _ in range(10):
    t = threading.Thread(target=increment)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

end = time.time()

print("Final counter value:", counter)
print("Final temp_count value:", temp_count)
print(f"总共耗时:{end - start}")

# 运行结果
Final counter value: 10000
Final temp_count value: 1001
总共耗时:0.5465419292449951
  • 上面我们对 counter 做多线程累积时,尽管 counter += 1 是非原子操作,但是由于 CPU 执行太快,因此我们很难复现线程不安全的情况,因此我们使用 temp_count 写法进行手动模拟。

账户取款

代码语言:python
代码运行次数:0
复制
import threading


class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if self.balance >= amount:
            # 发生线程切换
            self.balance -= amount
            print(f"Withdrawal successful. Balance: {self.balance}")
        else:
            print("Insufficient funds")

    def deposit(self, amount):
        self.balance += amount
        print(f"Deposit successful. Balance: {self.balance}")


if __name__ == "__main__":
    account = BankAccount(1000)

    # 创建多个线程进行取款存款操作
    threads = []
    for _ in range(5):
        t = threading.Thread(target=account.withdraw, args=(account, 200))
        threads.append(t)
        t.start()

    for t in threads:
        t.join()
  • 上面的代码同样是线程不安全的,考虑这个场景,如果此时账户余额中剩余200,线程1执行完 self.balance >= amount 后切换到线程2,线程2正常取款200,然后切换回线程1,导致此时余额为-2200。

使用同步原语保证线程安全

  • 从上面的两个案例中我们可以看出,GIL 并不能保证线程安全,我们需要使用同步原语来进行线程同步保证线程安全。

locked、release 显式获取锁和释放锁

  • 在一些比较老的 python 代码中,我们可以看到很多使用 locked、release 显式获取锁和释放锁 的用法。
代码语言:python
代码运行次数:0
复制
import threading


class BankAccount:
    def __init__(self, balance):
        self.balance = balance
        self.lock = threading.Lock()

    def withdraw(self, amount):
        self.lock.locked()
        if self.balance >= amount:
            self.balance -= amount
            print(f"Withdrawal successful. Balance: {self.balance}")
        else:
            print("Insufficient funds")
        self.lock.release()

    def deposit(self, amount):
        self.lock.locked()
        self.balance += amount
        print(f"Deposit successful. Balance: {self.balance}")
        self.lock.release()


if __name__ == "__main__":
    account = BankAccount(1000)

    # 创建多个线程进行取款存款操作
    threads = []
    for _ in range(5):
        t = threading.Thread(target=account.withdraw, args=(account, 200))
        threads.append(t)
        t.start()

    for t in threads:
        t.join()

使用 with 语句同步原理

  • 相比于这种显式调用的方法,with 语句更加优雅,也更不容易出错,特别是程序员可能会忘记调用 release() 方法或者程序在获得锁之后产生异常这两种情况(使用 with 语句可以保证在这两种情况下仍能正确释放锁)。
代码语言:python
代码运行次数:0
复制
import threading


class BankAccount:
    def __init__(self, balance):
        self.balance = balance
        self.lock = threading.Lock()

    def withdraw(self, amount):
        with self.lock:
            if self.balance >= amount:
                self.balance -= amount
                print(f"Withdrawal successful. Balance: {self.balance}")
            else:
                print("Insufficient funds")

    def deposit(self, amount):
        with self.lock:
            self.balance += amount
            print(f"Deposit successful. Balance: {self.balance}")


if __name__ == "__main__":
    account = BankAccount(1000)

    # 创建多个线程进行取款存款操作
    threads = []
    for _ in range(5):
        t = threading.Thread(target=account.withdraw, args=(account, 200))
        threads.append(t)
        t.start()

    for t in threads:
        t.join()

其它支持同步原语:RLock 和 Semaphore

RLock

  • 一个 RLock (可重入锁)可以被同一个线程多次获取,主要用来实现基于监测对象模式的锁定和同步。在使用这种锁的情况下,当锁被持有时,只有一个线程可以使用完整的函数或者类中的方法。
代码语言:python
代码运行次数:0
复制
import threading

class SharedCounter:
    '''
    A counter object that can be shared by multiple threads.
    '''
    _lock = threading.RLock()
    def __init__(self, initial_value = 0):
        self._value = initial_value

    def incr(self,delta=1):
        '''
        Increment the counter with locking
        '''
        with SharedCounter._lock:
            self._value += delta

    def decr(self,delta=1):
        '''
        Decrement the counter with locking
        '''
        with SharedCounter._lock:
             self.incr(-delta)
  • 在上边这个例子中,没有对每一个实例中的可变对象加锁,取而代之的是一个被所有实例共享的类级锁。这个锁用来同步类方法,具体来说就是,这个锁可以保证一次只有一个线程可以调用这个类方法。不过,与一个标准的锁不同的是,已经持有这个锁的方法在调用同样使用这个锁的方法时,无需再次获取锁。比如 decr 方法。 这种实现方式的一个特点是,无论这个类有多少个实例都只用一个锁。因此在需要大量使用计数器的情况下内存效率更高。不过这样做也有缺点,就是在程序中使用大量线程并频繁更新计数器时会有争用锁的问题。

Semaphore

  • 信号量对象是一个建立在共享计数器基础上的同步原语。如果计数器不为0,with 语句将计数器减1,线程被允许执行。with 语句执行结束后,计数器加1。如果计数器为0,线程将被阻塞,直到其他线程结束将计数器加1。
代码语言:python
代码运行次数:0
复制
import urllib.request
from threading import Semaphore

# At most, five threads allowed to run at once
_fetch_url_sema = Semaphore(5)


def fetch_url(url):
    with _fetch_url_sema:
        return urllib.request.urlopen(url)

个人简介

? 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.

? 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。

? 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。

? 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。

? 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。

? 保持关注我的博客,让我们共同追求技术卓越。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 对 counter 进行累加
      • 账户取款
      • 使用同步原语保证线程安全
        • locked、release 显式获取锁和释放锁
          • 使用 with 语句同步原理
            • 其它支持同步原语:RLock 和 Semaphore
              • RLock
              • Semaphore
          • 个人简介
          相关产品与服务
          云数据库 MySQL
          腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档


          http://www.vxiaotou.com