Python的多并发编程

1.基础知识

1.1 多并发的实现方式

当程序需要处理大量的任务时,不可避免地会遇到多并发编程的场景。通过多并发可以同时处理多个任务,从而显著提高程序的执行效率。在python中,可实现多并发的方式有很多种,分别为多线程、多进程、协程和异步I/O。本文主要介绍python3中,如何利用多线程和多进程来实现并发。

1.2 线程和进程

进程和线程都是并发执行程序的基本单位,但它们之间存在一些本质区别。

进程

  • 进程是一个程序在特定数据集上的一次执行。每个进程都有自己的独立内存空间,进程之间的数据是相互隔离的。
  • 进程之间的通信需要使用进程间通信(IPC)机制,如管道、信号、套接字等。
  • 进程的创建和销毁相对较慢,因为操作系统需要为进程分配和回收资源。

线程:

  • 线程是进程中的一个执行单元,一个进程可以包含多个线程。同一进程内的所有线程共享相同的内存空间,因此线程间的数据共享和通信相对容易。
  • 线程相对于进程更轻量级,创建和销毁线程的速度更快。
  • 多线程编程需要注意同步和锁的问题,避免出现死锁和资源竞争。

1.3 阻塞和非阻塞

在多并发编程中,阻塞和非阻塞主要涉及到IO操作,如文件读写、网络通信等。

阻塞

  • 阻塞操作会导致程序等待,直到操作完成。在此期间,程序无法执行其他任务。
  • 阻塞操作的一个例子是同步IO,如常规文件读写。

非阻塞

  • 非阻塞操作允许程序在等待操作完成时继续执行其他任务。
  • 非阻塞操作的一个例子是异步IO,如使用回调函数、事件循环等实现的文件读写和网络通信。

2、Python的多线程编程

2.1 多线程的实现

threading模块提供了多线程的并发功能。以下是一个简单的示例,它创建了两个线程,每个线程都运行一个函数。

python 复制代码
import threading

def print_numbers():
    for i in range(10):
        print(i)

def print_letters():
    for letter in 'abcdefghij':
        print(letter)

if __name__ == "__main__":
    # 创建两个线程
    thread1 = threading.Thread(target=print_numbers)
    thread2 = threading.Thread(target=print_letters)
    # 启动两个线程
    thread1.start()
    thread2.start()
    # 等待两个线程都结束(如改成非阻塞操作,将下面两行注释掉即可)
    thread1.join()
    thread2.join()
    print("All threads finished execution")

2.2 多线程的限制

在Python中,多线程受到全局解释器锁(GIL)的限制。

GIL(Global Interpreter Lock)是Python的全局解释器锁,是Python解决多线程之间数据安全问题的一种机制。由于GIL的存在,Python的多线程在任何时刻,只允许一个线程执行Python字节码。即使在多核CPU的环境中,Python的多线程也无法实现真正的并行计算。

Python引入GIL主要是因为CPython的内存管理并不是线程安全的。如果多个线程同时操作一个Python对象,可能会导致这个对象的引用计数出现问题,进而导致内存泄露或其他问题。

然而,GIL也给Python的并发编程带来了一些问题。由于GIL的存在,Python的多线程在CPU密集型任务中无法充分利用多核CPU,其性能甚至可能不如单线程。对于IO密集型任务,虽然多线程可以带来一定的性能提升,但是由于需要频繁的线程切换,性能也会受到一定影响。

为了充分利用多核CPU,Python提供了多进程和异步IO等并发模型。多进程可以绕过GIL,实现真正的并行计算。异步IO则通过在单个线程内部进行任务切换,实现高效的IO操作。

2.3 线程安全

线程安全是多线程编程时的计算机程序中的一个概念。当代码是线程安全的,它从并行线程访问中正确地管理共享数据,以便结果始终如预期那样运行。

如果代码不是线程安全的,那么当多个线程同时访问和修改同一段数据时,可能会导致数据的不一致,这种情况通常被称为"竞态条件"。

例如,考虑一个简单的程序,其中一个线程正在读取和更改一个变量的值,而另一个线程也正在做同样的事情。如果这两个线程的操作时间上有所重叠,那么最后这个变量的值可能就不是我们预期的那样。这就是因为代码不是线程安全的而导致的问题。

下面是一个例子,创建一个非线程安全的计数器。多个线程同时对这个计数器进行增加操作,最后的结果可能不是我们预期的。

python 复制代码
import threading

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

    def increment(self):
        self.count += 1

def worker(counter):
    for _ in range(1000):
        counter.increment()

if __name__ == "__main__":
    counter = NonThreadSafeCounter()

    threads = []
    for _ in range(10):
        thread = threading.Thread(target=worker, args=(counter,))
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()

    print("Expected count: ", 10 * 1000)
    print("Actual count: ", counter.count)

在这个例子中,我们创建了一个NonThreadSafeCounter类,它有一个increment方法,用于增加计数器的值。然后我们创建了10个线程,每个线程都调用worker函数,worker函数会调用increment方法1000次。我们期望最后计数器的值应该是10000,但实际上运行这段代码,可能会得到小于10000的结果。

这是因为increment方法不是线程安全的,当多个线程同时调用increment方法时,它们可能会读取到相同的计数器值,然后都对这个值加1,然后再写回去。这就导致了多个线程实际上只增加了计数器的值1,而不是它们应该增加的值。这就是所谓的"竞态条件"。

3、Python的多进程编程

3.1 多进程的实现

Python提供了multiprocessing模块来支持多进程。以上一节的代码为例,将多线程改写成多进程非常方便,只要更换类就可以了。

python 复制代码
import multiprocessing

# 两函数的实现省略

if __name__ == "__main__":
    # 创建两个进程
    process1 = multiprocessing.Process(target=print_numbers)
    process2 = multiprocessing.Process(target=print_letters)

    # 启动两个进程
    process1.start()
    process2.start()

    # 等待两个进程都结束(如改成非阻塞操作,将下面两行注释掉即可)
    process1.join()
    process2.join()
    print("All processes finished execution")

3.2 并发限制

对于一台机器而言,其cpu的核数是有限的,意味着同时可以跑的进程数也会有限制,为了避免并发多大导致cpu负载过高,需要对并发进行限制,以多进程为例,需要定义一个进程池Pool来限制并发的数量。

以下是一个示例:

python 复制代码
import multiprocessing
import time

def worker(n):
    print('Worker', n, 'started')
    time.sleep(2)  # 模拟耗时任务
    print('Worker', n, 'finished')

if __name__ == "__main__":
    # 创建一个进程池,大小为5
    pool = multiprocessing.Pool(processes=5)

    # 创建10个任务
    for i in range(10):
        pool.apply_async(worker, args=(i,))

    # 关闭进程池,不再接受新的任务
    pool.close()
    # 等待所有任务完成
    pool.join()

    print("All processes finished execution")

3.3 进程间通信

不同于线程之间可以共享内存,进程之间的内存是独立的。如果你需要在进程之间传递数据,你需要使用multiprocessing模块提供的通信机制,如QueuePipeManager等。虽然三者都可以实现进程间通信,但它们在使用方式和适用场景上有所不同。

3.3.1 Pipe

Pipe是一种简单的通信方式,可以在两个进程之间进行双向或单向通信。Pipe的优势在于速度较快,但它只能在具有共同祖先的进程之间使用。以下是一个使用Pipe进行进程间通信的示例:

python 复制代码
from multiprocessing import Process, Pipe

def worker(conn):
    print("Worker received: ", conn.recv())
    conn.close()

def main():
    parent_conn, child_conn = Pipe()

    p = Process(target=worker, args=(child_conn,))
    p.start()

    parent_conn.send("Hello, Worker!")
    p.join()

if __name__ == "__main__":
    main()

在这个示例中,我们创建了一个Pipe对象,然后在主进程中向队列中添加了一个字符串。然后我们创建了一个子进程,并将队列传给了子进程。

3.3.2 Queue

Queue是一种基于管道和锁的通信方式,可以在多个进程之间进行通信。它是线程安全的,适用于多个生产者和消费者的场景。但是由于使用了锁,它的速度可能比Pipe慢。以下是一个使用Queue进行进程间通信的示例:

python 复制代码
from multiprocessing import Process, Queue

def worker(q):
    print("Worker received: ", q.get())

def main():
    q = Queue()

    p = Process(target=worker, args=(q,))
    p.start()

    q.put("Hello, Worker!")
    p.join()

if __name__ == "__main__":
    main()

3.3.3. Manager

Manager是一种更高级的进程间通信方式,它允许多个进程共享更多种类的数据结构,如列表、字典、集合等。但是由于Manager使用了服务器进程,它的速度可能比PipeQueue都慢。以下是一个使用Manager进行进程间通信的示例:

python 复制代码
from multiprocessing import Process, Manager

def worker(d, key, value):
    d[key] = value

def main():
    with Manager() as manager:
        d = manager.dict()

        p = Process(target=worker, args=(d, 'key', 'Hello, Worker!'))
        p.start()
        p.join()

        print("Main process received: ", d['key'])

if __name__ == "__main__":
    main()

总结:

  • 如果你只需要在两个进程之间进行简单的通信,可以使用Pipe
  • 如果你需要在多个进程之间进行通信,并且需要线程安全的队列,可以使用Queue
  • 如果你需要在多个进程之间共享更复杂的数据结构,可以使用Manager

3.4进程安全

和线程安全同理,在进行多进程编程时,也要注意进程安全。尤其是在使用共享内存或者文件时,你需要确保不同的进程不会同时写入相同的内存或文件。

相关推荐
aiguangyuan几秒前
基于BiLSTM-CRF的命名实体识别模型:原理剖析与实现详解
人工智能·python·nlp
禹凕4 分钟前
Python编程——进阶知识(MYSQL引导入门)
开发语言·python·mysql
Victor3564 分钟前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack5 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo6 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
阿钱真强道6 分钟前
13 JetLinks MQTT:网关设备与网关子设备 - 温控设备场景
python·网络协议·harmonyos
Victor3567 分钟前
MongoDB(3)什么是文档(Document)?
后端
我的xiaodoujiao10 分钟前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 47--设置Selenium以无头模式运行代码
python·学习·selenium·测试工具·pytest
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
寻星探路6 小时前
【深度长文】万字攻克网络原理:从 HTTP 报文解构到 HTTPS 终极加密逻辑
java·开发语言·网络·python·http·ai·https