目录
三个实例对比
结论
多线程编程对于程序员来说,是一项非常重要的技能。在C++11标准问世之前,C++标准是不支持多线程的。在C++11出台前,如果你想在linux平台进行多线程编程,就要使用linux的多线程库pthread,而pthread是按照POSIX标准实现的,与C++标准无关。在C++11标准下,你可以使用std::thread,实现多线程编程。但是,多线程编程涉及的不仅仅是thread,编程者也要考虑资源竞争的情形,这就涉及到互斥锁、信号量等。这些也包含在c++11中。
当两个线程同时访问一个资源的时候,可能出现竞争的情况:假如其中至少一个线程在对此资源做写操作。这时,互斥锁的作用就表现出来了:凡是被互斥锁保护的代码片段,至多有一个线程可以访问它。关于互斥锁的使用方法,读者可自行查阅相关资料。
互斥锁的原理在我的博客《Peterson算法的分析》已经有所描述,这里不重复了。
下面看一组对比实验,共3个程序(平台:银河麒麟V4, CPU采用arm架构)。
第一个程序,在子线程里将一个int变量不断增加,然后打印结果,并显示耗时。
第二个程序,在子线程里将一个int变量不断增加,每次增加都用互斥锁保护,然后打印结果,并显示耗时。
第三个程序,在子线程里将一个atomic<int>原子变量不断增加,然后打印结果,并显示耗时。
三个实例对比
第一个程序名为nomutx.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <sys/time.h>
std::mutex mtx;
int x = 0, y = 0;
std::atomic<int> z(0);
void f1(void)
{
struct timeval t1, t2;
gettimeofday(&t1, NULL);
for(int k = 0; k < 1000000; k++)
x++;
gettimeofday(&t2, NULL);
double dT = (t2.tv_sec - t1.tv_sec) + (double)(t2.tv_usec - t1.tv_usec)/1000000.0;
std::cout<<"t1 spent: "<<dT<<" x = "<<x<<std::endl;
}
int main(void)
{
std::thread t1(f1);
t1.join();
return 0;
}
第二个程序名为mutx.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <sys/time.h>
std::mutex mtx;
int x = 0, y = 0;
std::atomic<int> z(0);
void f2(void)
{
struct timeval t1, t2;
gettimeofday(&t1, NULL);
for(int k = 0; k < 1000000; k++)
{
mtx.lock();
y++;
mtx.unlock();
}
gettimeofday(&t2, NULL);
double dT = (t2.tv_sec - t1.tv_sec) + (double)(t2.tv_usec - t1.tv_usec)/1000000.0;
std::cout<<"t2 spent: "<<dT<<" y = "<<y<<std::endl;
}
int main(void)
{
std::thread t2(f2);
t2.join();
return 0;
}
第三个程序名为atomics.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <sys/time.h>
std::mutex mtx;
int x = 0, y = 0;
std::atomic<int> z(0);
void f3(void)
{
struct timeval t1, t2;
gettimeofday(&t1, NULL);
for(int k = 0; k < 1000000; k++)
z++;
gettimeofday(&t2, NULL);
double dT = (t2.tv_sec - t1.tv_sec) + (double)(t2.tv_usec - t1.tv_usec)/1000000.0;
std::cout<<"t3 spent: "<<dT<<" z = "<<z.load()<<std::endl;
}
int main(void)
{
std::thread t3(f3);
t3.join();
return 0;
}
编译,并运行,看结果:
重复三组实验,虽然最后的x,y,z取值都一样(1000000),但是耗时区别明显:普通的int变量,在不使用互斥锁保护的情况下,耗时最短,大约3毫秒左右;原子变量耗时次之,大约18毫秒;耗时最多是使用互斥锁保护的情况,50毫秒左右。
这里有人会说,上面为什么要用互斥锁和原子变量?上面三个例子并不存在资源竞争,没有必要使用。当然没必要使用了,我们这里只是对比它们的效率。
结论
不难得出如下结论:
1 使用互斥锁是有开销的
2 使用原子变量也有开销,但是其效率还是明显高于互斥锁。
古希腊哲学家德谟克利特提出了原子的概念,认为物质的基本单元是原子,原子不能再被分割。这里我们不去探讨他的对错。之所以叫原子变量,正是因为这种变量一个线程被读写时,另一个线程插不进来,很像德谟克利特口中的不可分割的原子。不同的编译器上,实现原子变量的方式各不相同,这里不做深入探讨。但是原子变量的作用,在上面的例子里面已经展示的很清楚了:它不能被两个线程同时操作,仿佛是被互斥锁保护了一样。但是其效率却高于使用互斥锁的情形。
打个比方:有一个苹果,一部分已经烂了。你想吃掉没烂的部分,就要用刀把烂的部分抠掉。这就要考验你的刀工了:如果抠的部分太小,仍然留下一部分烂苹果,你吃了就会生病(资源竞争);如果抠的部分太大,固然不会影响你的健康,但是抠掉的部分有没烂的,这些没烂的就浪费了(开销过大)。
互斥锁的颗粒度较大,或者说,它的刀工比较粗,如果把烂的部分全抠掉,难免把腐烂部分周边的好苹果也捎带抠掉一些(性能损失);而原子变量的颗粒度更细,或者说,它的刀工比较细,浪费的好苹果就少一些,性能就好于互斥锁。