什么是并发编程和并行编程,他们的区别是什么
- 并发(Concurency):同一时间内管理多个任务的执行,但并不是要求多个任务真正在物理层面上运行。
- 并行(Parallelism): 真正意义的“同时执行”需要多个核心来在同一时刻独自完成。
下面的这个短视频大致会让我们有一个简单的理解。
MythBusters流言终结者使用GPU绘画:蒙娜丽莎
在这个视频里, 流言终结组分别用CPU和GPU控制的机器人完成了一副画,这个动画其实很好的向我们普及了并发和并行的区别。
python是否支持并行开发呢?
关于这个话题, 其实众说纷纭。由于有着全局解释器锁(Global Interpreter Lock, GIL),所以多线程很难实现在CPU密集任务上的并行。所以一部分人不认为python是适合高并发项目。
GIL是什么?
GIL 是 CPython 实现中的一个全局互斥锁,它保证同一时刻只会有一个线程在解释器层面执行 Python 字节码。
• 对于 CPU 密集型任务,多线程因为 GIL 无法得到真正的并行化,所以性能提升有限甚至可能变差。
• 对于 I/O 密集型任务(如网络请求、文件读写),线程经常会因等待 I/O 而主动让出执行权,从而让其他线程有机会运行,这样在总体上能提升程序对 I/O 请求的处理能力。
我们来测试一下多线程。
优点:
• 对 I/O 密集型任务比较有效率,如网络请求、文件读写、数据库访问等。
• 线程启动和切换开销相对进程较小。
缺点:
• 受 GIL 限制,对于 CPU 密集型任务不适合。
• 对共享内存变量需要小心加锁,否则容易出现数据竞争问题。
下面的这张图就可以很好的显示了多线程的工作情况。
样例代码如下:
import threading
import time
import matplotlib.pyplot as plt
import random
# 全局数据结构:记录每个线程的执行区间
# { thread_name: [(start_time, end_time), (start_time, end_time), ...], ...}
execution_intervals = {}
def cpu_bound_task(duration=0.005):
# 一个CPU密集函数(简单版本),做一些无意义的计算以消耗CPU时间
start = time.time()
while time.time() - start < duration:
# 做一些无意义的计算
x = 0
for i in range(1000):
x += i*i
def worker(name, iterations=10):
intervals = []
for _ in range(iterations):
start = time.time()
cpu_bound_task() # 执行CPU密集任务的一小段
end = time.time()
intervals.append((start, end))
# 加一点随机sleep让图表更清晰,以防过于密集挤在一起
time.sleep(random.uniform(0.001, 0.003))
execution_intervals[name] = intervals
if __name__ == "__main__":
threads = []
# 创建5个线程
for i in range(5):
t = threading.Thread(target=worker, args=(f"Thread-{i}", 20))
threads.append(t)
t.start()
for t in threads:
t.join()
# 所有线程完成后绘制甘特图
fig, ax = plt.subplots(figsize=(10, 6))
ax.set_title("Gantt Chart of CPU-bound Multi-thread Execution in Python")
ax.set_xlabel("Time (seconds)")
ax.set_ylabel("Threads")
# 为每个线程分配一个y位置
# 例如 Thread-0 在 y=10, Thread-1 在 y=20, ..., 每个高度为5
height = 5
yticks = []
ylabels = []
base_y = 10
# 为了在图中清晰表示,将y值根据线程索引分层
for idx, (thread_name, intervals) in enumerate(execution_intervals.items()):
y_pos = base_y + idx * (height * 2)
yticks.append(y_pos + height/2)
ylabels.append(thread_name)
# 绘制该线程的执行区间条段
# intervals为[(start, end), ...]
# broken_barh需要((start, width))对形式
bar_data = [(start, end - start) for (start, end) in intervals]
ax.broken_barh(bar_data, (y_pos, height), facecolors="tab:blue")
ax.set_yticks(yticks)
ax.set_yticklabels(ylabels)
ax.grid(True)
plt.tight_layout()
plt.show()
我们来测试一下多进程。
优点:
• 对 I/O 密集型任务比较有效率,如网络请求、文件读写、数据库访问等。
• 线程启动和切换开销相对进程较小。
缺点:
• 受 GIL 限制,对于 CPU 密集型任务不适合。
• 对共享内存变量需要小心加锁,否则容易出现数据竞争问题。
代码如下:
import multiprocessing
import time
import random
def worker(name, start_time):
# 记录进程开始运行的时间(相对于主进程启动所有进程的时间点)
process_start = time.time() - start_time
# 模拟工作负载:这里让进程随机等待0.1到1.0秒不等
delay = random.uniform(0.1, 1.0)
time.sleep(delay)
# 任务结束时间
process_end = time.time() - start_time
# 时间差
elapsed = process_end - process_start
# 打印进程信息
print(f"进程名称: {name}, 启动时间: {process_start:.4f}s, 结束时间: {process_end:.4f}s, 运行时长: {elapsed:.4f}s")
if __name__ == "__main__":
start_time = time.time()
processes = []
for i in range(20):
p = multiprocessing.Process(target=worker, args=(f"Process-{i}", start_time))
processes.append(p)
p.start()
for p in processes:
p.join()
print("所有进程执行完毕。")
最后我们来测试一下python的携程。
协程是一种轻量级的并发实现手段,通过单线程实现高并发的 I/O 操作。在 Python 3.5+ 引入的 async / await 语法,使得编写协程代码更加直观。
• 核心思想是异步 I/O:当协程遇到 I/O 操作(网络请求等)时,不会阻塞当前协程的执行,而是让出控制权给事件循环,让其他协程继续运行。
• 通过这样一种事件驱动机制,可以在单线程下以类似并发的方式执行大量 I/O 密集的异步任务。
)
import asyncio
import time
import matplotlib.pyplot as plt
# 用来存储每个协程任务的执行时间
# 格式: {task_name: (start_time, end_time)}
execution_times = {}
async def io_bound_task(name, start_time):
# 记录任务开始时间(相对于主程序启动的时间)
s = time.time() - start_time
# 模拟IO等待,例如网络请求、文件读写等
# 这里我们用 sleep 来模拟:
# 两次随机等待以体现不同的并发运行点
await asyncio.sleep(0.1) # 模拟前半段IO
await asyncio.sleep(0.2) # 模拟后半段IO
e = time.time() - start_time
execution_times[name] = (s, e)
async def main():
start_time = time.time()
# 创建多个协程任务
tasks = [io_bound_task(f"Task-{i}", start_time) for i in range(5)]
# 并发运行所有任务
await asyncio.gather(*tasks)
asyncio.run(main())
# 所有协程执行完毕后绘图
fig, ax = plt.subplots(figsize=(10, 6))
ax.set_title("Asyncio Coroutines Execution Timeline")
ax.set_xlabel("Time (seconds)")
ax.set_ylabel("Coroutines")
# 我们对5个协程进行绘制,每个协程水平排开
task_names = sorted(execution_times.keys())
height = 0.8
yticks = []
ylabels = []
for idx, tname in enumerate(task_names):
start, end = execution_times[tname]
duration = end - start
y_pos = idx * 2 # 给每个任务留点间隔
ax.broken_barh([(start, duration)], (y_pos, height), facecolors='tab:blue')
yticks.append(y_pos + height/2)
ylabels.append(tname)
ax.set_yticks(yticks)
ax.set_yticklabels(ylabels)
ax.grid(True)
plt.tight_layout()
plt.show()
运行该代码后,你将看到一张图表,X轴为执行时间,Y轴为协程任务名称。每条水平线段代表一个协程在这段时间内执行。从图中你会看到多条线段在时间上重叠,说明在同一时间段内,多个协程任务都在进行。