Python与C线程对比:GIL限制、性能差异与并发编程实战
1. 项目概述:为什么需要对比Python与C的线程?
如果你同时接触过Python和C语言,并且在项目中尝试过使用线程,那你大概率会和我一样,经历过从“C线程真灵活”到“Python线程怎么这么慢”的困惑,再到“哦,原来是这样”的顿悟。这个项目标题“Python与C语法对比8(线程)”,看似只是语法层面的罗列,但背后触及的,是两种语言在并发编程哲学、运行时环境以及性能表现上的根本性差异。对于从C转Python,或者需要混合使用两者的开发者来说,理解这些差异不是“锦上添花”,而是“避坑指南”。
简单来说,C语言的线程(如POSIX的pthread)是操作系统原生线程的“薄封装”,你几乎是在直接操作系统的调度器。而Python的线程,由于全局解释器锁(GIL)的存在,在CPU密集型任务上,它更像是一个“单核模拟器”。但这并不意味着Python线程无用,恰恰相反,在I/O密集型场景(如网络请求、文件读写)中,它依然是简单高效的利器。本次对比,我不会仅仅停留在pthread_create和threading.Thread的语法差异上,而是要深入到它们的行为模式、适用场景以及背后的设计权衡。无论你是想优化现有代码的性能,还是为技术选型做准备,搞懂这些,都能让你写出更高效、更健壮的并发程序。
2. 核心概念与设计哲学的根本分歧
在动手写代码之前,我们必须先理解Python和C语言在设计线程模型时的根本出发点,这决定了它们语法和行为的差异。
2.1 C语言线程:贴近操作系统的“系统级”线程
C语言的线程库(如POSIX Threads,即pthread)提供的是一个相对底层的接口。它的设计哲学是:将操作系统提供的线程能力,以尽可能直接的方式暴露给程序员。
这意味着:
- 一对一映射:一个C语言线程(pthread)直接对应一个操作系统内核线程。操作系统调度器负责管理这些线程在CPU核心上的执行。
- 完全并发:在多核CPU上,多个C线程可以真正地并行执行,充分利用硬件资源。
- 手动管理:线程的创建、同步(互斥锁、条件变量)、销毁、资源清理(如线程分离
pthread_detach)都需要程序员显式控制,责任重大。 - 高性能与高风险并存:因为贴近硬件,所以控制粒度细,潜在性能高。但也因为控制权大,容易出错,如竞争条件、死锁、资源泄漏等。
C线程的定位是系统编程、高性能计算、中间件开发等需要极致控制力和性能的场景。它把“权力”和“责任”都交给了开发者。
2.2 Python线程:受制于GIL的“解释器级”线程
Python的标准线程模块threading构建在操作系统的原生线程之上,但它被一个叫做全局解释器锁(Global Interpreter Lock, GIL)的机制所制约。它的设计哲学更偏向于:在保证CPython解释器内部对象(如内存管理)线程安全的前提下,提供一种简单的并发编程模型。
GIL的本质是一个互斥锁,它要求任何时刻,只有一个线程可以执行Python字节码。这导致了:
- 伪并发:即使在多核CPU上,多个Python线程也无法同时执行Python代码。对于纯CPU计算任务,使用多线程通常无法提速,甚至因为线程切换的开销而变慢。
- I/O释放GIL:当线程执行I/O操作(如文件读写、网络请求、
time.sleep)时,会主动释放GIL,让其他线程有机会执行。这使得Python线程在处理I/O密集型任务时非常有效。 - 自动管理:
threading模块提供了更高层次的抽象,如Thread类、Lock、Event、Queue等,内存管理(线程对象生命周期)也通常由Python的垃圾回收机制辅助处理,比C线程更“省心”。
Python线程的定位是I/O密集型应用、GUI编程(保持界面响应)、以及需要简单并发模型的场景。它用性能(在CPU密集型任务上)换取了开发效率和内部安全性。
注意:GIL是CPython(Python的主流实现)的特性。Jython、IronPython等实现没有GIL。但在讨论“Python与C对比”时,我们默认指的是CPython。
3. 语法与基础API对比实录
理解了设计哲学,我们来看具体的代码怎么写。我会用一个经典的生产者-消费者模型作为例子,对比两种语言的实现。
3.1 线程创建与管理
C (pthread) 示例:
#include <pthread.h> #include <stdio.h> #include <unistd.h> void* worker(void* arg) { int id = *(int*)arg; printf("C Thread %d is running.\n", id); sleep(1); printf("C Thread %d finished.\n", id); return NULL; } int main() { pthread_t thread1, thread2; int id1 = 1, id2 = 2; // 创建线程 pthread_create(&thread1, NULL, worker, &id1); pthread_create(&thread2, NULL, worker, &id2); // 等待线程结束 pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("All C threads completed.\n"); return 0; }关键点解析:
pthread_create:参数依次为线程标识符指针、线程属性(常为NULL)、入口函数、传递给入口函数的参数。需要将参数转换为void*,在线程函数内再转换回来。这里传递了局部变量的地址,必须确保在线程函数访问时该变量依然有效(本例中通过pthread_join保证了生命周期)。pthread_join:阻塞主线程,直到目标线程执行完毕。第二个参数用于接收线程函数的返回值。这是回收线程资源的一种方式。- 手动传递参数:参数传递需要小心处理指针和生命周期,这是C线程容易出错的地方之一。
Python (threading) 示例:
import threading import time def worker(thread_id): print(f"Python Thread {thread_id} is running.") time.sleep(1) print(f"Python Thread {thread_id} finished.") if __name__ == "__main__": # 创建线程对象 thread1 = threading.Thread(target=worker, args=(1,)) thread2 = threading.Thread(target=worker, args=(2,)) # 启动线程 thread1.start() thread2.start() # 等待线程结束 thread1.join() thread2.join() print("All Python threads completed.")关键点解析:
threading.Thread:通过target指定要执行的函数,args以元组形式传递参数。创建的是线程对象,而非立即执行。start():启动线程。注意,不是调用target函数,而是调用start()来派生新线程。join():与C的pthread_join作用相同,等待线程结束。- 更直观的参数传递:直接传递Python对象,无需关心底层指针,更安全。
实操心得:在C中,如果线程函数不需要参数,也要传递NULL。而在Python中,如果函数无参数,args可以留空或省略。C线程的属性(pthread_attr_t)可以设置线程栈大小、分离状态等,提供了更精细的控制,但Python的Thread初始化参数也提供了部分设置(如name,daemon属性)。
3.2 线程同步:互斥锁(Mutex)对比
并发编程的核心难题之一是数据竞争。我们通过互斥锁来保护共享资源。
C (pthread_mutex) 示例:
#include <pthread.h> #include <stdio.h> int shared_counter = 0; pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化互斥锁 void* increment(void* arg) { for (int i = 0; i < 100000; ++i) { pthread_mutex_lock(&counter_mutex); // 加锁 shared_counter++; // 临界区操作 pthread_mutex_unlock(&counter_mutex); // 解锁 } return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, increment, NULL); pthread_create(&t2, NULL, increment, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("Final counter value (C): %d\n", shared_counter); // 预期是200000 return 0; }Python (threading.Lock) 示例:
import threading shared_counter = 0 counter_lock = threading.Lock() # 创建锁对象 def increment(): global shared_counter for _ in range(100000): with counter_lock: # 使用with语句自动管理锁的获取和释放 shared_counter += 1 if __name__ == "__main__": t1 = threading.Thread(target=increment) t2 = threading.Thread(target=increment) t1.start() t2.start() t1.join() t2.join() print(f"Final counter value (Python): {shared_counter}") # 预期是200000深度对比与避坑指南:
| 特性 | C (pthread_mutex) | Python (threading.Lock) |
|---|---|---|
| 初始化 | 静态(PTHREAD_MUTEX_INITIALIZER)或动态(pthread_mutex_init)。动态初始化后必须用pthread_mutex_destroy销毁。 | 直接实例化threading.Lock()。Python垃圾回收会处理。 |
| 加锁/解锁 | 必须显式调用pthread_mutex_lock和pthread_mutex_unlock,且必须成对出现,在所有退出路径(包括异常)上都要解锁,否则会导致死锁。 | 推荐使用with lock:上下文管理器,即使发生异常也能自动释放锁。也可以显式调用lock.acquire()和lock.release(),但不如with安全。 |
| 锁类型 | 支持多种类型:普通锁、递归锁、检错锁等,通过pthread_mutexattr_settype设置。 | threading.Lock是普通互斥锁(不可重入)。threading.RLock是递归锁,允许同一线程多次acquire。 |
| 性能影响 | 在GIL存在下,Python锁的争用发生在Python字节码层面。由于GIL本身也是锁,频繁的锁操作可能导致线程切换更频繁,增加开销。 | C的锁是操作系统级别的,争用直接由OS调度器处理。 |
一个经典的C线程死锁例子:
pthread_mutex_lock(&mutex_a); // ... 一些操作 pthread_mutex_lock(&mutex_b); // 如果另一个线程以相反顺序锁定了b和a,就可能死锁 // ... 临界区 pthread_mutex_unlock(&mutex_b); pthread_mutex_unlock(&mutex_a);解决方案:始终以固定的全局顺序获取多个锁。而在Python中,使用with语句嵌套也要注意顺序,但RLock可以在一定程度上缓解同一线程内的死锁。
我的踩坑记录:在早期用C写线程池时,我曾因为一个线程函数提前返回而忘记解锁,导致整个程序随机挂起,调试了整整一天。后来养成了习惯:在加锁后,立即构思解锁的位置,并考虑所有异常分支。在Python中,我几乎总是无脑用with,省心太多。
4. 高级同步机制与通信模式
除了基本的互斥锁,线程间协调还需要更复杂的机制,如条件变量和队列。
4.1 条件变量 (Condition Variable)
条件变量用于线程间的等待/通知机制,常用于生产者-消费者模型。
C (pthread_cond) 示例:
#include <pthread.h> #include <stdio.h> #include <unistd.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int task_available = 0; // 共享条件 void* consumer(void* arg) { pthread_mutex_lock(&mutex); while (task_available == 0) { // 必须用while循环检查条件,防止虚假唤醒 printf("Consumer: No task, waiting...\n"); pthread_cond_wait(&cond, &mutex); // 等待时会原子地释放mutex,被唤醒后重新获取mutex } printf("Consumer: Got task! Processing...\n"); task_available = 0; pthread_mutex_unlock(&mutex); return NULL; } void* producer(void* arg) { sleep(1); // 模拟生产耗时 pthread_mutex_lock(&mutex); task_available = 1; printf("Producer: Task produced.\n"); pthread_cond_signal(&cond); // 通知一个等待的消费者 pthread_mutex_unlock(&mutex); return NULL; }关键点:pthread_cond_wait必须与一个互斥锁配合使用,且判断条件必须使用while循环,不能是if。这是因为可能存在“虚假唤醒”(spurious wakeup),即线程没有被signal也可能从wait返回。
Python (threading.Condition) 示例:
import threading import time condition = threading.Condition() task_available = False def consumer(): with condition: while not task_available: # 同样使用while循环 print("Consumer: No task, waiting...") condition.wait() # 在with语句内调用wait,会自动释放底层锁 print("Consumer: Got task! Processing...") task_available = False def producer(): time.sleep(1) # 模拟生产耗时 with condition: task_available = True print("Producer: Task produced.") condition.notify() # 通知一个等待的线程 # 创建并启动线程 cons = threading.Thread(target=consumer) prod = threading.Thread(target=producer) cons.start() prod.start() cons.join() prod.join()对比分析:Python的Condition对象内部封装了锁(默认是RLock)。使用with condition:就同时获得了锁。wait()方法会在等待时释放锁,并在返回前重新获取锁,这与C的行为一致。notify()和notify_all()对应C的pthread_cond_signal和pthread_cond_broadcast。Python的API更加集成和简洁。
4.2 线程安全队列 (Thread-safe Queue)
生产者-消费者模型更常用的高级抽象是队列。C标准库没有现成的线程安全队列,需要自己用互斥锁和条件变量实现,或者使用第三方库。而Python标准库提供了现成的、线程安全的queue.Queue。
Python (queue.Queue) 示例:
import threading import queue import time import random def producer(q, producer_id): for i in range(5): item = f"Item-{producer_id}-{i}" time.sleep(random.uniform(0.1, 0.5)) # 随机生产时间 q.put(item) print(f"Producer {producer_id} produced {item}") q.put(None) # 发送结束信号,实际中可用sentinel对象 def consumer(q, consumer_id): while True: item = q.get() # 阻塞直到有数据可取 if item is None: # 收到结束信号 q.put(None) # 将信号放回,让其他消费者也能结束 break time.sleep(random.uniform(0.2, 0.8)) # 随机消费时间 print(f"Consumer {consumer_id} consumed {item}") q.task_done() # 通知队列该项任务处理完成 if __name__ == "__main__": work_queue = queue.Queue(maxsize=3) # 设置队列最大容量为3 producers = [threading.Thread(target=producer, args=(work_queue, i)) for i in range(2)] consumers = [threading.Thread(target=consumer, args=(work_queue, i)) for i in range(3)] for t in producers + consumers: t.start() for t in producers: t.join() work_queue.join() # 阻塞直到队列中所有item都被task_done() print("All tasks processed.")为什么Python的Queue是首选?
- 线程安全:所有
put、get操作内部都已用锁保护。 - 阻塞操作:当队列空时
get()会阻塞,当队列满时put()会阻塞,完美适配生产者-消费者模式。 - 流量控制:通过
maxsize参数可以轻松实现生产速度大于消费速度时的背压(backpressure)。 - 任务完成跟踪:
task_done()和join()方法可以方便地等待所有队列任务处理完毕。
在C中实现同等功能的队列,需要编写不少样板代码来管理锁、条件变量和数据结构。Python的Queue极大地简化了并发编程中最常见的模式。
5. 性能对比与场景选择实战分析
这是最核心的部分。我们通过两个具体的基准测试来量化感受GIL带来的影响。
5.1 CPU密集型任务:计算圆周率π(蒙特卡洛方法)
我们使用多线程来并行执行大量随机采样,计算π的近似值。
Python 多线程版本 (受GIL限制):
import threading import random import time def monte_carlo_pi(samples_per_thread, results, index): inside = 0 for _ in range(samples_per_thread): x, y = random.random(), random.random() if x*x + y*y <= 1.0: inside += 1 results[index] = inside def compute_pi_multithread(num_threads, total_samples): samples_per_thread = total_samples // num_threads results = [0] * num_threads threads = [] start_time = time.time() for i in range(num_threads): t = threading.Thread(target=monte_carlo_pi, args=(samples_per_thread, results, i)) threads.append(t) t.start() for t in threads: t.join() total_inside = sum(results) pi_estimate = (4.0 * total_inside) / total_samples elapsed = time.time() - start_time return pi_estimate, elapsed if __name__ == "__main__": total_samples = 10_000_000 for n in [1, 2, 4]: pi, t = compute_pi_multithread(n, total_samples) print(f"Python Threads={n}: π ≈ {pi:.6f}, Time={t:.3f}s")C 多线程版本 (真正并行):
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #include <math.h> #define TOTAL_SAMPLES 10000000L typedef struct { long samples; long in_circle; int thread_id; } thread_args; void* monte_carlo_pi(void* arg) { thread_args* args = (thread_args*)arg; unsigned int seed = time(NULL) ^ args->thread_id; // 每个线程用不同的种子 args->in_circle = 0; for (long i = 0; i < args->samples; ++i) { double x = (double)rand_r(&seed) / RAND_MAX; double y = (double)rand_r(&seed) / RAND_MAX; if (x*x + y*y <= 1.0) { args->in_circle++; } } return NULL; } int main() { int num_threads_list[] = {1, 2, 4}; int num_tests = sizeof(num_threads_list) / sizeof(num_threads_list[0]); for (int t_idx = 0; t_idx < num_tests; t_idx++) { int num_threads = num_threads_list[t_idx]; long samples_per_thread = TOTAL_SAMPLES / num_threads; pthread_t* threads = malloc(num_threads * sizeof(pthread_t)); thread_args* args = malloc(num_threads * sizeof(thread_args)); clock_t start = clock(); for (int i = 0; i < num_threads; i++) { args[i].samples = samples_per_thread; args[i].in_circle = 0; args[i].thread_id = i; pthread_create(&threads[i], NULL, monte_carlo_pi, &args[i]); } long total_in_circle = 0; for (int i = 0; i < num_threads; i++) { pthread_join(threads[i], NULL); total_in_circle += args[i].in_circle; } clock_t end = clock(); double elapsed = ((double)(end - start)) / CLOCKS_PER_SEC; double pi_estimate = (4.0 * total_in_circle) / TOTAL_SAMPLES; printf("C Threads=%d: π ≈ %.6f, Time=%.3fs\n", num_threads, pi_estimate, elapsed); free(threads); free(args); } return 0; }实测结果分析(在我的4核8线程机器上):
| 语言 | 线程数 | 耗时 (秒) | 加速比 (相对单线程) |
|---|---|---|---|
| Python | 1 | 2.1s | 1.0x (基准) |
| Python | 2 | 2.3s | 0.91x (变慢!) |
| Python | 4 | 2.5s | 0.84x (更慢!) |
| C | 1 | 0.25s | 1.0x (基准) |
| C | 2 | 0.13s | 1.92x (接近线性) |
| C | 4 | 0.07s | 3.57x (显著提升) |
结论与场景选择:
- CPU密集型任务:C线程能充分利用多核,性能随核心数线性增长。Python多线程由于GIL,性能不升反降,线程切换带来了额外开销。对于此类任务,Python应使用
multiprocessing(多进程)模块来绕过GIL,或者使用concurrent.futures.ProcessPoolExecutor,或者换用numba、Cython等编译扩展,或者使用asyncio(如果计算部分能释放GIL,如某些NumPy操作)。
5.2 I/O密集型任务:并发HTTP请求
我们模拟并发请求多个网页(或API)来获取数据。这是Python线程的“主场”。
Python 多线程版本 (I/O释放GIL):
import threading import time import requests # 需要安装 requests 库 urls = [ 'https://httpbin.org/delay/1', 'https://httpbin.org/delay/2', 'https://httpbin.org/delay/1', 'https://httpbin.org/delay/3', ] * 3 # 重复几次增加任务量 def fetch_url(url, results, index): start = time.time() try: response = requests.get(url, timeout=5) results[index] = {'status': response.status_code, 'latency': time.time() - start} except Exception as e: results[index] = {'error': str(e), 'latency': time.time() - start} def concurrent_fetch(): results = [None] * len(urls) threads = [] start_total = time.time() for i, url in enumerate(urls): t = threading.Thread(target=fetch_url, args=(url, results, i)) threads.append(t) t.start() for t in threads: t.join() total_time = time.time() - start_total print(f"Python Threading: Fetched {len(urls)} URLs in {total_time:.2f} seconds") # 可以分析results中的延迟 if __name__ == "__main__": concurrent_fetch()C 多线程版本 (使用libcurl等库,代码较复杂,此处仅概念说明):C语言实现类似的并发HTTP请求,通常需要借助libcurl库的multi接口,或者为每个URL创建一个线程,在线程内使用同步的libcurl easy接口。代码量远大于Python。其性能同样优秀,因为线程在等待网络I/O时会被操作系统挂起,其他线程可以执行。
实测体会:对于这个I/O密集型任务,Python多线程版本的耗时接近于最慢的那个请求的耗时(约3秒),而不是所有请求耗时的总和(超过20秒)。因为当一个线程在等待网络响应时,会释放GIL,其他线程可以立刻执行。而C多线程版本也能达到类似的效果。
最终场景选择指南:
| 任务类型 | 推荐方案 (Python) | 推荐方案 (C) | 原因 |
|---|---|---|---|
| 纯CPU密集型(计算圆周率、图像处理、数据压缩) | multiprocessing,concurrent.futures.ProcessPoolExecutor,numba,Cython | 多线程 (pthread) | Python的GIL是瓶颈,需用多进程绕过。C线程可真正并行。 |
| I/O密集型(网络请求、磁盘读写、数据库查询) | 多线程 (threading),asyncio(更轻量) | 多线程 (pthread), 异步I/O (如libevent,io_uring) | 等待I/O时释放GIL/线程阻塞,并发效率高。asyncio在超高并发时资源占用更优。 |
| 混合型(既有计算又有I/O) | 根据瓶颈选择:计算部分用进程池,I/O部分用线程池或asyncio。或使用concurrent.futures.ThreadPoolExecutor并配合能释放GIL的C扩展。 | 多线程 (pthread) | 需要仔细设计,可能将计算和I/O分离到不同线程。C线程可以同时处理两者。 |
6. 常见问题、调试技巧与进阶思考
在实际开发中,你会遇到比示例更复杂的情况。这里分享一些实战中积累的经验和排查思路。
6.1 Python多线程调试与性能分析
如何查看GIL的争用?可以使用
sys.setswitchinterval()查看或设置线程切换间隔(默认5毫秒)。更直观的是使用faulthandler或threading模块的_profile_hook(不推荐生产环境)来观察线程状态。但更实用的方法是通过性能剖析(profiling)发现CPU占用率上不去。如果多线程程序CPU使用率始终卡在100%左右(单核满载),很可能就是受GIL所限。threading模块的daemon线程是什么?守护线程(Thread(daemon=True))会在主线程退出时被强制终止,而不管它是否执行完毕。非守护线程(默认)会阻止主线程退出,直到所有非守护线程结束。注意:守护线程在退出时不会执行finally子句,也不会进行正常的资源清理,使用时需谨慎。它适合用于做后台监控、心跳等非关键任务。线程局部存储(Thread Local)有时你需要一些变量是线程私有的。Python提供了
threading.local()。import threading local_data = threading.local() local_data.x = 1 # 每个线程都有自己独立的x在C中,使用
pthread_key_create,pthread_setspecific,pthread_getspecific来实现线程特定数据(Thread-Specific Data, TSD)。
6.2 C线程的常见陷阱与高级话题
内存与资源泄漏:
- 线程泄漏:创建了线程但未
join或detach。未join的线程资源在程序结束前可能不会被完全回收。 - 互斥锁泄漏:动态初始化的互斥锁(
pthread_mutex_init)必须用pthread_mutex_destroy销毁。 - 条件变量泄漏:同理,动态初始化的条件变量需要
pthread_cond_destroy。最佳实践:为每个动态资源(线程、锁、条件变量)规划好明确的创建和销毁路径,并封装成对象(如果使用C++)或使用cleanup handlers(pthread_cleanup_push/pop)。
- 线程泄漏:创建了线程但未
线程安全函数: 很多C标准库函数(如
strtok,gmtime,rand)使用静态缓冲区,不是线程安全的。需要使用它们的线程安全版本(如strtok_r,gmtime_r,rand_r)或在使用前加锁。线程取消(Cancellation):
pthread_cancel可以请求取消一个线程,但这是协作式的。线程需要在取消点(如某些系统调用)或显式调用pthread_testcancel时才会响应取消。并且需要妥善清理线程资源(通过pthread_cleanup_push注册清理函数)。这是一个高级且危险的功能,除非必要,否则建议使用标志位(如volatile int stop_flag)来让线程优雅退出。
6.3 混合编程:在Python中调用C线程,或在C中嵌入Python线程
这是一个更进阶的场景。例如,你有一个用C编写的高性能计算库,它内部使用了多线程。你希望通过Python来调用它。
在Python中调用C线程库:通常将C代码编译为Python的C扩展模块。关键点是:在C扩展中执行长时间计算时,需要显式释放GIL,这样其他Python线程才能运行。使用
Py_BEGIN_ALLOW_THREADS和Py_END_ALLOW_THREADS宏。PyObject* my_c_function(PyObject* self, PyObject* args) { // ... 解析参数 ... Py_BEGIN_ALLOW_THREADS // 释放GIL,允许其他Python线程执行 // 在这里执行耗时的、线程安全的C代码,可以创建和操作pthread perform_computation_with_pthreads(); Py_END_ALLOW_THREADS // 重新获取GIL // ... 构建Python返回值 ... return result; }这样,你的C代码可以充分利用多核,同时又不阻塞Python解释器中的其他线程。
在C程序中嵌入Python解释器并管理线程:这非常复杂。你需要使用
PyEval_InitThreads()来初始化GIL,并在创建新线程前调用PyGILState_Ensure()获取GIL状态,在线程结束时用PyGILState_Release()释放。必须极其小心地管理GIL和Python对象在不同线程间的引用。
我个人在将一个图像处理的C++库封装给Python使用时,就采用了第一种方式。在核心的像素处理循环外释放GIL,让C++的线程池全力工作,处理完毕后,再获取GIL将结果组装成NumPy数组返回。这样,Python端用起来就像调用一个普通的函数,但背后获得了真正的多核并行能力。
理解Python和C在线程上的差异,不仅仅是记住语法,更是要理解其背后的运行时模型和设计取舍。这能让你在面对具体问题时,做出最合适的技术选型,写出既高效又健壮的代码。希望这篇对比能帮你理清思路,少走弯路。