C++ 11通过标准库引入了对多线程的支持,这个是c++的新特性之一,也就是说我们直接用即可,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念(这个后文会讲到)。线程啥的就不再解释了,直接上干货;
头文件一定记得写如下几个:
#include //线程库
#include //条件变量
#include //互斥锁
函数 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1, args2,…) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数 |
get_id() | 获取线程id |
jionable() | 线程是否是有效的,joinable代表的是一个正在执行中的线程。 |
join() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与主线程分离,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关。 |
注意:
样例1(对应上面第三点):
void Fun1(int& x)
{
x += 20;
}
void Fun2(int* x)
{
*x += 20;
}
int main()
{
int a = 10;
thread t1(Fun1, a);
t1.join();
//线程函数参数尽管是引用方式,实际引用的是线程栈中的拷贝
cout << a << endl; // 10
// 如果想要通过形参改变外部实参时,怎么办呢?这时借助std::ref()函数
thread t3(Fun1, std::ref(a));
t3.join();
cout << a << endl; //30
thread t2(Fun2, &a);
t2.join();
cout << a << endl; //50
return 0;
}
C++11标准定义“原子类型”,可以保证原子类型在线程间被互斥的访问。
atomic_bool abool; //对应bool
atomic_char achar; //char
atomic_schar aschar; //signed char
atomic_uchar auchar; //unsigned char
atomic_int aint; //int
atomic_uint auint; //unsigned int
atomic_short ashort; //short
atomic_ushort aushort; //unsigned short
atomic_long along; //long
atomic_ulong aulong; //unsigned long
atomic_llong allong; //long long
atomic_ullong aullong; //unsigned long long
atomic_char16_t achar16_t; //char16_t
atomic_char32_t achar32_t; //char32_t;
atomic_wchar_t awchar_t; //wchar_t
但是,我们应该使用atomic类模板。通过该模板,可以定义出任意需要的原子类型:
std::atomic< type > t;
对线程而言,原子类型通常属于“资源型”的数据,这意味着多个线程通常只能访问的原子类型的拷贝。所以在C++11中,原子类型只能从其模板参数类型中进行构造,标准不允许原子类型经行拷贝构造、移动构造,以及operator=等,防止以外发生;
举个例子:
atomic< float > af{ 1.2f };
//atomic< float > af1{ af }; //这里无法编译
原因:atomic模板类的拷贝构造函数、移动构造函数、operator=等总是默认被删除的
在C++11中,标准将原子操作定义为atomic模板类的成员函数,这囊括了绝大多数典型的操作,如读、写、交换等,当然,对于内置类型而言,主要是通过重载一些全局操作符来完成的,在编译的时候,会产生一条特殊的lock前缀的x86指令,lock能够控制总线及实现x86平台上的原子性。
上面的那些是原子类型的函数的操作:读(load)、写(store)、交换(exchange)、比较并交换(compare_exchange_weak/compare_exchange_stronge)等操作;
当然,有时编译器会给我们作出优化:
atomic<int> a;
a = 1; //a.store(1);
int b = a; //b = a.load();
上图中,那个atomic_flag,这个要特别关注一下,听说效率很高,可以自制自旋锁,如下:
void Lock(atomic_flag *lock) {
while (lock->test_and_set()); }
void Ublock(atomic_flag *lock) {
lock->clear(); }
//test_and_set()函数是设置true值,返回之前的值。
//clear()是复位,置为false;
std::atomic_flag lock = ATOMIC_FLAG_INIT; //初始化
代码演示:
// 自旋锁实现.cpp : 定义控制台应用程序的入口点。
//
#include
#include
#include
#include
using namespace std;
std::atomic_flag lock = ATOMIC_FLAG_INIT; //声明了全局变量,初始化为值ATOMIC_FLAG_INIT,即false状态
void Lock(atomic_flag *lock) {
while (lock->test_and_set()){
cout<<"Waiting..."<<endl;
} }
void Ublock(atomic_flag *lock) {
lock->clear(); }
void func(){
Lock(&lock);
cout << "func working..." << endl;
Ublock(&lock);
}
void foo(){
Lock(&lock);
cout<<"foo working..."<<endl;
Ublock(&lock);
}
int main(void)
{
std::thread t1(func);
std::thread t2(foo);
t1.join();
t2.join();
system("pause");
return 0;
}
一、多线程启动函数:std::async()
声明方式:
template <class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type>
async (launch policy, Fn&& fn, Args&&... args);
其中:
// 异步启动的策略
enum class launch {
// 异步启动,在调用std::async()时创建一个新的线程以异步调用函数,并返回future对象;
async = 0x1,
// 延迟启动,在调用std::async()时不创建线程,直到调用了future对象的get()或wait()方法时,才创建线程;
deferred = 0x2,
// 自动,函数在某一时刻自动选择策略,这取决于系统和库的实现,通常是优化系统中当前并发的可用性
any = async | deferred,
sync = deferred
};
//参数 fn 是要调用的可调用 (Callable) 对象
//参数args 是传递给 f 的参数
//std::launch::async:在调用async就开始创建线程。
//std::launch::deferred:延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程。
std::async()是一个接受回调(函数或函数对象)作为参数的函数模板,通过启动一个新线程或者复用一个它认为合适的已有线程异步调用。
std::async返回一个std::future< T >,它存储由std::async()执行的函数对象返回的值。所以通常都有std::future伴随着使用,因为future中存储了线程函数返回的结果。
内部原理:
std::async先异步操作用std::packaged_task包装线程函数,然后将异步操作的结果放到std::promise中,最后再通过future.get/wait来获取这个未来的结果。(后面会讲到)
std::future提供了一种访问异步操作结果的机制,也就是我们可以通过这个类对象,异步访问被调用线程函数的结果;
是类模板:
template
future::type>
获取future结果有三种方式:
也可以通过查询future的状态(future_status)来获取异步操作的结果。future_status有三种状态:
例子:
//查询future的状态
std::future_status status;
do {
status = future.wait_for(std::chrono::seconds(1)); //等待一秒
if (status == std::future_status::deferred) {
std::cout << "deferred\n";
} else if (status == std::future_status::timeout) {
std::cout << "timeout\n";
} else if (status == std::future_status::ready) {
std::cout << "ready!\n";
}
} while (status != std::future_status::ready);
std::chrono知识点
代码演示:
# include
# include
# include
# include
using namespace std;
int funca(int a,int b){
return a+b;
}
int funcb(int a){
return a;
}
int main(void)
{
future f1 = std::async(funca,1,2); // 是绑定的函数返回值
future f2 = std::async(funcb,3);
auto it = f1.get() + f2.get();
cout<
std::promise可以获取线程函数里的值,不过要等执行完毕后才可以获取;当然,是间接地通过promise内部提供的future来获取的!
用法:
std::promise pr;
std::thread t([](std::promise& p){ p.set_value_at_thread_exit(9); },std::ref(pr));
std::future f = pr.get_future();
点击了解
这个是包装了一个可调用对象(如function, lambda expression, bind expression, or another function object);packaged_task保存的是一个函数。
用法:
std::packaged_task task([](){ return 7; });
std::thread t1(std::ref(task));
std::future f1 = task.get_future();
auto r1 = f1.get();
锁: 最常见的就是mutex (有RAII思想的管理锁的类模板,可以预防我们忘记解锁)
C++11根据mutext的属性提供四种的互斥量,分别是:
时间锁:
timed_mutex myMutex;
chrono::milliseconds timeout(1000); //1秒
if (myMutex.try_lock_for(timeout))
{
//在1秒内获取了锁
//业务代码
myMutex.unlock();
}
else
{
//在100毫秒内没有获取锁
//业务代码
}
time_mutex博客
mutex成员函数(常用):
6. lock(),互斥量加锁,如果互斥量已被加锁,线程阻塞
7. bool try_lock(),尝试加锁,如果互斥量未被加锁,则执行加锁操作,返回true;如果互斥量已被加锁,返回false,线程不阻塞。
8. void unlock(),解锁互斥量
std::lock_guard:
管理mutex的类。以独占所有权的方式管理mutex对象的上锁和解锁操作,对象构建时传入mutex,会自动对mutex加入,直到离开类的作用域,析构时完成解锁。RAII式的栈对象能保证在异常情形下mutex可以在lock_guard对象析构被解锁。
template<class _Mutex>
class lock_guard
{
public:
// 在构造lock_gard时,_Mtx还没有被上锁
explicit lock_guard(_Mutex& _Mtx)
: _MyMutex(_Mtx)
{
_MyMutex.lock();
}
// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
lock_guard(_Mutex& _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{
}
~lock_guard()
{
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
例子:
#include
#include
#include
std::mutex mut;
void Print(int num)
{
std::cout << "this is thread_unlock: " <<num<< std::endl;
{
std::lock_guard<std::mutex> lg(mut);//初始化就上锁
std::cout << "this is thread: " << num << std::endl;
}//离开块作用域就自动解锁
}
int main()
{
std::thread t1(Print, 1);
std::thread t2(Print, 2);
t1.join();
t2.join();
std::cout << "this is main thread " << std::endl;
return 0;
}
std::unique_lock:
也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。这个比较灵活,可以让我们指定“何时”以及“如何”锁定和结果Mutex,有挺多函数给我们进行选择。
头文件:# include < condition_variable >
std::condition_variable readyCondVar;
条件的检测是在互斥锁的保护下进行的。线程在改变条件状态之前必须首先锁住互斥量。如果条件为假,这个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量 可以被用来实现这两进程间的线程同步。
常用API接口:
代码:
std::mutex mutex;
std::condition_variable cv;
// 条件变量与临界区有关,用来获取和释放一个锁,因此通常会和mutex联用。
std::unique_lock lock(mutex); //和RALL锁机制使用
// 此处会释放lock,然后在cv上等待,直到其它线程通过cv.notify_xxx来唤醒当前线程,
// cv被唤醒后会再次对lock进行上锁,然后wait函数才会返回。
// wait返回后可以安全的使用mutex保护的临界区内的数据。此时mutex仍为上锁状态
cv.wait(lock)
(上面的wait这个函数可能会导致惊群效应,所以我们可以用重载版本,cv.wait(lock,可调用函数对象));
类似这样:
> g_cv.wait(lock, [] ){
return xxx; });
参考此篇文章:请点击!
点击链接查看这个知识点。(这篇博客也不严谨,我是持怀疑态度。。。)
参考文章: