Python基础教程:多线程编程

1,087 阅读7分钟

进程(process)指的是正在运行的程序的实例,当我们执行某个程序时,进程就被操作系统创建了。而线程(thread)则包含于进程之中,是操作系统能够进行运算调度的最小单元,多个线程可以同处一个进程中,且同时处理不同的任务。一条进程中可以并发多个线程,而同一条线程将共享该进程中的全部系统资源。

每个进程都有自己独立的地址空间、内存和数据栈,因此进程之间通讯不方便,所以需使用用进程间通讯(InterProcess Communication, IPC)。而同一个进程中的线程共享资源,因此线程间通讯非常方便,只需注意数据同步与互斥的问题。

Python支持多线程编程和多进程编程。在本教程中,我们将学习有关Python多线程编程的基础知识、线程同步、线程池以及如何使用多进程。

1. 多线程基础

线程是进程内的执行单元,每个进程都至少有一个线程。Python标准库提供了thread模块和threading模块来支持线程编程。在Python3中,thread模块已经改名为_thread,并在_thread基础上开发了更为强大的threading模块,因此我们可以使用threading.Thread类创建线程对象,并通过start()方法启动线程。

import threading

def worker():
    print("I am a thread.")

t = threading.Thread(target=worker)
t.start()

在上面的代码中,我们创建了一个名为worker的函数,它是线程的工作内容。我们使用threading.Thread类创建了一个线程对象t,并将worker作为参数传递给了Thread构造函数。然后,我们调用t.start()方法来启动线程。当我们运行这段代码时,会看到以下输出:

I am a thread.

另外,我们也可以通过继承Thread类并重写run方法来实现线程的创建

import threading

class MyThread(threading.Thread):
	def __init__(self, n):
		self.n = n
		super().__init__() # 调用父类函数初始化线程
	def run(self):
		print('线程:', self.n)
for i in range(1,3):
	t = MyThread(i)
	t.start()

结果应该输出打印出:

线程1
线程2

2. 多线程同步

当多个线程同时访问共享资源时,可能会发生竞态条件。为避免这种情况,我们需要使用线程同步机制。Python提供了锁(Lock)、信号量(Semaphore)、事件(Event)等机制来实现线程同步。

下面是一个使用锁来确保线程同步的示例:

import threading

lock = threading.Lock()

count = 0

def worker():
    global count
    lock.acquire()
    try:
        count += 1
    finally:
        lock.release()

threads = []
for i in range(10):
    t = threading.Thread(target=worker)
    threads.append(t)

for t in threads:
    t.start()

for t in threads:
    t.join()

print(count)

在上面的代码中,我们定义了一个全局变量count和一个锁对象lockworker函数是每个线程的工作内容,它通过lock来确保count的修改是原子性的。我们创建了10个线程并启动它们,等待所有线程执行完毕后输出count的值。当我们运行这段代码时,会看到以下输出:

10

如果把锁去掉后,count的值会是几呢?动手试试吧!会有意向不到的结果哦~

3. 线程池

线程池可以用来提高代码的性能,在需要大量线程处理任务时,可以使用线程池来减少线程的创建和销毁次数,提高线程的复用率。

Python标准库提供了concurrent.futures模块来支持线程池编程。我们可以使用ThreadPoolExecutor类来创建和管理线程池。

下面是一个使用线程池来处理任务的示例:

from concurrent.futures import ThreadPoolExecutor

def worker(num):
    return num * num

executor = ThreadPoolExecutor(max_workers=4)

results = []
for i in range(10):
    result = executor.submit(worker, i)
    results.append(result)

for result in results:
    print(result.result())

在上面的代码中,我们定义了一个worker函数用来处理任务。我们创建了一个ThreadPoolExecutor对象executor,并将最大工作线程数设置为4。然后,我们使用submit方法提交10个任务给executor,并将结果保存在results列表中。最后,我们遍历results列表并输出每个任务的结果。

看到这些个熟悉的单词...恍惚间有种写java代码的感觉。

python多线程

4. 多进程编程

进程是计算机中程序执行的基本单位,而线程则是进程中执行代码的单位。多进程编程就是利用多个独立运行的进程来完成任务。相比单进程,多进程有以下优点:

  • 提高了程序的并发性能。
  • 可以更好地利用多核CPU。
  • 能够处理更大的数据集。

在进行多进程编程时,需要注意进程之间的通信和同步问题。

5. Python实现多进程编程

Python标准库中的multiprocessing模块提供了实现多进程编程的工具。其中常用的方法包括:

  • Process:创建一个新进程。
  • Pool:创建一组进程池。
  • Queue:进程之间的消息队列。

下面是一个简单的示例,展示如何使用multiprocessing模块创建子进程:

import multiprocessing

def worker():
    """子进程要执行的任务"""
    print('子进程正在运行')

if __name__ == '__main__':
    p = multiprocessing.Process(target=worker)
    p.start()
    print('主进程已经结束')

在这个例子中,我们创建一个新的进程,并将其target属性指向worker函数。然后使用start方法启动该进程。注意到在Windows系统中,需要将启动进程的代码放在if __name__ == '__main__':语句内,以避免出现一些问题。

5.1 Python进程池

import multiprocessing

def worker(num):
    """子进程要执行的任务"""
    print(f'子进程{num}正在运行')

if __name__ == '__main__':
    with multiprocessing.Pool(processes=4) as pool:
        results = pool.map(worker, range(4))
        print(results)

在这个例子中,我们使用了进程池来管理多个子进程。首先创建一个Pool对象,其中processes参数指定了进程池的大小。然后使用map方法提交任务,该方法会自动分配任务给空闲的子进程并返回结果。最后打印输出结果。

5.2 在进程之间进行通信

import multiprocessing

def writer(q):
    """写入数据"""
    for i in range(10):
        q.put(i)

def reader(q):
    """读取数据"""
    while True:
        item = q.get()
        if item is None:
            break
        print(item)

if __name__ == '__main__':
    q = multiprocessing.Queue()

    # 启动子进程
    p1 = multiprocessing.Process(target=writer, args=(q,))
    p2 = multiprocessing.Process(target=reader, args=(q,))
    p1.start()
    p2.start()

    # 等待子进程结束
    p1.join()
    q.put(None)
    p2.join()

在这个例子中,我们创建了一个消息队列,并通过Queue对象将其传递给两个子进程。一个子进程负责将数据写入队列中,而另一个子进程则从队列中读取数据并输出到屏幕上。注意到在程序末尾我们向队列中添加了None元素,以便通知读取数据的子进程结束循环。

6.多进程计算圆周率

下面是使用Python多进程和蒙特卡罗(Monte Carlo)方法估计圆周率的示例代码:

import random
from multiprocessing import Pool

def estimate_pi(n):
    num_points_inside_circle = 0
    for _ in range(n):
        x = random.uniform(-1, 1)
        y = random.uniform(-1, 1)
        if x**2 + y**2 <= 1:
            num_points_inside_circle += 1
    return 4 * num_points_inside_circle / n

if __name__ == '__main__':
    num_processes = 4
    num_samples = int(1e8)
    with Pool(num_processes) as p:
        results = p.map(estimate_pi, [int(num_samples/num_processes)] * num_processes)
    print(sum(results)/num_processes)

该代码使用random模块中的uniform函数生成在[1,1][-1, 1]范围内均匀分布的随机数,并统计落入以原点为圆心,半径为1的圆中的点数。最后,将所有子进程的结果相加并除以进程数以得到圆周率的估计值。我算出的结果是3.1416664800000005, 你们呢?

其中,Pool(num_processes) 创建一个具有num_processes个进程的进程池,p.map(estimate_pi, [int(num_samples/num_processes)] * num_processes) 在每个进程上调用estimate_pi函数,并将输入参数设为所需的样本数量的四分之一,然后返回其结果。

7.总结

在Python多线程与进程编程中,我们不仅要理解和掌握的关于threading 模块和multiprocessing模块如何使用,更要掌握的是线程和进程之间的区别和联系以及线程通信、进程通信的方式。

上一篇教程:Python基础教程:正则表达式

Python提供了丰富的多线程和多进程编程工具,使我们能够更有效地利用计算机的多核心处理能力。线程同步和线程池等同步原语可以帮助我们避免竞争条件,并提高程序性能。使用好多线程和多进程,可以使Python编程变得更加高效和灵活。Python爬虫应用中,经常会大量使用线程和进程来提升抓取效率哦