文章目錄
  1. 1. C++11中的多线程
    1. 1.1. 线程
    2. 1.2. 互斥量
      1. 1.2.1. 独占互斥量std::mutex
      2. 1.2.2. 递归互斥量std::recursive_mutex
      3. 1.2.3. 带超时的互斥量std::timed_mutex和std::recursive_timed_mutex
    3. 1.3. 条件变量
    4. 1.4. 原子变量
    5. 1.5. call_once和once_flag的使用
    6. 1.6. 异步操作


作者:Frank
时间:2016-12-14

在C++11之前,C++语言中并没有对并发编程提供支持。而现在C++11中增加了线程以及线程相关的类,很方便的支持了并发编程。本节将对C++11中多线程部分涉及的知识进行总结,包括互斥量、条件变量、原子变量、call_once等。此外,本节还会对C++11中提供的一些便利性的工具进行讲述,主要有chrono库,数值和字符转换以及宽窄字符转换。

C++11中的多线程

线程

C++11中利用thread来创建使用线程,用std::thread创建线程非常简单,只需要提供线程函数或者函数对象即可,并且可以同时指定函数的参数,其基本使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <thread>

void func(){
//do some work
}

void func2(int a){
//do some work
}

int main(){
std::thread t(func);
t.join();

std::thread t2(func);
t.detach();
//do other jobs

std::thread t3(func2,1);
t.join();
return 0;
}

在std::thread中,join表示阻塞当前线程,直到线程函数执行结束。如果不希望线程被阻塞执行,可以调用线程的detach方法,将线程和线程对象分离。通过detach,让线程作为后台线程去执行,当前线程也不会阻塞了。但是,detach之后就无法和线程发生联系了,线程何时执行完主线程也无法控制了。而带参数的函数线程只需要在函数对象后加上对应的参数即可得到。需要注意的是,std::thread出了作用域之后会被析构,这时如果线程函数还没有执行完则会发生错误,因此,需要保证线程函数的生命周期在线程变量std::thread的生命周期之内。
线程不能复制,但可以移动:

1
2
3
4
std::thread t1(func);
std::thread t2(std::move(t1));
t1.join();
t2.join();

当线程被移动之后,线程对象t1将不代表任何线程。另外,还可以通过std::bind或lambda表达式来创建线程,如下:

1
2
3
4
std::thread t1(std::bind(func2,2));
std::thread t2([](int a,int b){},1,2);
t1.join();
t2.join();

为了保证线程函数的生命周期在线程变量的生命周期之内,可以将线程对象保存在一个容器中,以保证线程对象的生命周期。

在std::thread中,可以获取到当前线程的ID,还可以获取CPU核心数量,其示例代码如下所示:

1
2
3
4
std::thread t(func);
cout<<t.get_id()<<endl;//获取当前线程id
//获取CPU核数,如果获取失败则返回0
cout<<std::thread::hardware_concureency()<<endl;

同时,还可以使得当前线程休眠一定时间,其示例代码如下:

1
2
3
4
5
6
7
8
9
void f(){
std::this_thread::sleep_for(std::chrono::seconds(3));
cout<<"time out"<<endl;
}
int main(){
std::thread t(f);
t.join();
return 0;
}

互斥量

互斥量是一种线程同步的手段,用来保护多线程同时访问的共享数据。C++11中提供了2种语义的互斥量(mutex):

  1. std::mutex:独占的互斥量,不能递归使用;
  2. std::timed_mutex:带超时的独占互斥量,不能递归使用;
  3. std::recursive_mutex:递归互斥量,不带超时;
  4. std::recursive_timed_mutex:带超时的递归互斥量;

这些互斥量的基本接口很相似,一般用法是通过lock()方法来阻塞线程,直到获得互斥量的所有权为止。在线程获得互斥量并完成任务后,必须使用unclock()来解除对互斥量的占用,lock()和unlock()必须成对出现。try_lock()尝试锁定互斥量,如果成功则返回true,如果失败则返回false,它是非阻塞的。

独占互斥量std::mutex

std::mutex的基本用法如下所示:

1
2
3
4
5
6
std::mutex g_lock;
void func(){
g_lock.lock();
//do some work
g_lock.unlock();
}

使用lock_guard可以简化lock/unlock的写法,同时也更安全,因为lock_guard在构造时会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,因此,应尽量用lock_guard。其基本使用示例如下:

1
2
3
4
void func(){
std::lock_gurad<std::mutex> lock(g_lock);//出作用域自动解锁
//do some work
}

递归互斥量std::recursive_mutex

递归锁允许同一线程多次获得该互斥锁,可以用来解决同一线程需要多次获取互斥量时的死锁问题。其基本用法和和mutex区别并不大,只需要在需要使用std::recursive_mutex的地方用其代替std::mutex即可。
但需要注意的是,尽量不要使用递归锁,因为:

  1. 需要用到递归锁定的多线程互斥处理往往本身就是可以简化的,允许递归互斥很容易放纵复杂逻辑的产生,从而导致一些多线程同步引起的晦涩问题;
  2. 递归锁比起非递归锁,效率会低一些;
  3. 递归锁虽然允许同一线程多次获得同一互斥量,可重复获得的最大次数并未具体说明,一旦超过一定次数,再对lock进行调用就会抛出std::system错误

带超时的互斥量std::timed_mutex和std::recursive_timed_mutex

std::timed_mutex是超时的独占锁,std::recursive_timed_mutex是超时的递归锁,主要用于在获取锁时增加超时功能。
std::timed_mutex比std::mutex多了两个超时获取锁接口:try_lock_for和try_lock_until,其基本用法如下:

1
2
3
4
5
6
7
8
9
10
std::timed_mutex;
void work(){
std::chrono::milliseconds timeout(1000);
while(true){
if(mutex.try_lock_for(timeout)){
//do some work
mutex.unlock();
}
}
}

条件变量

条件变量是C++11提供的另外一种等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时,才能唤醒当前的阻塞线程,条件变量需要和互斥量配合起来使用,C++11提供了两种条件变量:

  • condition_variable:配合std::unique_lock进行wait操作;
  • condition_variable_any:和任意带有lock,unlock语义的mutex搭配使用,比价灵活,但效率比condition_variable差一些。

条件变量的使用过程如下:

  1. 拥有条件变量的线程获取互斥量;
  2. 循环检查某个条件,如果条件不满足,则阻塞直到条件满足;如果条件满足,则向下执行;
  3. 某个线程满足条件执行完后调用notify_one或者notify_all唤醒一个或者所有的等待线程。

原子变量

C++11提供了一个原子类型std::atomic,可以使用任意类型作为模板参数,其基本使用示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <atomic>
struct AtomicCounter{
std::atomic<int> value;

void increment(){
++value;
}
void decrement(){
--value;
}
int get(){
return value.load();
}
};

call_once和once_flag的使用

为了保证在多线程环境中某个函数仅被调用一次,可以用std::call_once,其需要一个once_flag作为call_once的入参,其基本用法为:

1
2
std::once_flag flag;
std::call_once(flag,[](){//do some work});

异步操作

C++11提供了异步操作相关的类,主要有std::future、std::promise和std::packaged_task。std::future作为异步结果的传输通道,可以很方便的获取线程函数的返回值;std::promise用来包装一个值,将数据和future绑定起来,方便线程赋值;std::packaged_task用来包装一个可调用对象,将函数和future绑定起来,以便异步调用。

  1. std::future
    std::future提供了获取异步操作结果的通道。可以以同步等待的方式来获取结果,可以通过查询future的状态(future_status)来获取异步操作的结果。future_status有如下三种状态:1)Deferred,异步操作还没开始;2)Ready,异步操作已经完成;3)Timeout,异步操作超时。其基本代码示例如下:
    1
    2
    3
    4
    5
    6
    7
    8
    //查询future的状态
    std::future_status status;
    do{
    status=std::future.wait_for(std::chrono::seconds(1));
    if(status==std::future_status::deferred){
    //do some work
    }else ...
    }while(status!=std::future_status::ready);

获取future结果有三种方式:get,wait_for,wait,其中get等待异步操作结束并返回结果;wait只是等待异步操作完成,没有返回值;wait_for是超时等待返回结果。

  1. 协助线程赋值的类std::promise
    std::promise将数据和future绑定起来,为获取线程函数中某个值提供了便利,在线程函数中为外面传来的promise赋值,在线程函数执行完成后就可以通过std::promise的future获取该值。std::promise的基本示例如下:

    1
    2
    3
    4
    std::promise<int> pr;
    std::thread t([](std::promise<int>& p){p.set_value_at_thread_exit(9);},std::ref(pr));
    std::future<int> f=pr.get_future();
    auto r=f.get();
  2. 可调用对象的包装类std::packaged_task
    std::packaged_task包装了一个可调用对象的包装类,将函数和future绑定起来,以便异步调用,std::promise是保存了一个共享的值,而std::packaged_task是保存了一个函数。其基本用法如下所示:

    1
    2
    3
    4
    std::packaged_task<int()> task([](){return 7;});
    std::thread t1(std::ref(task));
    std::future<int> f1=task.get_future();
    auto r1=f1.get();
  3. 线程异步操作函数async
    std::async比std::promise和std::packaged_task更高一层,它可以用来直接创建异步的task,异步任务返回的结果也在future中。std::async的原型是async(std::launch::async|std::lauch::deferred,f,args…),第一个参数时函数的创建策略,有如下两种策略:

  • std::lauch::async:在调用async时就开始创建线程;
  • std::lauch::deferred:延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程;


转载请注明出处

文章目錄
  1. 1. C++11中的多线程
    1. 1.1. 线程
    2. 1.2. 互斥量
      1. 1.2.1. 独占互斥量std::mutex
      2. 1.2.2. 递归互斥量std::recursive_mutex
      3. 1.2.3. 带超时的互斥量std::timed_mutex和std::recursive_timed_mutex
    3. 1.3. 条件变量
    4. 1.4. 原子变量
    5. 1.5. call_once和once_flag的使用
    6. 1.6. 异步操作