Python C扩展:多线程与随机数

0 投票
3 回答
621 浏览
提问于 2025-04-17 07:20

我在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 个回答

0

Python中的线程并不是真正的线程。所有的Python线程都在同一个操作系统级别的线程中运行,并且由于有一个叫做GIL(全局解释器锁)的东西,它们是一次一个地执行的。如果你的工作任务比较长,可以考虑用进程来重写你的代码,这样可能会有所帮助。

关于GIL的维基百科页面

----编辑----

没错,这段话是关于C语言的。但GIL依然很重要。关于C扩展中线程的信息

0

要知道这是否是你程序的瓶颈,你需要进行一些性能测试来确认,但这很可能是个问题。

random() 以及类似的函数如果有隐藏的状态变量,可能会成为并行编程中的严重瓶颈。如果它们被设计成线程安全,通常的做法是通过加锁来控制访问,这样会导致整体速度变慢。

在POSIX系统中,适合用作线程安全随机数生成器的选择是 erand48。与 drand48 不同的是,它需要将状态变量作为参数传入。你只需要在每个线程的栈上保存一个状态变量(它是一个 unsigned short[3] 类型),然后用这个状态变量调用 erand48

另外要记住,这些是 随机生成器。如果你在不同的线程之间使用相同的状态变量,那么生成的随机数就不是独立的。

0

看起来你的问题不是出在random()这个函数上,因为无论线程数量多少,代码都是一样的。随着线程数量增加,性能下降,可能是因为锁的开销太大。你真的需要多个线程吗?你的工作函数执行需要多长时间?平均队列深度是多少?随机选择项目听起来不是个好主意。如果队列中的项目数量小于等于2,那你就完全不需要进行随机计算了。此外,不如为每个工作线程使用一个不同的队列,然后按顺序插入,这样会更好。或者,至少可以简单地记住上一个线程使用的索引,然后就不要选择那个索引。

撰写回答