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进程安全

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

相关推荐
AskHarries13 分钟前
Java字节码增强库ByteBuddy
java·后端
海阔天空_201326 分钟前
Python pyautogui库:自动化操作的强大工具
运维·开发语言·python·青少年编程·自动化
佳佳_27 分钟前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
零意@34 分钟前
ubuntu切换不同版本的python
windows·python·ubuntu
思忖小下1 小时前
Python基础学习_01
python
q567315231 小时前
在 Bash 中获取 Python 模块变量列
开发语言·python·bash
是萝卜干呀1 小时前
Backend - Python 爬取网页数据并保存在Excel文件中
python·excel·table·xlwt·爬取网页数据
代码欢乐豆1 小时前
数据采集之selenium模拟登录
python·selenium·测试工具
许野平2 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
狂奔solar2 小时前
yelp数据集上识别潜在的热门商家
开发语言·python