CPU 亲和性
Linux 可以运行在多处理器的机器上,为了维持多个CPU之间的负载均衡,线程可能会被OS调度到其它CPU上,这种情况下线程就无法利用原先CPU上边的缓存了,也就降低了CPU cache的命中率了。所谓的CPU亲和性,就是让线程在指定的CPU上长时间运行而不被调度到其它CPU上边,以提高CPU cache的命中率。
在Linux中,可以使用pthread_setaffinity_np()
为线程设置CPU的亲和性:
注意到,它的第一个参数类型是pthread_t
,代表 Linux 线程的ID。在C++中,每个std::thread
都对应着底层操作系统的一个线程,不过我们可以使用std::thread
的native_handle()
函数来返回它对应的 OS 线程的ID,在Linux系统中,这个函数的返回值类型是pthread_t
。
下面的例子,我们让线程绑定到 CPU-0 上:
改善 std::thread
在工程实践中,你会发现std::thread
并不像想象中的那么完美。一个明显的问题是,当std::thread
即将析构时,你要人为地确保实际的线程已经结束运行了或者已经分离了(可以分别调用它的join()
和detach()
成员函数),否则程序将会抛出std::terminate
异常。当然,由于std::thread
的析构函数没有保证正确地释放资源,这也同时导致了std::thread
不是异常安全的。
下面提供了一个ThreadRAII
类,它在析构时会等待线程运行结束,或者直接分离线程:
创建ThreadRAII
对象时,可以顺带指定它的析构行为,到底是等待线程运行结束呢还是直接分离线程。下面的例子中,程序退出时,ThreadRAII
对象的析构函数会保证线程正常结束运行:
任务并发
除了std::thread
,C++标准库还引入了std::async
,提供了所谓的基于任务的并发,也可以说std::async
在某些方面弥补std::thread
的缺陷。std::thread
一个恼人的地方是,你没办法直接地拿到任务的返回值,而必须借助一些迂回的方式:
而std::async
会返回一个std::future
对象,通过这个对象就可以获得任务的返回值了,可以看到std::async
让代码变得简洁得多了:
理解 std::async 的行为
当std::async
启动一个任务时,它到底会不会创建一个新的线程呢?答案取决于你是怎样使用std::async
的,当你把任务交给std::async
去执行的时候,可以同时指定任务的启动方式:
std::launch::async
表示异步执行,也就是说把任务放到新的线程中并且立即执行。std::launch::deferred
意味着任务会推迟执行,也就是说只有当你调用了std::future
对象的get()
或wait()
方法时,任务才会执行,这时std::async
会把任务放到当前线程中执行。
假设我们没有指定使用哪种启动方式,那么std::async
会自动在这两种方式中选择一种,这种情况下,我们就无法知道任务到底会不会在新线程中执行了。然而这种不确定性一不小心就会给代码引入 bug,并且这种 bug 很难重现。例如,使用线程局部存储时,就可能遇到不可思议的问题:
任务的返回结果是不可预测的,因为它返回变量val
的值。如果std::async
采用的是std::launch::async
方式,任务将返回100,否则将返回0,这使得代码中任何依赖这个结果的地方都变得不可确定了,很容易引入难以检测的bug。
一种更好的做法是,使用std::async
时总是让它异步执行,这可以减少不必要的麻烦,可以用一个函数来封装这个行为: