【C++】多线程(二):std::mutex std::atomic的使用
这篇文章接着上一篇,继续介绍C++中的多线程。
推荐先阅读上一篇 【C++】多线程(一):std::thread的使用
互斥
我们前面的函数,无论是线程之间,还是线程和主线程之间,都是没有数据交换的。 接下来让多个线程操作一个全局变量试试。
int global_num = 0;
void plus1000()
{
for (int i = 0; i < 1000; i++)
global_num++;
}
int main()
{
thread ths[10];
for (auto &th : ths)
th = thread(plus1000);
for (auto &th : ths)
th.join();
cout << "n = " << global_num << endl;
return 0;
}
运行结果并不是固定的,很奇怪。只有第一次出现了结果异常的,后续都是正常的。即使我把生成的 exe 删掉,结果也是正常的。
[Running] cd "d:\Codes\CPP\VSCodeProjects\Nov\ThreadTest\" && g++ ThreadTest.cpp -o ThreadTest && "d:\Codes\CPP\VSCodeProjects\Nov\ThreadTest\"ThreadTest
n = 9059
[Done] exited with code=0 in 3.399 seconds
[Running] cd "d:\Codes\CPP\VSCodeProjects\Nov\ThreadTest\" && g++ ThreadTest.cpp -o ThreadTest && "d:\Codes\CPP\VSCodeProjects\Nov\ThreadTest\"ThreadTest
n = 10000
[Done] exited with code=0 in 1.189 seconds
[Running] cd "d:\Codes\CPP\VSCodeProjects\Nov\ThreadTest\" && g++ ThreadTest.cpp -o ThreadTest && "d:\Codes\CPP\VSCodeProjects\Nov\ThreadTest\"ThreadTest
n = 10000
如果这里针对全局变量的操作是单线程的,就不会有数据异常的问题。要解决这个问题,需要用到两个变量。
std::mutex
mutex
又称互斥量,C++ 11中与 mutex相关的类(包括锁类型)和函数都声明在 mutex
头文件中,所以如果你需要使用 std::mutex,就必须包含 mutex 头文件。然后在适当的地方声明一个 mutex 变量即可。
mutex mtx;
在操作全局变量前对 mutex 上锁,操作完解锁,就可以避免数据异常的问题。
void plus1000()
{
for (int i = 0; i < 1000; i++)
{
mtx.lock();
global_num++;
mtx.unlock();
}
}
不停地加锁和解锁会消耗 CPU 的性能,也会延长程序的运行时间。C++ 有很多对程序进行计时的函数,我们这里使用 Windows 平台的一个函数。
QueryPerformanceCounter()是一个Windows API,所需头文件为<windows.h>
这个函数返回高精确度性能计数器的值,它可以以微妙为单位计时.但是QueryPerformanceCounter()
确切的精确计时的最小单位是与系统有关的,所以,必须要查询系统以得到QueryPerformanceCounter()返回的嘀哒声的频率.
QueryPerformanceFrequency() 提供了这个频率值,返回每秒嘀哒声的个数.
void plus1000()
{
for (int i = 0; i < 1000; i++)
{
mtx.lock();
global_num++;
mtx.unlock();
}
}
int main()
{
LARGE_INTEGER t1,t2,tc;
QueryPerformanceFrequency(&tc);
QueryPerformanceCounter(&t1);
thread ths[10];
for (auto &th : ths)
th = thread(plus1000);
for (auto &th : ths)
th.join();
QueryPerformanceCounter(&t2);
cout << "n = " << global_num << ", total time = " << (double)(t2.QuadPart-t1.QuadPart)/(double)tc.QuadPart << endl;
return 0;
}
输出的结果为
n = 10000, total time = 0.0003675
std::atomic
atom 意为 原子,即不可分割的最小操作。理解这个需要一点操作系统多线程的知识。
回到 C++ 来说,需要引入头文件atomic
,将 int 的声明方式改为 atomic_int
或者atomic<int>
atomic_int global_num = 0;
这两个是一样的,从源代码可以看出来:
/// atomic_int
typedef atomic<int> atomic_int;
即可去掉 mutex 锁,依然 能保证多线程下数据不会出现异常。这时的输出为:
n = 10000, total time = 0.0003647
感觉其实也没差多少,我更愿意理解为误差。
async 异步
thread
在使用的时候有个很大的问题,就是没法获取函数的返回值。(不过,如果你是一个写过一些 C++ 代码的人,你应该熟悉使用引用参数替代返回值的写法,这里就不展开了),另外功能上也没有 async
全面。
事实上,thread
是一定会创建一个新的线程的,但 async
不一定,这在系统资源紧张的时候尤为明显,此时强行创建一个新的线程有概率导致程序崩溃。总之,先写个程序吧。
#include <iostream>
#include <future>
using namespace std;
int main()
{
async([]
{ cout << "Maybe a new thread?" << endl; });
cout << "Yeah, u r right!" << endl;
return 0;
}
和 thread 不一样,async 是个函数,其声明如下:
async(_Fn&& __fn, _Args&&... __args)
{
return std::async(launch::async|launch::deferred,
std::forward<_Fn>(__fn),
std::forward<_Args>(__args)...);
}
async(launch __policy, _Fn&& __fn, _Args&&... __args)
第一个参数即你希望 async 以什么样的方式执行 fun,
标识符 | 作用 |
---|---|
launch::async | 开启一个新线程立刻执行fun |
launch::deferred | 不立刻执行fun,而是延迟到调用获取结果get的时候再执行,此时也不会开启新的线程 |
std::launch::async or std::launch::deferred | 由操作系统决定采用以上哪种方式,当没有传入launch参数时,此为参数的默认值 |
/// Launch code for futures
enum class launch
{
async = 1,
deferred = 2
};