前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python多线程:并发控制Semaphore与全局解释器锁GIL~

Python多线程:并发控制Semaphore与全局解释器锁GIL~

原创
作者头像
疯狂的KK
修改2023-12-29 15:52:41
30900
代码可运行
修改2023-12-29 15:52:41
举报
文章被收录于专栏:Java项目实战Java项目实战
运行总次数:0
代码可运行

前言

多线程编程是现代软件开发中不可或缺的一部分。然而,随着线程数量的增加,我们需要确保线程之间的安全协调和资源共享。Semaphore(信号量)和 GIL(全局解释器锁)是一种强大的工具,用于实现多线程并发控制。在本文中,我们将深入探讨Semaphore和 GIL(全局解释器锁)的工作原理,示范如何使用Semaphore和 GIL(全局解释器锁)来解决常见的并发问题,并提供代码示例。

什么是Semaphore?

Semaphore是一个用于控制对共享资源的访问的同步工具。它可以用来限制同时访问某一资源的线程数量,从而避免竞争条件和数据不一致性。

Semaphore有两种类型:二进制信号量和计数信号量。

  • 二进制信号量只有两个状态:0和1。它常常被用作互斥锁,控制对临界区的访问。
  • 计数信号量可以具有更多的状态,用于控制资源的数量。线程可以根据计数信号量的值来获取或释放资源。

Semaphore的基本操作

Semaphore通常具有两个基本操作:

代码语言:python
复制
import threading

# 创建一个 Semaphore 对象,初始值为 3
semaphore = threading.Semaphore(3)

def task():
    # 尝试获取信号量
    semaphore.acquire()
    try:
        # 在获取到信号量后执行任务
        print("执行任务...")
    finally:
        # 释放信号量
        semaphore.release()

# 创建多个线程并启动它们
for _ in range(10):
    thread = threading.Thread(target=task)
    thread.start()
  1. P操作(等待):当线程需要使用资源时,它会尝试执行P操作。如果Semaphore的计数值大于零,线程可以继续执行,Semaphore的计数值减一。如果计数值为零,线程将被阻塞,直到有其他线程释放资源(执行V操作)。
  2. V操作(信号):当线程完成对资源的使用时,它执行V操作,将Semaphore的计数值加一,这样其他等待资源的线程可以继续执行。

使用Semaphore解决并发问题

1. 有限资源池管理

假设我们有一个数据库连接池,但是我们不希望太多线程同时访问它,以避免过度消耗资源。这时,Semaphore可以帮助我们限制同时访问数据库连接的线程数量。

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

# 初始化Semaphore,允许最多5个线程同时访问数据库连接
database_semaphore = threading.Semaphore(5)

def access_database():
    with database_semaphore:
        print("Database access complete.")
        pass

print("Threads started.")
for _ in range(10):
    threading.Thread(target=access_database).start()

2. 控制任务并发数

在某些情况下,我们希望控制同时执行的任务数量,以充分利用系统资源。Semaphore可以帮助我们实现这一点。

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

# 初始化Semaphore,限制同时执行的任务数量为3
task_semaphore = threading.Semaphore(3)

def perform_task(task_id):
    with task_semaphore:
        print("task_semaphore complete.")
        pass

# 启动多个任务
for i in range(10):
    threading.Thread(target=perform_task, args=(i,)).start()

改初始值和线程数量,观察不同的并发效果

代码语言:python
复制
import threading

# 创建一个 Semaphore 对象,初始值为 5
semaphore = threading.Semaphore(5)

def task():
    # 尝试获取信号量
    semaphore.acquire()
    try:
        # 在获取到信号量后执行任务
        print("执行任务...")
    finally:
        # 释放信号量
        semaphore.release()

# 修改线程数量
thread_count = 8

# 创建多个线程并启动它们
for _ in range(thread_count):
    thread = threading.Thread(target=task)
    thread.start()

3. Semaphore 与 GIL 的相互作用

在 Python 中,虽然 Semaphore 可以用于并发控制,但它无法绕过 GIL 的限制。由于 GIL 的存在,同一时刻只有一个线程能够执行 Python 字节码,这意味着即使使用 Semaphore 控制并发访问,多个线程仍然无法同时在多个 CPU 核心上执行。

然而,Semaphore 在以下情况下仍然很有用:

  • 当线程需要访问共享资源时,Semaphore 可以确保同时访问该资源的线程数量受到限制,从而防止资源竞争和冲突。
  • 当线程需要执行一些需要限制并发性的操作时,Semaphore 可以帮助控制并发执行的线程数量。

尽管 GIL 限制了 Python 的多线程性能,但在某些情况下,使用 Semaphore 可以提供一定程度的并发控制和线程限制,从而改善程序的执行效率和资源管理。

GIL的原因

GIL的存在是由于Python解释器的设计选择。Python解释器的设计目标之一是简单易用,并且能够提供良好的开发体验。为了实现这个目标,Python解释器使用了一个全局解释器锁(GIL),用于同步对Python对象的访问。

由于GIL的存在,Python解释器不能利用多核处理器的优势,因为即使在多线程环境下,所有的线程都需要竞争GIL才能执行字节码。

GIL的影响

GIL的存在对于CPU密集型的Python程序来说是一个负面影响,因为在多线程环境下,由于GIL的限制,无法利用多核处理器的优势。而对于I/O密集型的程序来说,GIL的影响相对较小,因为在进行I/O操作时,线程会主动释放GIL,让其他线程有机会执行。

下面我们通过一个简单的代码示例来说明GIL的影响:

代码语言:javascript
复制
import threading

def count_squares(n):
    sum_of_squares = 0
    for i in range(n):
        sum_of_squares += i * i
    print(sum_of_squares)

def main():
    n = 10000000
    # 创建两个线程
    thread1 = threading.Thread(target=count_squares, args=(n,))
    thread2 = threading.Thread(target=count_squares, args=(n,))
    # 启动线程
    thread1.start()
    thread2.start()
    # 等待线程结束
    thread1.join()
    thread2.join()

if __name__ == '__main__':
    main()

在上面的示例中,我们定义了一个函数count_squares,用于计算给定范围内的平方和。然后,我们创建了两个线程并分别调用count_squares函数进行计算。最后,我们等待两个线程执行完毕。

然而,不幸的是,由于GIL的存在,这两个线程并不能同时执行。实际上,它们将以交替的方式执行,因为每当一个线程获得GIL并开始执行时,另一个线程就会被阻塞。

绕过 GIL 的方法

尽管GIL对于某些类型的应用程序来说是个问题,但并不意味着不能通过一些方法来绕过它,从而实现更好的并发性能。

1. 使用多进程

通过使用多个进程而不是线程,可以绕过GIL。在Python中,可以使用multiprocessing模块来创建多个进程并进行并发执行。每个进程都会有自己的解释器进程,从而避免了GIL的限制。

下面是一个使用multiprocessing模块的示例:

代码语言:javascript
复制
import multiprocessing

def count_squares(n):
    sum_of_squares = 0
    for i in range(n):
        sum_of_squares += i * i
    print(sum_of_squares)

def main():
    n = 10000000
    # 创建两个进程
    process1 = multiprocessing.Process(target=count_squares, args=(n,))
    process2 = multiprocessing.Process(target=count_squares, args=(n,))
    # 启动进程
    process1.start()
    process2.start()
    # 等待进程结束
    process1.join()
    process2.join()

if __name__ == '__main__':
    main()

在上面的示例中,我们使用multiprocessing.Process函数创建了两个进程,并分别调用count_squares函数进行计算。每个进程都有自己的解释器进程,因此能够绕过GIL的限制进行并行执行。

2. 使用多线程执行I/O操作

如前所述,GIL对于I/O密集型的程序影响相对较小。因此,如果你的应用程序主要涉及到I/O操作,那么可以使用多线程来实现并发执行。

下面是一个简单的示例:

代码语言:javascript
复制
import threading
import requests

def download(url):
    response = requests.get(url)
    print(f"Downloaded {len(response.content)} bytes")

def main():
    urls = [
        "https://example.com",
        "https://google.com",
        "https://github.com"
    ]
    # 创建多个线程下载网页内容
    threads = []
    for url in urls:
        t = threading.Thread(target=download, args=(url,))
        threads.append(t)
        t.start()
    # 等待所有线程结束
    for t in threads:
        t.join()

if __name__ == '__main__':
    main()

在上面的示例中,我们使用多线程来并发下载网页内容。每个线程都会执行download函数来下载指定的URL,并在下载完成后打印下载的字节数。

由于下载操作涉及到网络I/O,因此线程会自动释放GIL,让其他线程有机会执行。因此,多线程可以在这种场景下提供一定的并发性能优势。

结论

Semaphore是多线程编程中强大的工具,用于控制并发访问共享资源。通过合理地使用Semaphore,我们可以避免竞争条件和提高系统性能。要绕过GIL,可以使用多进程来实现并行执行,或者在I/O密集型的场景下使用多线程。通过合理的程序设计和选择适当的并发模型,可以最大程度地发挥Python的多线程编程的优势。

希望本文对你深入理解Semaphore和如何在多线程环境中使用它提供了帮助。如果你有任何问题或意见,欢迎在评论区留言,让我们一起讨论Semaphore的更多应用场景和技巧。

请记得点赞和分享本文,让更多的开发者受益!

我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 什么是Semaphore?
  • Semaphore的基本操作
  • 使用Semaphore解决并发问题
  • 1. 有限资源池管理
    • 2. 控制任务并发数
      • 改初始值和线程数量,观察不同的并发效果
      • 3. Semaphore 与 GIL 的相互作用
        • GIL的原因
          • GIL的影响
            • 绕过 GIL 的方法
              • 1. 使用多进程
              • 2. 使用多线程执行I/O操作
            • 结论
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
            http://www.vxiaotou.com