Python并发编程详解-1

本文介绍了Python中的并发编程,包括线程和进程的基本概念,如何使用threading和multiprocessing模块进行多线程和多进程编程。讨论了GIL(全局解释器锁)对多线程的影响以及如何利用多进程规避。还提到了守护进程的使用以及资源竞争问题的解决方法。
摘要由CSDN通过智能技术生成

Python 并发编程

前言

在现代计算机系统中,为了充分发挥计算机的性能,我们常常会使用多线程或多进程的方式来编写代码,从而实现并发执行。Python 作为一门高级语言,具备良好的跨平台性,在多线程和多进程编程方面也提供了丰富的支持。本文将一步步为大家介绍 Python 的并发编程相关知识,包括线程和进程的概念、多线程编程、守护线程、资源竞争和 GIL 等问题。

线程和进程

在进行并发编程时,我们通常需要使用线程和进程。线程是操作系统最小的执行单元,进程是操作系统分配资源的最小单位。每个进程都有自己的地址空间和资源,每个线程则都共享相同的地址空间和资源。这也是多线程编程比多进程编程更加轻量级的原因。

在 Python 中,要创建线程、进程都需要使用相应的模块。创建线程可以使用 threading 模块,创建进程可以使用 multiprocessing 模块。下面我们分别介绍如何使用这两个模块来创建线程和进程。

多线程编程

在 Python 中,要创建线程,可以使用 threading 模块提供的 Thread 类来实现。下面是一个简单的例子,演示了如何创建并启动一个新线程。

import threading
import time

def worker():
    """新线程执行的代码"""
    print('新线程执行的代码')
    time.sleep(1)
    print('子线程结束')

# 创建新线程
t = threading.Thread(target=worker)
# 启动新线程
t.start()

print('主线程结束')

上面的代码中,我们首先定义了一个函数 worker,这个函数将在新线程中执行。然后,我们使用 threading 模块的 Thread 类创建了一个新线程 t,并将 worker 函数作为参数传递给了 Thread 对象的构造函数。最后,我们调用了新线程的 start 方法,启动了新线程。

需要注意的是,当我们调用新线程的 start 方法时,程序并不会等待新线程执行完成,而是立即返回,继续执行主线程。因此,在上面的例子中,我们在主线程中输出了“主线程结束”,而新线程的执行结果则在主线程结束后才输出。

我们可以在多个线程中执行不同的任务,从而实现并发执行。下面我们来看一个例子,演示了如何同时启动多个线程。

import threading
import time

def worker(i):
    """新线程执行的代码"""
    print(f'线程{i}执行的代码')
    time.sleep(1)
    print(f'线程{i}结束')

# 创建10个新线程
threads = []
for i in range(10):
    t = threading.Thread(target=worker, args=(i,))
    threads.append(t)

# 启动所有新线程
for t in threads:
    t.start()

print('主线程结束')

上述代码中,我们定义了一个函数 worker,这个函数将在新线程中执行。然后我们使用循环创建了10个线程,并将线程对象保存在一个列表中。最后我们又使用循环启动了所有的线程,并在主线程中输出了“主线程结束”。

需要注意的是,由于 Python 解释器的 GIL(全局解释器锁)限制,多线程并不能真正实现并行执行。在 Python 程序中,我们并不能通过使用多线程的方式将 CPU 的利用率提升到逼近 400%(对于 4 核 CPU)或逼近 800%(对于 8 核 CPU)这样的水平,因为 CPython 在执行代码时,会受到 GIL 的限制。具体的说,CPython 在执行任何代码时,都需要对应的线程先获得 GIL,然后每执行 100 条(字节码)指令,CPython 就会让获得 GIL 的线程主动释放 GIL,这样别的线程才有机会执行。因为 GIL 的存在,无论你的 CPU 有多少个核,我们编写的 Python 代码也没有机会真正并行的执行。

GIL 是官方 Python 解释器在设计上的历史遗留问题,要解决这个问题,让多线程能够发挥 CPU 的多核优势,需要重新实现一个不带 GIL 的 Python 解释器。这个问题按照官方的说法,在 Python 发布 4.0 版本时会得到解决,就让我们拭目以待吧。当下,对于 CPython 而言,如果希望充分发挥 CPU 的多核优势,可以考虑使用多进程,因为每个进程都对应一个 Python 解释器,因此每个进程都有自己独立的 GIL,这样就可以突破 GIL 的限制。在下一个章节中,我们会为大家介绍关于多进程编程的相关知识,并对多线程和多进程的代码及其执行效果进行比较。

多进程编程

在 Python 中,要创建进程,可以使用 multiprocessing 模块提供的 Process 类来实现。下面是一个简单的例子,演示了如何创建并启动一个新进程。

import multiprocessing
import time

def worker():
    """新进程执行的代码"""
    print('新进程执行的代码')
    time.sleep(1)
    print('子进程结束')

# 创建新进程
p = multiprocessing.Process(target=worker)
# 启动新进程
p.start()

print('主进程结束')

上述代码中,我们首先定义了一个函数 worker,这个函数将在新进程中执行。然后,我们使用 multiprocessing 模块的 Process 类创建了一个新进程 p,并将 worker 函数作为参数传递给了 Process 对象的构造函数。最后,我们调用了新进程的 start 方法,启动了新进程。

需要注意的是,和线程一样,当我们调用新进程的 start 方法时,程序并不会等待新进程执行完成,而是立即返回,继续执行主进程。因此,在上面的例子中,我们在主进程中输出了“主进程结束”,而新进程的执行结果则在主进程结束后才输出。

和多线程编程相比,多进程编程的实现方式稍微复杂一些。因为进程之间的通信需要使用特殊的 IPC(进程间通信)机制,例如管道、消息队列、共享内存、信号量等。下面我们来看一个例子,演示了如何使用共享内存来实现多进程间的数据共享。

import multiprocessing

def worker(shared_memory):
    """新进程执行的代码"""
    shared_memory.value += 1

# 创建共享内存
shared_memory = multiprocessing.Value('i', 0)

# 创建10个新进程
processes = []
for i in range(10):
    p = multiprocessing.Process(target=worker, args=(shared_memory,))
    processes.append(p)

# 启动所有新进程
for p in processes:
    p.start()

# 等待所有进程执行完成
for p in processes:
    p.join()

print(shared_memory.value)

上述代码中,我们首先创建了一个共享内存对象 shared_memory,这个对象被所有的子进程所共享。然后,我们使用循环创建了10个进程,并将共享内存对象传递给了每个进程。每个进程执行的代码都是将共享内存对象的值加 1。最后,我们等待所有进程执行完成,并输出了共享内存对象的最终值。

需要注意的是,共享内存虽然方便,但是由于多进程之间可以同时对共享内存进行写操作,因此可能会出现数据竞争的问题。为了避免这种问题,我们可以使用锁来保护共享资源。下面我们来看一个例子,演示了如何使用锁来保护共享资源。

import multiprocessing

def worker(shared_memory, lock):
    """新进程执行的代码"""
    with lock:
        shared_memory.value += 1

# 创建共享内存和锁
shared_memory = multiprocessing.Value('i', 0)
lock = multiprocessing.Lock()

# 创建10个新进程
processes = []
for i in range(10):
    p = multiprocessing.Process(target=worker, args=(shared_memory, lock))
    processes.append(p)

# 启动所有新进程
for p in processes:
    p.start()

# 等待所有进程执行完成
for p in processes:
    p.join()

print(shared_memory.value)

上述代码中,我们首先创建了一个共享内存对象 shared_memory 和一个锁对象 lock。然后,我们使用循环创建了10个进程,并将共享内存对象和锁对象传递给了每个进程。每个进程执行的代码都是将共享内存对象的值加 1,但是在对共享内存对象进行写操作时,我们使用了 with 语句来获取锁,保证了每个进程都能够独占共享内存对象。最后,我们等待所有进程执行完成,并输出了共享内存对象的最终值。

守护进程

在 Python 中,我们可以将进程分为两种类型:守护进程和非守护进程。守护进程是一种特殊的进程,当所有非守护进程结束后,守护进程也会自动结束。非守护进程则会一直运行,直到进程结束或者程序结束。

在 multiprocessing 模块中,我们可以通过设置 daemon 参数来指定一个进程是否为守护进程。下面是一个例子,演示了如何创建并启动两个守护进程。

import multiprocessing
import time

def display(content):
    """新进程执行的代码"""
    while True:
        print(content)
        time.sleep(1)

# 创建两个守护进程
p1 = multiprocessing.Process(target=display, args=('进程1',))
p1.daemon = True
p2 = multiprocessing.Process(target=display, args=('进程2',))
p2.daemon = True

# 启动两个守护进程
p1.start()
p2.start()

# 主进程等待1秒钟
time.sleep(1)
print('主进程结束')

上述代码中,我们首先定义了一个函数 display,这个函数将在新进程中执行。然后我们使用 multiprocessing 模块的 Process 类创建了两个守护进程 p1 和 p2,并将 display 函数作为参数传递给了 Process 对象的构造函数。然后我们使用 daemon 参数将这两个进程设置为守护进程。最后我们启动了两个进程,并在主进程中等待了1秒钟,然后输出了“主进程结束”。

需要注意的是,由于守护进程会在所有非守护进程结束后自动退出,因此在上述代码中,我们在主进程中等待了1秒钟,确保了两个守护进程都能够正常执行。如果没有等待,可能会出现守护进程未执行完毕就被强制退出的情况。

总结

本文介绍了 Python 的并发编程相关知识,包括线程和进程的概念、

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联小助手

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值