并发编程是一种让程序可以同时执行多个任务的编程技术。在计算机中,这意味着多个任务在同一时间段内交替进行,但不一定同时进行。并发编程可以通过多线程、多进程和异步编程来实现。
Python对并发编程的支持
Python提供了多种并发编程的方式,包括多线程、多进程和异步编程。每种方式有其适用的场景和优缺点。
多线程是一种并发编程技术,它允许在同一进程中并发执行多个线程。线程是轻量级的子进程,共享同一进程的内存空间和资源,但它们有独立的执行路径和调用栈。通过多线程,程序可以同时处理多个任务,从而提高执行效率。
多线程主要用于以下场景:
Python提供了threading模块,用于创建和管理线程。
继承threading.Thread类:可以通过创建一个子类继承threading.Thread类,并重写run()方法,定义线程要执行的任务。
import threading
class MyThread(threading.Thread):
def run(self):
for i in range(5):
print(f"Thread {self.name} is running, iteration {i}")
# 创建线程实例
thread1 = MyThread()
thread2 = MyThread()
# 启动线程
thread1.start()
thread2.start()
# 等待线程完成
thread1.join()
thread2.join()
直接使用threading.Thread
类:另一种方式是直接创建threading.Thread实例,并传入目标函数和参数。
import threading
def task(name):
for i in range(5):
print(f"Thread {name} is running, iteration {i}")
# 创建并启动线程
thread1 = threading.Thread(target=task, args=("A",))
thread2 = threading.Thread(target=task, args=("B",))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
多进程是一种并发编程技术,它允许程序在多个进程中并发执行任务。每个进程都有独立的内存空间和资源,由操作系统管理。这使得多进程特别适合用于CPU密集型任务,能够充分利用多核CPU的性能。
多线程与多进程的主要区别在于:
Python提供了multiprocessing
模块,用于创建和管理多进程。它的API设计类似于threading模块,使得从多线程编程过渡到多进程编程变得更加容易。
start() | 启动进程 |
join() | 阻塞主进程,直到子进程完成 |
is_alive() | 检查进程是否还在运行 |
terminate() | 强制终止进程 |
使用multiprocessing模块创建进程的两种常见方式:
使用Process类:直接创建Process对象,并传入目标函数和参数
import multiprocessing
def task(name):
print(f"Task {name} is running")
if __name__ == '__main__':
process1 = multiprocessing.Process(target=task, args=('A',))
process2 = multiprocessing.Process(target=task, args=('B',))
process1.start()
process2.start()
process1.join()
process2.join()
继承Process类:通过继承multiprocessing.Process类并重写run()方法来定义进程的任务。
import multiprocessing
class MyProcess(multiprocessing.Process):
def run(self):
print(f"Process {self.name} is running")
if __name__ == '__main__':
process1 = MyProcess()
process2 = MyProcess()
process1.start()
process2.start()
process1.join()
process2.join()
异步编程是一种并发编程的方式,它允许程序在执行耗时任务时不阻塞主线程,从而提高应用程序的性能和响应速度。与传统的多线程或多进程方式不同,异步编程通过事件循环(event loop)来调度任务的执行,使得程序能够在等待I/O操作时处理其他任务。
同步编程:任务按顺序执行,一个任务完成后才能执行下一个任务。如果任务需要等待I/O操作(如文件读写、网络请求),整个程序就会阻塞,直到该操作完成。
多线程编程:通过创建多个线程同时执行多个任务,从而提高并发性能。然而,线程之间共享内存可能导致数据竞争问题,需要复杂的锁机制来确保数据一致性。
异步编程:任务不会阻塞程序,而是通过事件循环管理任务的调度。当一个任务需要等待I/O操作时,事件循环可以执行其他任务。异步编程通常使用 async 和 await 关键字来定义和执行异步任务。
asyncio 是Python的标准库,提供了异步编程的支持。它使得编写异步代码变得更简单和直观,通过 async 和 await 关键字,我们可以轻松定义异步函数并在事件循环中执行。
事件循环是 asyncio 的核心,负责调度和执行异步任务。一个事件循环在运行时,不断检查是否有任务需要执行,并在任务等待I/O操作时切换到其他任务。
import asyncio
async def main():
print("Hello")
await asyncio.sleep(1)
print("World")
# 运行事件循环
asyncio.run(main())
在这个示例中,main 是一个异步函数,通过 await asyncio.sleep(1) 实现了非阻塞的等待。
异步函数使用 async def 来定义,返回一个协程对象(coroutine)。在异步函数中,你可以使用 await 关键字来等待另一个异步操作的完成。
import asyncio
async def fetch_data():
print("Start fetching")
await asyncio.sleep(2) # 模拟耗时操作
print("Done fetching")
return {"data": 123}
async def main():
result = await fetch_data()
print(result)
asyncio.run(main())
await 的作用是暂停当前协程,等待 fetch_data 函数中的异步操作完成,然后继续执行。
并发运行多个任务
asyncio.gather 可以同时运行多个异步任务,并在所有任务完成后返回它们的结果。
import asyncio
async def task(name, delay):
print(f"Task {name} started")
await asyncio.sleep(delay)
print(f"Task {name} finished")
return name
async def main():
results = await asyncio.gather(
task("A", 2),
task("B", 1),
task("C", 3)
)
print(results)
asyncio.run(main())
在这个示例中,asyncio.gather 同时运行三个任务,并在它们完成后返回结果列表。
超时控制
有时,某些任务可能由于某些原因(如网络延迟)而需要花费很长时间。asyncio.wait_for 提供了一个超时机制,可以在超时时间内等待任务完成,否则会抛出 asyncio.TimeoutError。
import asyncio
async def long_task():
await asyncio.sleep(5)
return "Finished"
async def main():
try:
result = await asyncio.wait_for(long_task(), timeout=3)
print(result)
except asyncio.TimeoutError:
print("Task timed out")
asyncio.run(main())
同步与异步代码的混合
在现实项目中,有时需要在异步代码中调用同步代码。asyncio.to_thread 可以将同步的阻塞代码转移到线程池中运行,以避免阻塞事件循环。
import asyncio
import time
def blocking_task():
time.sleep(5)
return "Blocking task result"
async def main():
result = await asyncio.to_thread(blocking_task)
print(result)
asyncio.run(main())
Python也提供了额外的一些函数来提供辅助的能力
在编程中,队列(Queue)是一种常用的数据结构,它遵循先进先出(FIFO, First-In-First-Out)的原则,即第一个进入队列的元素也是第一个被取出的元素。Python 提供了 queue 模块来支持多线程和多进程编程中的队列操作。Queue 是线程和进程安全的,因此在多线程和多进程环境下使用非常方便。
Queue 内部使用锁机制,确保在多线程/多进程环境下操作时不会发生数据竞争,通过 Queue 可以轻松实现线程/进程之间的数据传递,而不需要手动管理复杂的同步机制。
Python 的 queue 模块提供了三个主要的类:
import queue
q = queue.Queue(maxsize=5) # 创建一个最大容量为5的队列
q.put(1) # 向队列中添加元素
q.put(2)
print(q.get()) # 从队列中取出元素,输出: 1
print(q.get()) # 输出: 2
print(q.empty()) # 检查队列是否为空,输出: True
put(item, block=True, timeout=None)
:将 item 放入队列。如果队列已满且 block 为 True,则阻塞等待直到有空间。get(block=True, timeout=None)
:从队列中取出并返回一个元素。如果队列为空且 block 为 True,则阻塞等待直到有可用元素。empty()
:检查队列是否为空。full()
:检查队列是否已满。qsize()
:返回队列中当前元素的数量。LifoQueue 和 PriorityQueue
import queue
# LifoQueue 示例
lq = queue.LifoQueue()
lq.put(1)
lq.put(2)
print(lq.get()) # 输出: 2
# PriorityQueue 示例
pq = queue.PriorityQueue()
pq.put((1, 'task1')) # 元组的第一个元素为优先级,数字越小优先级越高
pq.put((3, 'task3'))
pq.put((2, 'task2'))
print(pq.get()) # 输出: (1, 'task1')
print(pq.get()) # 输出: (2, 'task2')
Queue 在多线程编程中非常有用,特别是用于实现生产者-消费者模式。在这种模式中,生产者线程将任务或数据放入队列,消费者线程从队列中取出任务并处理。
import threading
import queue
import time
def producer(q):
for i in range(5):
item = f'item-{i}'
q.put(item)
print(f'Produced {item}')
time.sleep(1)
def consumer(q):
while True:
item = q.get()
if item is None: # 结束信号
break
print(f'Consumed {item}')
q.task_done()
q = queue.Queue()
producer_thread = threading.Thread(target=producer, args=(q,))
consumer_thread = threading.Thread(target=consumer, args=(q,))
producer_thread.start()
consumer_thread.start()
producer_thread.join()
q.put(None) # 发送结束信号
consumer_thread.join()
在这个示例中,生产者线程不断向队列中添加数据,而消费者线程从队列中取出数据进行处理。队列中的数据处理完毕后,通过发送 None 结束信号通知消费者线程停止。
注意:对于无限大的队列,要小心避免内存耗尽的风险。使用有界队列(设置 maxsize 参数)可以限制队列的最大大小。多进程应用同理。
线程安全指某个函数,函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正常完成。由于线程的执行随时会发生切换,就造成了不可预料的结果,出现线程不安全。为了避免这种情况,Python提供了线程安全锁机制,使得多个线程在访问共享资源时能够相互排斥,从而保证数据的一致性。
锁(Lock)是一种用于管理对共享资源访问的同步原语。它允许一次只有一个线程访问共享资源,其他线程必须等待,直到锁被释放为止。
import threading
lock = threading.Lock()
def critical_section():
with lock:
# 保护共享资源的代码块
print("This is a critical section")
# 在多个线程中使用
thread1 = threading.Thread(target=critical_section)
thread2 = threading.Thread(target=critical_section)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
with lock
: 确保了每次只有一个线程可以进入临界区,防止多个线程同时修改共享资源。
在多线程编程中,创建和销毁线程是需要一定开销的,特别是在高频率地创建和销毁线程时,这种开销可能会显著影响程序的性能。线程池(Thread Pool)是一种优化机制,它通过提前创建好一组可复用的线程来管理并发任务的执行,避免了频繁创建和销毁线程的开销。
在Python中,concurrent.futures
模块提供了 ThreadPoolExecutor 类来实现线程池。ThreadPoolExecutor 是一个非常方便的工具,它可以管理一个固定数量的线程,并提供简单的方法来提交任务和获取结果。
from concurrent.futures import ThreadPoolExecutor
使用 ThreadPoolExecutor 可以轻松创建线程池并提交任务。
from concurrent.futures import ThreadPoolExecutor
import time
def task(name):
print(f"Task {name} is running")
time.sleep(2)
return f"Task {name} completed"
# 创建一个包含3个线程的线程池
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(task, i) for i in range(5)]
for future in futures:
print(future.result())
在这个示例中,我们创建了一个包含3个线程的线程池,并提交了5个任务。由于线程池最多只允许3个线程同时运行,所以这5个任务会分批执行。submit 方法将任务提交给线程池,返回一个 Future 对象。Future 表示异步执行的结果,可以通过 result() 方法获取任务的返回值。
ThreadPoolExecutor 提供了几种重要的方法来管理线程和任务:
submit(fn, *args, **kwargs)
:提交一个任务到线程池中执行,并返回一个 Future 对象。map(func, *iterables)
:类似于内置的 map() 函数,但它会将 func 分发到多个线程中并发执行。shutdown(wait=True)
:停止线程池,不再接受新的任务。wait=True 表示等待所有线程执行完毕再关闭。from concurrent.futures import ThreadPoolExecutor
def square(n):
return n * n
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(square, range(10))
for result in results:
print(result)
map 方法将一个可迭代对象中的每个元素传递给指定的函数,并将结果按顺序返回。与 submit 不同,map 是阻塞的,即它会等待所有任务执行完成后返回结果。
处理大量的网络请求
import requests
from concurrent.futures import ThreadPoolExecutor
urls = [
'https://www.example.com',
'https://www.python.org',
'https://www.github.com',
# 添加更多的URL
]
def fetch(url):
response = requests.get(url)
return url, response.status_code
with ThreadPoolExecutor(max_workers=5) as executor:
results = executor.map(fetch, urls)
for url, status in results:
print(f"{url} returned status code {status}")
并行处理文件
import os
from concurrent.futures import ThreadPoolExecutor
def process_file(file_path):
with open(file_path, 'r') as f:
content = f.read()
return file_path, len(content)
file_paths = [f"./files/file{i}.txt" for i in range(10)]
with ThreadPoolExecutor(max_workers=3) as executor:
results = executor.map(process_file, file_paths)
for file_path, length in results:
print(f"{file_path} has {length} characters")
如有疑问,欢迎评论区留言!