【问题标题】:Set CPU affinity when create a thread创建线程时设置 CPU 亲和性
【发布时间】:2014-08-30 00:26:36
【问题描述】:

我想创建一个 C++11 线程,我希望它在我的第一个内核上运行。我发现pthread_setaffinity_npsched_setaffinity可以改变一个线程的CPU亲和性,并将其迁移到指定的CPU上。但是,在线程运行后,此亲和性规范会发生变化。

如何创建具有特定 CPU 亲和性的 C++11 线程(cpu_set_t 对象)?

如果在初始化 C++11 线程时无法指定亲和性,如何在 C 中使用 pthread_t

我的环境是 Ubuntu 上的 G++。一段代码表示赞赏。

【问题讨论】:

  • 很抱歉,我认为 C++11 不支持这一点(可能是由于可移植性问题)——你可能不得不放弃 std::thread 并使用 pthread_create 和你的属性启动线程已经准备好使用pthread_attr_setaffinity_np,或者使用std::thread 并让创建的线程立即设置自己的亲和性(避免您尝试从创建线程设置它时遇到的竞争条件)。
  • @TonyD 非常感谢。我在答案中添加了代码。希望这是您的建议。
  • 看起来不错……干杯。

标签: c++ c multithreading c++11 pthreads


【解决方案1】:

我很抱歉在这里成为“神话终结者”,但设置线程亲和性非常重要,随着我们所有人使用的系统本质上变得越来越 NUMA(非统一内存架构),它的重要性随着时间的推移而变得越来越重要. 如今,即使是普通的双插槽服务器也将 RAM 单独连接到每个插槽,并且从插槽到其自己的 RAM 与相邻处理器插槽(远程 RAM)的内存访问差异很大。在不久的将来,处理器将进入市场,其中内部内核集本身就是 NUMA(用于不同内核组的独立内存控制器等)。 这里不需要我重复别人的工作,网上找“NUMA和线程亲和”就可以了——你可以借鉴其他工程师多年的经验。

不设置线程关联实际上等于“希望”操作系统调度程序能够正确处理线程关联。让我解释: 您有一个带有一些 NUMA 节点(处理和内存域)的系统。您启动一个线程,该线程使用内存做一些事情,例如malloc 一些内存,然后处理等。现代操作系统(至少是 Linux,其他可能也是)到目前为止做得很好,默认情况下,内存是从线程运行的 CPU 的同一域分配的(如果可用) . 到时候,分时操作系统(所有现代操作系统)将使线程进入睡眠状态。当线程重新进入运行状态时,它可能会在系统中的 any 内核上运行(因为您没有为其设置关联掩码),并且您的系统越大,它在远离其先前分配或使用的内存的 CPU 上被“唤醒”的机会就越高。现在,您的所有内存访问都将是远程的(不确定这对您的应用程序性能意味着什么?在线阅读有关 NUMA 系统上的远程内存访问的更多信息)

因此,总而言之,当在具有非同寻常的体系结构的系统上运行代码时,亲和性设置接口非常重要——如今,这种体系结构正迅速成为“任何系统”。一些线程运行时环境/库允许在运行时对此进行控制,而无需任何特定编程(参见 OpenMP,例如在英特尔的 KMP_AFFINITY 环境变量的实现中)——对于 C++11 实现者来说,在其中包含类似的机制是正确的他们的运行时库和语言选项(在此之前,如果您的代码旨在在服务器上使用,我强烈建议您在代码中实现亲和力控制)

【讨论】:

  • +1 Windows 是一个很好的例子,说明“希望操作系统能正确处理”可能会出错。至少在 Win7 下(我没有尝试过 8 或 10 个)线程最终分配了一个首选的核心循环。这非常简单,在 95% 的情况下都足够好。但剩下的 5% 真的很苦。
  • 这仅适用于具有易于预测的工作负载的流程。您需要更加聪明才能击败近期的调度器及其优化器,因为它们越来越有可能从 AI 的最新进展中获得性能。
  • 这似乎无法回答问题。
【解决方案2】:

是的,有办法做到这一点。我在这个博客link

上遇到了这种方法

我重写了 Eli Bendersky 博客上的代码,链接贴在上面。 您可以将下面的代码保存到 test.cpp 并编译并运行它:

 // g++ ./test.cpp  -lpthread && ./a.out
// 
#include <thread>
#include <vector>
#include <iostream>
#include <mutex>
#include <sched.h>
#include <pthread.h>
int main(int argc, const char** argv) {
  constexpr unsigned num_threads = 4;
  // A mutex ensures orderly access to std::cout from multiple threads.
  std::mutex iomutex;
  std::vector<std::thread> threads(num_threads);
  for (unsigned i = 0; i < num_threads; ++i) {
    threads[i] = std::thread([&iomutex, i,&threads] {
      // Create a cpu_set_t object representing a set of CPUs. Clear it and mark
      // only CPU i as set.
      cpu_set_t cpuset;
      CPU_ZERO(&cpuset);
      CPU_SET(i, &cpuset);
      int rc = pthread_setaffinity_np(threads[i].native_handle(),
                                      sizeof(cpu_set_t), &cpuset);
      if (rc != 0) {
        std::cerr << "Error calling pthread_setaffinity_np: " << rc << "\n";
      }
      std::this_thread::sleep_for(std::chrono::milliseconds(20));
      while (1) {
        {
          // Use a lexical scope and lock_guard to safely lock the mutex only
          // for the duration of std::cout usage.
          std::lock_guard<std::mutex> iolock(iomutex);
          std::cout << "Thread #" << i << ": on CPU " << sched_getcpu() << "\n";
        }

        // Simulate important work done by the tread by sleeping for a bit...
        std::this_thread::sleep_for(std::chrono::milliseconds(900));
      }
    });


  }

  for (auto& t : threads) {
    t.join();
  }
  return 0;
}

【讨论】:

    【解决方案3】:

    在 C++ 11 中,您无法在创建线程时设置线程亲和性(除非在线程中运行的函数自己执行此操作),但一旦创建线程,您可以通过任何本机设置亲和性通过获取线程的本机句柄 (thread.native_handle()) 来获得接口,因此对于 Linux,您可以通过以下方式获取 pthread id:

    pthread_t my_thread_native = my_thread.native_handle();

    然后,您可以使用任何传入 my_thread_native 的 pthread 调用,它需要 pthread 线程 ID。

    请注意,大多数线程工具都是特定于实现的,即 pthreads、windows 线程、其他操作系统的本地线程都有自己的接口和类型,这部分代码的可移植性不是很好。

    【讨论】:

      【解决方案4】:

      找了一会,好像我们在创建C++thread的时候不能设置CPU亲和性。

      原因是,创建线程时不需要指定亲和性。那么,为什么要在语言中让它成为可能。

      假设我们希望将工作负载f() 绑定到 CPU0。我们可以通过调用pthread_setaffinity_np 将亲和性更改为就在真正的工作负载之前。

      但是,我们可以在 C 中创建线程时指定亲和性。(感谢 Tony D 的评论)。 例如下面的代码输出“Hello pthread”。

      void *f(void *p) {
        std::cout<<"Hello pthread"<<std::endl;
      }
      
      cpu_set_t cpuset;
      CPU_ZERO(&cpuset);
      CPU_SET(0, &cpuset);
      pthread_attr_t pta;
      pthread_attr_init(&pta);
      pthread_attr_setaffinity_np(&pta, sizeof(cpuset), &cpuset);
      pthread_t thread;
      if (pthread_create(&thread, &pta, f, NULL) != 0) {
          std::cerr << "Error in creating thread" << std::endl;
      }
      pthread_join(thread, NULL);
      pthread_attr_destroy(&pta);
      

      【讨论】:

      • 错误答案。您正在使用粗体大写来对用例做出过于宽泛的断言。 pthread_setaffinity_np 改变当前运行进程的线程,导致不必要的上下文切换。
      • @ACyclic,你是什么意思“改变当前运行进程的线程”?另外,请注意,从手册页: pthread_setaffinity_np() 函数将线程线程的 CPU 亲和掩码设置为 cpuset 指向的 CPU 集
      • "原因是,创建线程时不需要指定亲和性。所以,为什么要在语言中让它成为可能。"...很好玩。
      • @aho 我会澄清一个用例。在诸如嵌入式或实时优先级系统之类的系统中,抢先式多线程是不可能的或不可取的。此 C++ 线程 api 要求线程在与生成进程相同的硬件核心/超线程上启动(从技术上讲,它不允许您在启动线程之前预先指定 CPU 亲和性)。因此,它假设了抢先式多线程的可用性。如果当前线程以实时优先级运行,则新线程将永远不会启动。如果此限制不适用于您,请使用 C++ 线程 API。
      猜你喜欢
      • 2018-05-19
      • 2013-04-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-10-22
      • 1970-01-01
      • 1970-01-01
      • 2020-09-07
      相关资源
      最近更新 更多