CPU 亲和性
Linux 可以运行在多处理器的机器上,为了维持多个CPU之间的负载均衡,线程可能会被OS调度到其它CPU上,这种情况下线程就无法利用原先CPU上边的缓存了,也就降低了CPU cache的命中率了。所谓的CPU亲和性,就是让线程在指定的CPU上长时间运行而不被调度到其它CPU上边,以提高CPU cache的命中率。
在Linux中,可以使用pthread_setaffinity_np()
为线程设置CPU的亲和性:
1
2
|
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,
const cpu_set_t *cpuset);
|
注意到,它的第一个参数类型是pthread_t
,代表 Linux 线程的ID。在C++中,每个std::thread
都对应着底层操作系统的一个线程,不过我们可以使用std::thread
的native_handle()
函数来返回它对应的 OS 线程的ID,在Linux系统中,这个函数的返回值类型是pthread_t
。
下面的例子,我们让线程绑定到 CPU-0 上:
#include <iostream> #include <thread> #include <chrono> #include <sched.h> int main() { std::thread t([]() { while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout << "Thread #" << std::this_thread::get_id() << ": on CPU" << sched_getcpu() << std::endl; } }); cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(0, &cpuset); pthread_setaffinity_np(t.native_handle(), sizeof(cpu_set_t), &cpuset); t.join(); return 0; }
获取语义的开销
为了测量memory_order_acquire的开销,我在3 个不同的多核处理器中编译运行以上例子。对于每个架构,我选择对c++11原子支持最好的编译器。你们将在GitHub上找到完整的代码。
让我们来看一下读-获取附近代码产生的机器码:
g = Guard.load(memory_order_acquire); if (g != 0) p = Payload;
Intel x86-64
在Intel x86-64上,Clang编译器给这个例子产生了紧凑的机器码--每行C++代码对应一条机器指令。这一处理器家族采用强内存模型,所以编译器不需要放置特定有内存栅栏以实现读-获取。只需要保证机器指令的顺序正确就行。
PowerPC
PowerPC是弱排序CPU,这就意味着编译器在多核系统中必须放置内存栅栏指令以保证获取语义。在这个例子中,GCC使用了这里推荐的由3个指令组成的一串指令:cmp;bne;isync。(单个指令lwsync也可以完成相同的工作)
ARMv7
ARM也是弱排序CPU,所以编译器在多核系统中也必须放置内存栅栏指令以保证获取语义。在ARMv7中,dmb ish是最合适的指令,尽管也是一个内存栅栏。
如下就是我们例子的主循环在测试机器上每循环一次的计时:
在PowerPC和ARMv7上,内存栅栏指令造成的性能惩罚,但它们对正确运行是必须的。事实上,如果你从ARMv7机器码中删除dmb ish指令,同时保留其它指令,在iPhone 4S上内存重排序能被直接观察到。
对于memory order多线程环境中(CPU也是乱序执行的)的几种规定如下下表,
Value | Explanation |
---|---|
memory_order_relaxed | 对其它读写操作没有同步,只保证本操作是原子的 |
memory_order_consume | load操作,当前线程依赖该原子变量的访存操作不能reorder到该指令之前,对其他线程store操作(release)可见 |
memory_order_acquire | load操作,当前线程所有访存操作不能reorder到该指令之前,对其他线程store操作(release)可见 |
memory_order_release | store操作,当前线程所有访存操作不能reorder到该指令之后,对其他线程load操作(consume)可见 |
memory_order_acq_rel | load/store操作,memory_order_acquire + memory_order_release |
memory_order_seq_cst | memory_order_acq_rel + 顺序一致性(sequential consisten) |
memory_order_relaxed: 只保证当前操作的原子性,不考虑线程间的同步,其他线程可能读到新值,也可能读到旧值。比如 C++ shared_ptr 里的引用计数,我们只关心当前的应用数量,而不关心谁在引用谁在解引用。
memory_order_release:(可以理解为 mutex 的 unlock 操作)
memory_order_acquire: (可以理解为 mutex 的 lock 操作)
注意这是使用了relaxed、release和acquire三种约束。relaxed只保证修改顺序,所以对于write()函数来说,一定是先执行x后执行y操作。不过若是将y也使用relaxed,虽然在write()中是先x后y的顺序,而在read()的眼中,可能是先y后x的顺序,这是优化导致的。而因为y的读和写使用了acquire和release约束,所以可以保证在不同线程间对于相同的原子变量读和写的操作顺序一致。
root@ubuntu:/data1# cat test9.cpp #include <thread> #include <atomic> #include <cassert> #include <string> std::atomic<bool> x{false}, y{false}; std::atomic<int> z{0}; void write() { // relaxed只保证修改顺序 x.store(true, std::memory_order_relaxed); // release保证在它之前的所有写操作顺序一致 y.store(true, std::memory_order_release); } void read() { // acquire保证在它之前和之后的读操作顺序一致 while(!y.load(std::memory_order_acquire)); // relaxed只保证修改顺序 if(x.load(std::memory_order_relaxed)) ++z; } int main() { std::thread t1(write); std::thread t2(read); t1.join(); t2.join(); assert(z.load() != 0); return 0; } root@ubuntu:/data1#
root@ubuntu:/data1# g++ -g -Wall -std=c++11 -pthread test9.cpp -o test9 root@ubuntu:/data1# ./test9 root@ubuntu:/data1#
1. Relaxed ordering: 在单个线程内,所有原子操作是顺序进行的。按照什么顺序?基本上就是代码顺序(sequenced-before)。这就是唯一的限制了!两个来自不同线程的原子操作是什么顺序?两个字:任意。
2. Release -- acquire: 来自不同线程的两个原子操作顺序不一定?那怎么能限制一下它们的顺序?这就需要两个线程进行一下同步(synchronize-with)。同步什么呢?同步对一个变量的读写操作。线程 A 原子性地把值写入 x (release), 然后线程 B 原子性地读取 x 的值(acquire). 这样线程 B 保证读取到 x 的最新值。注意 release -- acquire 有个牛逼的副作用:线程 A 中所有发生在 release x 之前的写操作,对在线程 B acquire x 之后的任何读操作都可见!本来 A, B 间读写操作顺序不定。这么一同步,在 x 这个点前后, A, B 线程之间有了个顺序关系,称作 inter-thread happens-before.
https://www.zhihu.com/question/24301047
https://www.codedump.info/post/20191214-cxx11-memory-model-2/
https://jishuin.proginn.com/p/763bfbd358bf