哈喽,各位好!今天咱们来聊聊C++里那些跟CPU“谈恋爱”的技巧:sched_setaffinity
和SetThreadAffinityMask
。这两个家伙听起来挺高大上,其实就是让你的程序线程指定在哪个CPU核心上跑,说白了就是“霸占”CPU资源!
咱们先从基础说起,然后慢慢深入,最后搞点高级应用。准备好了吗?Let’s go!
一、为啥要搞CPU亲和性?(Why Bother?)
想象一下,你是一个繁忙的厨师(程序),厨房里有很多灶台(CPU核心)。如果你到处乱窜,一会儿用这个灶台,一会儿用那个灶台,是不是效率不高?因为每次切换灶台,你都要搬运食材(数据),还得适应新的温度(缓存)。
CPU亲和性就是让你固定在一个或几个灶台上,减少切换的损耗,提高效率。主要有以下几个好处:
- 提升性能: 减少上下文切换带来的开销,尤其是在多线程、高并发的场景下效果显著。
- 降低延迟: 某些对延迟非常敏感的应用(比如实时音视频处理、高性能计算),固定在特定核心上可以减少抖动。
- 隔离任务: 将不同类型的任务分配到不同的CPU核心上,避免相互干扰。例如,可以将UI线程和计算线程分开,保证UI的流畅性。
- NUMA优化: 在NUMA(Non-Uniform Memory Access)架构下,将线程分配到靠近其访问内存的CPU核心上,减少内存访问延迟。
二、主角登场:sched_setaffinity
和 SetThreadAffinityMask
这两个函数分别用于Linux/Unix-like系统和Windows系统,用来设置线程的CPU亲和性。
1. sched_setaffinity
(Linux/Unix-like)
-
头文件:
#include <sched.h>
-
函数原型:
int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);
-
参数:
pid
: 线程ID。如果是当前线程,可以传0。cpusetsize
:mask
的大小,通常是sizeof(cpu_set_t)
。mask
: 一个cpu_set_t
类型的位掩码,用来指定允许线程运行的CPU核心。
-
返回值: 成功返回0,失败返回-1,并设置
errno
。
cpu_set_t
是什么?
cpu_set_t
是一个位图,每一位代表一个CPU核心。例如,如果设置了第0位和第2位,就表示线程可以在CPU 0和CPU 2上运行。
代码示例:
#define _GNU_SOURCE // 必须定义这个宏才能使用CPU_SET等宏
#include <iostream>
#include <sched.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
void* thread_func(void* arg) {
int core_id = *(int*)arg;
cpu_set_t cpuset;
CPU_ZERO(&cpuset); // 初始化cpu_set_t
CPU_SET(core_id, &cpuset); // 设置指定核心
pthread_t thread = pthread_self();
int s = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
if (s != 0) {
std::cerr << "pthread_setaffinity_np error: " << strerror(s) << std::endl;
return nullptr;
}
std::cout << "Thread " << pthread_self() << " is running on core " << core_id << std::endl;
// 模拟一些工作
for (int i = 0; i < 100000000; ++i) {
// do something
}
return nullptr;
}
int main() {
pthread_t thread1, thread2;
int core1 = 0;
int core2 = 1;
pthread_create(&thread1, nullptr, thread_func, &core1);
pthread_create(&thread2, nullptr, thread_func, &core2);
pthread_join(thread1, nullptr);
pthread_join(thread2, nullptr);
return 0;
}
代码解释:
#define _GNU_SOURCE
: 这个宏必须定义,才能使用CPU_SET
等宏。CPU_ZERO(&cpuset)
: 初始化cpu_set_t
,将所有位都设置为0。CPU_SET(core_id, &cpuset)
: 设置指定核心的位,例如CPU_SET(0, &cpuset)
表示允许线程在CPU 0上运行。pthread_setaffinity_np
: 这个函数是pthread库提供的,用来设置线程的CPU亲和性。注意,它不是POSIX标准,所以名字里有个_np
(non-portable)。pthread_self()
: 获取当前线程ID。
重要提示:
sched_setaffinity
通常需要root权限才能设置其他进程的亲和性。- 如果指定的CPU核心不存在,
sched_setaffinity
会返回错误。 - 在多线程程序中,最好使用
pthread_setaffinity_np
来设置线程的亲和性,而不是直接使用sched_setaffinity
。
2. SetThreadAffinityMask
(Windows)
-
头文件:
#include <windows.h>
-
函数原型:
DWORD_PTR SetThreadAffinityMask(HANDLE hThread, DWORD_PTR dwThreadAffinityMask);
-
参数:
hThread
: 线程句柄。可以用GetCurrentThread()
获取当前线程的句柄。dwThreadAffinityMask
: 一个位掩码,用来指定允许线程运行的CPU核心。
-
返回值: 成功返回线程之前的亲和性掩码,失败返回0。
代码示例:
#include <iostream>
#include <windows.h>
#include <process.h> // For _beginthreadex
unsigned __stdcall thread_func(void* arg) {
DWORD_PTR mask = (DWORD_PTR)arg;
DWORD_PTR result = SetThreadAffinityMask(GetCurrentThread(), mask);
if (result == 0) {
std::cerr << "SetThreadAffinityMask error: " << GetLastError() << std::endl;
return 1;
}
std::cout << "Thread " << GetCurrentThreadId() << " affinity mask set to: " << mask << std::endl;
// 模拟一些工作
for (int i = 0; i < 100000000; ++i) {
// do something
}
return 0;
}
int main() {
HANDLE thread1, thread2;
DWORD threadID1, threadID2;
DWORD_PTR core1Mask = 0x1; // CPU 0
DWORD_PTR core2Mask = 0x2; // CPU 1
thread1 = (HANDLE)_beginthreadex(nullptr, 0, thread_func, (void*)core1Mask, 0, (unsigned*)&threadID1);
thread2 = (HANDLE)_beginthreadex(nullptr, 0, thread_func, (void*)core2Mask, 0, (unsigned*)&threadID2);
WaitForSingleObject(thread1, INFINITE);
WaitForSingleObject(thread2, INFINITE);
CloseHandle(thread1);
CloseHandle(thread2);
return 0;
}
代码解释:
GetCurrentThread()
: 获取当前线程的句柄。SetThreadAffinityMask
: 设置线程的CPU亲和性掩码。0x1
(CPU 0),0x2
(CPU 1): 使用十六进制表示位掩码。0x1
表示二进制00000001
,只有第0位是1,所以表示CPU 0。0x2
表示二进制00000010
,只有第1位是1,所以表示CPU 1。_beginthreadex
: Windows下创建线程的函数。
重要提示:
- Windows的CPU核心编号从0开始。
SetThreadAffinityMask
返回的是之前的亲和性掩码,可以用来恢复之前的设置。
三、高级应用:NUMA优化
NUMA(Non-Uniform Memory Access)是一种多处理器架构,其中每个处理器都有自己的本地内存,访问本地内存的速度比访问其他处理器的内存快得多。
问题: 如果线程运行在一个CPU核心上,但频繁访问另一个CPU核心的本地内存,性能会很差。
解决方案: 将线程分配到靠近其访问内存的CPU核心上。
如何确定哪个CPU核心靠近哪个内存?
这取决于你的硬件和操作系统。可以使用一些工具来获取这些信息,例如:
- Linux:
numactl --hardware
- Windows:
GetNumaNodeProcessorMask
函数
示例(Linux):
假设numactl --hardware
输出如下:
available: 2 nodes (0-1)
node 0 cpus: 0 2 4 6
node 0 size: 8117 MB
node 1 cpus: 1 3 5 7
node 1 size: 8192 MB
这表示有两个NUMA节点:
- 节点0的CPU核心:0, 2, 4, 6
- 节点1的CPU核心:1, 3, 5, 7
如果你的线程主要访问节点0上的内存,就应该将它分配到CPU核心0, 2, 4, 6中的一个。
代码示例(Linux):
#define _GNU_SOURCE
#include <iostream>
#include <sched.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <numa.h> // 引入numa.h头文件
void* thread_func(void* arg) {
int numa_node = *(int*)arg;
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
// 获取指定NUMA节点上的CPU核心列表
unsigned long long nodemask = 1ULL << numa_node; // 创建一个nodemask,指定NUMA节点
struct bitmask* cpumask = numa_allocate_cpumask(); // 分配一个bitmask来存储CPU核心列表
if (cpumask == nullptr) {
std::cerr << "numa_allocate_cpumask error" << std::endl;
return nullptr;
}
if (numa_node_to_cpus(numa_node, cpumask) != 0) {
std::cerr << "numa_node_to_cpus error" << std::endl;
numa_free_cpumask(cpumask);
return nullptr;
}
// 遍历NUMA节点上的CPU核心,并添加到cpuset中
for (int i = 0; i < numa_num_possible_cpus(); ++i) {
if (numa_bitmask_isbitset(cpumask, i)) {
CPU_SET(i, &cpuset);
std::cout << "Adding CPU core " << i << " to cpuset for NUMA node " << numa_node << std::endl;
}
}
numa_free_cpumask(cpumask); // 释放bitmask
pthread_t thread = pthread_self();
int s = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
if (s != 0) {
std::cerr << "pthread_setaffinity_np error: " << strerror(s) << std::endl;
return nullptr;
}
std::cout << "Thread " << pthread_self() << " is running on NUMA node " << numa_node << std::endl;
// 模拟一些工作
for (int i = 0; i < 100000000; ++i) {
// do something
}
return nullptr;
}
int main() {
if (numa_available() == -1) {
std::cerr << "NUMA is not available on this system" << std::endl;
return 1;
}
pthread_t thread1, thread2;
int numa_node1 = 0;
int numa_node2 = 1;
pthread_create(&thread1, nullptr, thread_func, &numa_node1);
pthread_create(&thread2, nullptr, thread_func, &numa_node2);
pthread_join(thread1, nullptr);
pthread_join(thread2, nullptr);
return 0;
}
代码解释:
#include <numa.h>
: 引入numa.h
头文件,使用libnuma库。需要安装libnuma库。numa_available()
: 检查系统是否支持NUMA。numa_node_to_cpus(numa_node, cpumask)
: 获取指定NUMA节点上的CPU核心列表,存储到cpumask
中。numa_bitmask_isbitset(cpumask, i)
: 检查cpumask
中第i
位是否被设置,如果被设置,表示CPU核心i
属于该NUMA节点。
注意: NUMA优化是一个复杂的话题,需要根据具体的应用场景和硬件环境进行调整。
四、性能测试与注意事项
光说不练假把式,设置了CPU亲和性之后,一定要进行性能测试,看看是否真的有提升。
性能测试工具:
- Linux:
perf
,top
,htop
- Windows:
Performance Monitor
,Resource Monitor
注意事项:
- 不要过度优化: CPU亲和性不是万能的,过度优化可能会导致其他问题,比如线程饥饿。
- 考虑任务的特点: 对于IO密集型的任务,CPU亲和性的效果可能不明显。
- 动态调整: 在某些情况下,可能需要根据系统负载动态调整CPU亲和性。
- 确保CPU核心数量足够: 如果你的CPU核心数量不够,设置CPU亲和性可能会导致性能下降。
- 验证亲和性是否生效: 使用工具(例如
top
)验证线程是否真的运行在指定的CPU核心上。
五、总结
sched_setaffinity
和SetThreadAffinityMask
是C++中设置CPU亲和性的利器。通过合理地使用它们,可以提高程序的性能、降低延迟、隔离任务,并进行NUMA优化。但是,一定要记住,CPU亲和性不是银弹,需要根据具体的应用场景和硬件环境进行调整,并通过性能测试来验证效果。
希望今天的讲解对你有所帮助!记住,编程就像谈恋爱,需要耐心和技巧,才能让你的程序和CPU“幸福地在一起”!下次再见!