C++11多线程相关功能小结
前段时间学习并写了些C++11
多线程的一些博客知识点,现在基本上入门级的知识算是基本概括了吧。因此,就有了这篇关于C++11
多线程的小结。那么我将从之前博客中的知识点抽取一些出来(个人认为是基础使用的进行总结),希望看过有所帮助。关于C++11
多线程的知识点如下:
- 如何创建线程
- 线程互斥量使用
- 异步线程操作
- 原子操作
1. 如何创建线程
C++11
抽象一个线程类std::thread
,方便我们进行使用。
#include <thread>
int main(void)
{
std::thread create_thread(create_function);
create_thread.join();
// create_thread.detach();
return 0;
}
上面即为关于如何创建线程对象,调用线程函数等。代码中的create_thread
为创建的线程对象,后面括号里面的create_function
为线程需要调用的函数。接下来就是线程的类成员函数join()
与detach()
。关于join()
就是需要等待线程执行完毕,main()
才能够继续往下执行。detach()
则是main()
函数不需要等待,继续往下执行【使用detach()要考虑清楚再使用】。这里面就谈及最重要的一个线程概念:阻塞。当然,这里面还有joinable()
函数,它的主要功能就是检查join()
是否调用成功。
【C++11多线程入门教程】系列之线程与进程的基本概念
【C++11多线程入门教程】系列之join()与detach()用法
2. 线程互斥量使用
互斥量在多线程中是比较重要概念,互斥量的存在主要是为了避免在数据共享过程中的访问冲突问题。那么,互斥量主要就是解决数据共享访问过程中,每个线程函数必须有序进行访问。这里插一句:多个线程只读不改写数据,那么不会存在访问冲突。只要存在某个线程有改写数据访问,那么必须存在互斥量来避免数据访问冲突。
互斥量的使用方式:
1 ===> std::mutex
mutex
为最基本的互斥量类,多线程中使用次数频率较高。mutex
类主要结合lock()
与unlock()
函数进行对数据访问加锁与解锁。
#include <thread>
#include <mutex>
#include <iostream>
std::mutex mtx; // 实例化mutex对象
void print(int i)
{
mtx.lock(); // 加锁
// ... // 执行共享数据读写都可以
mtx.unlock(); // 解锁
}
int main()
{
int a = 8;
std::thread create(print, a);
create.join();
return 0;
}
上述代码简要介绍互斥量的使用,进入线程调用函数print()
时候,互斥量对象mtx
对其访问数据a
进行加锁,退出时候进行解锁。中间的一些省略代表对其进行读写操作都可以。以此来避免该线程执行读写a
时候,其它线程无法对其进行访问与修改。一般情况下,我们声明即调用线程函数,因此在下面会使用join()
进行阻塞,知道线程函数执行完毕,main
函数才继续往下执行。
【C++11多线程入门教程】系列之互斥量mutex
【C++11多线程入门教程】系列之互斥量mutex(补)
2 ===> std::lock_guard
这里std::lock_guard
类如果你看过其源码就知道,主要就是在构造函数里面初始化mutex
对象,并自动进行加锁。然后,在析构函数里面对其mutex
对象自动进行解锁。主要就是为了解决大家在写加锁lock时候,操作一波数据后,忘记写解锁unlock操作。所以,std::lock_guard
类能够有效避免std::mutex
带来的问题,因此std::lock_guard
类创建就加锁,类对象离开作用域即解锁。合理安排作用域使用std::lcok_guard
能够有效避免忘记解锁。
#include <thread>
#include <mutex>
#include <iostream>
std::mutex mtx; // 实例化mutex对象
void print(int i)
{
//mtx.lock(); // 加锁
std::lock_guard<std::mutex> lg(mtx); // 该语句替换lock()与unlock()
// ... // 执行共享数据读写都可以
//mtx.unlock(); // 解锁
}
int main()
{
int a = 8;
std::thread create(print, a);
create.join();
return 0;
}
上述为基本的std::lock_guard
类的使用,当然该lock_guard
类有相关参数进行设置。当参数为std::adopt_lock时候,表示构造函数不进行互斥量锁定,只是声明,此时需要手动加锁。无参数时候,lock_guard
类默认构造函数调用,声明即加锁。
【C++11多线程入门教程】系列之互斥量lock_guard
3 ===> std::unique_lock
到这里,已经有个lock_guard
为什么还出现个unique_lock
类。其实我们上面讲述lock_guard
类时候,有说到合理设置作用域来使用lock_guard
类进行互斥操作。但是,有些情况下无法有效拆分作用域,但是使用lock_guard
又会互斥上锁整个作用域带来性能的下降,这个时候就该unique_lock
上场了。
unique_lock
不仅支持原有lock_guard
的所有功能,同时还更加灵活的手动进行加锁lock()
与解锁unlock()
,以此来灵活的限制上锁区域。关于std::unique_lock
第二个参数有3种:adopt_lock
表示默认假设已经上锁【注意是假设】,try_to_lock
表示尝试去锁定,但是必须保证锁处于unlock
的状态,然后尝试能否获取加锁;这种方式如果锁成功的话,就锁定该互斥量mutex
,如果锁失败也不会造成阻塞,继续往下执行。defer_lock
表示初始化一个没有加锁的mutex
,后续需要手动加锁。
lock_guard | unique_lock | |
---|---|---|
支持手动lock()与unlock() | 不支持 | 支持 |
参数 | adopt_lock | adopt_lock、try_to_lock、defer_lock |
#include <thread>
#include <mutex>
#include <iostream>
std::mutex mtx; // 实例化mutex对象
void print(int i)
{
//mtx.lock(); // 加锁
std::unique_lock<std::mutex> lg(mtx, std::defer_lock); // 初始未上锁的mutex
lg.lock();// 手动上锁
// ... // 执行共享数据读写都可以
//mtx.unlock(); // 解锁 不需要手动解锁
}
void print_try(int i)
{
std::unique_lock<std::mutex> lg(mtx, std::try_to_lock); // 尝试上锁
if (lg.owns_lock) // 判断是否上锁成功
{ // 成功
// ... // 执行共享数据读写都可以
}
else
{
std::cout << "获取上锁失败" << std::endl;
// 做其它事情,不会阻塞
}
// 解锁 不需要手动解锁
}
int main()
{
int a = 8;
std::thread create(print, a);
create.join();
return 0;
}
4 ===> std::condition_variable
关于条件变量的出现,主要是为了解决多线程之间的协作问题,消除线程等待时间。最为经典的就是生产消费问题,条件变量主要通过wait()
函数来告知另一个线程是否可以去读数据。wait()
函数会自动调用lock()
或unlock()
函数来进行加锁与解锁。那么另一个线程通过notify_one()
或notify_all()
函数来告知等待的线程wait()
进行唤醒。如此的话,就不会增加多个线程协作之间的等待时间,一旦生产线程完成后,立即解锁唤醒其中一个消费线程读取。
具体事例请参考【C++11多线程入门教程】系列之condition_variable
5 ===> std::async与 std::future
函数模板std::async
为了异步任务而启用,返回是一个std::future
对象。该异步函数std::async
并不是立即执行,只有当std::future
对象调用get()
函数时候,会自动进行阻塞等待std::async
执行完毕获取返回对象。但是,关于std::future
对象的get()
函数只能够调取一次,如果有多次调取需求请参考std::shared_future
的调用。
【C++11多线程入门教程】系列之future(一)
【C++11多线程入门教程】系列之future(二)
6 ===> std::atomic
关于原子类型,这里只强调一句:原子操作是最小的操作,不可分割。同时也不会存在原子类型操作导致的数据访问冲突问题。因此原子类型不需要进行加锁与解锁操作,因为它已经是计算机最小的执行单元。但是,一般原子的操作只有一些简单的操作运算,一般用来计数等。一般支持的运算符操作:++ - += -= &= |= ^=
。具体原子操作的细节请参考:
C++11多线程易错与推荐
上面对C++11
多线程进行简单的知识小结,下面对上述的知识易错点进行总结:
- 尽量不要使用
detach()
函数,避免线程未结束而主线程结束造成的异常结果。 - 使用互斥量
mutex
时候,一定不要忘记用完解锁unlock()
,特别是在作用域复杂时候例如:存在if else
等条件判断时候; - 当多个线程使用互斥量时候,记得最好有序对不同的互斥量进行加锁or解锁【多个互斥量对象有顺序的加锁与解锁】,避免死锁现象的产生;
- 简单函数或作用域简单的情况下推荐使用互斥量
lock_guard
,以此来避免忘记解锁的情况; - 当你需要使用更为复杂的多线程之间操作,推荐
unique_lock
类进行手动控制加锁与解锁时机,但是一定注意不要漏掉解锁; - 使用条件变量时候,注意
wait()
与notify_x()
函数的使用区间,避免死锁; - 使用异步函数模板
std::async
时候,记得考虑清楚函数入参的不同意义; - 类模板
std::future
接收异步函数std::async
时候,并不是立即执行。只有在调用get()
函数时候,进行阻塞直到异步函数std::async
执行完毕。 - 类模板
std::future
的get()
函数只能够调用一次,如果有多次需求,使用std::shared_future
。 - 原子操作时候,一定要谨记:并不是所有的操作都是原子操作,要确认无误在进行使用;例如:
++i
是原子操作,但是,i=i+1
就不是原子操作。
C++11多线程小结
上面写了关于C++11
多线程小结与避错,总结下来就一句话:严格遵守C++11
多线程语法规则进行调用,根据场景应用时候需要时刻考虑会不会造成数据访问冲突问题。如果上面都不存在,那么多线程的编写出错概率将会大大降低。好了,说了这么多。最后,还是那句:如有错误,还请批评指正。
参考
multi-threading
thread
mutex
future
condition_variable
atomic