【问题标题】:Why are background, idle threads slowing the main thread?为什么后台空闲线程会减慢主线程的速度?
【发布时间】:2022-01-01 23:04:06
【问题描述】:

我在一台装有 Apple M1 的笔记本电脑中,它有 4 个内核。我有一个问题可以通过动态生成数百万线程来并行解决。由于pthread_create 的开销很大,因此我通过在后台保留3 个线程来避免它。这些线程等待任务到达:

void *worker(void *arg) {
  u64 tid = (u64)arg;

  // Loops until the main thread signals work is complete
  while (!atomic_load(&done)) {

    // If there is a new task for this worker...
    if (atomic_load(&workers[tid].stat) == WORKER_NEW_TASK) {

      // Execute it
      execute_task(&workers[tid]);

    }

  }

  return 0;
}

这些线程使用pthread_create 生成一次:

pthread_create(&workers[tid].thread, NULL, &normal_thread, (void*)tid)

任何时候我需要完成一个新任务,而不是再次调用pthread_create,我只是选择一个空闲的工作人员并将任务发送给它:

workers[tid].stat = WORKER_NEW_TASK
workers[tid].task = ...

问题是:出于某种原因,将这 3 个线程留在后台会使我的主线程慢 25%。由于我的 CPU 有 4 个内核,我希望这 3 个线程根本不会影响主线程。

为什么后台线程会拖慢主线程?我做错什么了吗? while (!atomic_load(&done)) 循环是否消耗大量 CPU 资源?

【问题讨论】:

  • 您假设调度程序会将您的四个线程上的处理器亲和性设置为仅在选定的内核上运行。调度程序可以/将把活动推送到它想要的任何核心,核心交换机并不便宜(也不免费)。至于做错什么,除非您有非常充分的理由在原子标志上自旋循环,否则您最好使用 cvar+mutex 对来管理为您的队列服务的工作人员。
  • 我预计这个问题会被否决(因为根据我目前的知识,我真的无法让它变得更好),但我仍然很高兴我问了这个问题。 @WhozCraig 感谢您的提示,我会研究这个方向。 “除非你有充分的理由在原子标志上旋转循环,否则使用 cvar+mutex 可能会更好” - 这不是你可以在互联网上轻松找到的知识,真的需要专业知识。这不就是 SO 的用途吗?
  • 多线程编程确实是一个“专长”的东西,可以肯定的是,尤其是在 C 语言中。关于 POSIX 线程的学生文本将对如何实现 cvar+mutex 工作组系统有很好的参考. Butenhof 的“使用 POSIX 线程编程”是我 22 年前学习的文本(我的图书馆书架上仍然有该文本)。我相信网上也有很多例子;我只是更喜欢翻页而不是点击鼠标按钮。
  • 嗯,俄亥俄州立大学目前正被犹他州淘汰,所以我现在有点心事重重。我会看看我是否可以在网上找到一些例子。毕竟,这是元旦。如果您真的想获得半运行的内容,则可以在配置线程时始终尝试推挤core affinity。正如我所说,就我个人而言,旋转等待的概念让我很反感,但有时需要它(例如无锁范例)。
  • @MaiaVictor 让您的线程以阻塞方式等待工作完成,而不是消耗 CPU 周期。您可以使用 pthread 条件变量来实现。

标签: c multithreading pthreads threadpool


【解决方案1】:

问题是我有一个这样的线程:

typedef struct {
  atomic_int has_work;
} Worker;

Worker workers[MAX_THREADS];

void *worker(void *arg) {
  u64 tid = (u64)arg;
  while (1) {
    if (atomic_load(workers[tid].has_work)) {
      // do stuff
    }
  }
}

(...)

注意我是如何使用原子标志has_work 来“唤醒”工人的。正如@WhozCraig 在他的评论中指出的那样,旋转查看原子标志可能不是一个好主意。如果我理解正确的话,这是计算密集型的,它压倒了 CPU。他的建议是使用带条件变量的互斥锁,如 Butenhof 的“使用 POSIX 线程编程”第 3.3 节所述。 This 堆栈溢出答案有一个 sn-p 显示该模式的常见用法。生成的代码应如下所示:

typedef struct {
  int             has_work;
  pthread_mutex_t has_work_mutex;
  pthread_cond_t  has_work_signal;
}

Worker workers[MAX_THREADS];

void *worker(void *arg) {
  u64 tid = (u64)arg;
  while (1) {

    pthread_mutex_lock(&workers[tid].has_work_mutex);

    while (!workers[tid].has_work) {
      pthread_cond_wait(&workers[tid].has_work_signal, &workers[tid].has_work_mutex);
    }

    // do stuff

    pthread_mutex_unlock(&workers[tid].has_work_mutex);
  }
  return 0;
}

注意has_work 是如何被替换为使用普通的int 而不是原子的mutex 保护它。然后,“条件变量”与该互斥锁相关联。这允许我们使用pthread_cond_wait 让线程休眠,直到另一个线程发出“has_work”可能为真的信号。这与使用原子的版本具有相同的效果,但对 CPU 的占用更少,性能应该更好。

【讨论】:

  • 做得很好。这正是我所描述的道路。您可以通过使用单个条件变量 + 互斥锁对和一个队列来使其更加健壮,并让所有工作线程监视该 cvar 的信号,然后在互斥锁的保护下检查队列并弹出一个工作项如果存在。如果没有工作(可能发生),则返回等待。无论如何,您所拥有的一切都有效,而且您会发现它对 CPU 更加友好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-02-13
  • 1970-01-01
  • 2011-01-15
  • 1970-01-01
  • 1970-01-01
  • 2023-03-21
  • 1970-01-01
相关资源
最近更新 更多