Python C扩展:多线程与随机数
我在C语言中实现了一个工作队列的模式(是在一个Python扩展里),但是对性能感到很失望。
我有一个模拟程序,里面有一堆粒子(我们叫它们“元素”),我会测量完成所有计算所需的时间,并记录这个时间和参与计算的粒子数量。我是在一台四核的超线程i7处理器上运行这个代码,所以我原本期待随着线程数量增加,性能会提升(也就是所需时间会减少),大约到8个线程的时候应该是最好的。然而,结果却是最快的实现没有使用任何工作线程(函数直接执行,而不是放到队列里),而每增加一个工作线程,代码的运行速度反而越来越慢(每增加一个线程,速度下降的幅度都超过了没有线程时的运行时间!)我简单查看了一下我的处理器使用情况,发现无论运行多少线程,Python的CPU使用率都没有超过130%。而我的机器整体还有很多余量,系统的总使用率大约在200%左右。
在我的队列实现中(下面会展示),我需要从队列中随机选择一个项目,因为每个工作项的执行需要锁定两个元素,而相似的元素在队列中通常会靠得很近。因此,我希望线程能随机选择索引,去处理队列中的不同部分,以减少互斥锁的冲突。
我听说我最开始用rand()
的尝试会很慢,因为我的随机数生成不是线程安全的(这句话听起来有道理吗?我不太确定…)
我尝试过用random()
和drand48_r
来实现(不过不幸的是,后者在OS X上似乎不可用),但统计结果都没有改善。
也许其他人能告诉我问题的原因是什么?下面是代码(工作函数),如果你觉得任何队列添加函数或构造函数也有用,请告诉我。
void* worker_thread_function(void* untyped_queue) {
queue_t* queue = (queue_t*)untyped_queue;
int success = 0;
int rand_id;
long int temp;
work_item_t* work_to_do = NULL;
int work_items_completed = 0;
while (1) {
if (pthread_mutex_lock(queue->mutex)) {
// error case, try again:
continue;
}
while (!success) {
if (queue->queue->count == 0) {
pthread_mutex_unlock(queue->mutex);
break;
}
// choose a random item from the work queue, in order to avoid clashing element mutexes.
rand_id = random() % queue->queue->count;
if (!pthread_mutex_trylock(((work_item_t*)queue->queue->items[rand_id])->mutex)) {
// obtain mutex locks on both elements for the work item.
work_to_do = (work_item_t*)queue->queue->items[rand_id];
if (!pthread_mutex_trylock(((element_t*)work_to_do->element_1)->mutex)){
if (!pthread_mutex_trylock(((element_t*)work_to_do->element_2)->mutex)) {
success = 1;
} else {
// only locked element_1 and work item:
pthread_mutex_unlock(((element_t*)work_to_do->element_1)->mutex);
pthread_mutex_unlock(work_to_do->mutex);
work_to_do = NULL;
}
} else {
// couldn't lock element_1, didn't even try 2:
pthread_mutex_unlock(work_to_do->mutex);
work_to_do = NULL;
}
}
}
if (work_to_do == NULL) {
if (queue->queue->count == 0 && queue->exit_flag) {
break;
} else {
continue;
}
}
queue_remove_work_item(queue, rand_id, NULL, 1);
pthread_mutex_unlock(work_to_do->mutex);
pthread_mutex_unlock(queue->mutex);
// At this point, we have mutex locks for the two elements in question, and a
// work item no longer visible to any other threads. we have also unlocked the main
// shared queue, and are free to perform the work on the elements.
execute_function(
work_to_do->interaction_function,
(element_t*)work_to_do->element_1,
(element_t*)work_to_do->element_2,
(simulation_parameters_t*)work_to_do->params
);
// now finished, we should unlock both the elements:
pthread_mutex_unlock(((element_t*)work_to_do->element_1)->mutex);
pthread_mutex_unlock(((element_t*)work_to_do->element_2)->mutex);
// and release the work_item RAM:
work_item_destroy((void*)work_to_do);
work_to_do = NULL;
work_items_completed++;
success = 0;
}
return NULL;
}
3 个回答
Python中的线程并不是真正的线程。所有的Python线程都在同一个操作系统级别的线程中运行,并且由于有一个叫做GIL(全局解释器锁)的东西,它们是一次一个地执行的。如果你的工作任务比较长,可以考虑用进程来重写你的代码,这样可能会有所帮助。
----编辑----
没错,这段话是关于C语言的。但GIL依然很重要。关于C扩展中线程的信息
要知道这是否是你程序的瓶颈,你需要进行一些性能测试来确认,但这很可能是个问题。
random()
以及类似的函数如果有隐藏的状态变量,可能会成为并行编程中的严重瓶颈。如果它们被设计成线程安全,通常的做法是通过加锁来控制访问,这样会导致整体速度变慢。
在POSIX系统中,适合用作线程安全随机数生成器的选择是 erand48
。与 drand48
不同的是,它需要将状态变量作为参数传入。你只需要在每个线程的栈上保存一个状态变量(它是一个 unsigned short[3]
类型),然后用这个状态变量调用 erand48
。
另外要记住,这些是 伪 随机生成器。如果你在不同的线程之间使用相同的状态变量,那么生成的随机数就不是独立的。
看起来你的问题不是出在random()这个函数上,因为无论线程数量多少,代码都是一样的。随着线程数量增加,性能下降,可能是因为锁的开销太大。你真的需要多个线程吗?你的工作函数执行需要多长时间?平均队列深度是多少?随机选择项目听起来不是个好主意。如果队列中的项目数量小于等于2,那你就完全不需要进行随机计算了。此外,不如为每个工作线程使用一个不同的队列,然后按顺序插入,这样会更好。或者,至少可以简单地记住上一个线程使用的索引,然后就不要选择那个索引。