C++11多线程学习小结

关于c++11 多线程的一些小结, 内容基本上是自己看书的笔记。 详细内容还需参考cppreference.com

过程中参考了这篇博文,和 这篇博文

async 和 future

async

async 在后台启动一个异步执行的任务 通常和 future 一起使用

std::async(launch, function, arg ...)

  • launch 发射策略
  • function 是一个可调用对象, 如函数, lambda, 成员函数
  • arg 传递给可调用对象的参数

当传递给std::async()一个成员函数时, 后面必须传递一个该成员函数所在类的一个对象:

class X
{
public:
    void mem(int num);
};
...

X x;
auto a = std::async(&X::mem, x, 2);

如果传给std::async()的可调用对象没有返回值, std::future::get()也没有返回值

发射策略std::launch::asyncstd::launch::deferred

  • std::async(std::launch::async, function) 立即执行异步任务

  • std::async(std::launch::deferred, function) 延缓异步调用,直到std::future::get() 或者std::future::wait()

如果没有指定发射策略, 系统有可能立即执行异步任务, 有可能推迟。。。(测试了几个程序都是延迟到程序快退出时才执行)

future

std::future<type> f(operator) 将某一操作的结果存入一个 future对象中

在获取多线程异步操作结果中使用较多

std::future::get() 获取某一操作的结果(此结果可能是一个异常), 如果该操作还没出结果则阻塞

std::future<returntype> result(std::async(function))将一个异步执行的结果存入result

如果没有对返回的future调用getwait, function可能永不被调用(假设没有指定发射策略为async)

std::future 只能调用 get() 一次, 之后只能用 future::valid() 来检查, 其他调用会产生未定义行为

future::valid() 用来检查future是否有效,即结果是否已准备好

在执行异步任务发生异常时,异常不会马上抛出,而时等到get()调用时抛出

用例:

#include <future>
#include <thread>
#include <chrono>
#include <random>
#include <iostream>
#include <exception>
#include <list>
using namespace std;

int doSomethingWithExcep()
{
    //一直向链表中添加元素 直到栈用完抛出异常
    list<int> li;
    while(true)
    {
        for (int i = 0; i < 1000000; ++i)
        {
            li.push_back(i);
        }
        cout.put('.').flush();
    }
}

int doSomething( char c)
{
    //设置随机引擎和随机分布
    std::default_random_engine dre(c);
    std::uniform_int_distribution<int> id(10, 1000);
    for (int i = 0; i < 10; ++i)
    {
        //每次等待随机时间 然后打印字符
        std::this_thread::sleep_for(chrono::milliseconds(id(dre)));
        cout.put(c).flush();
    }
    return  c;
}

int func1()
{
    return doSomething('/');
}

int func2()
{
    return doSomething('+');
}

int main1()
{
    std::cout << "starting func1() in background"
        << " and func2 in foreground:" << std::endl;

    std::future<int> result1(std::async(std::launch::async, func1));

    int result2 = func2();

    int result = result1.get() + result2;

    std::cout << "\nresult of func1() + func2(): " << result << std::endl;

    std::cout << "waiting for exception ....\n" << std::endl;

    auto f1 = std::async(doSomethingWithExcep);
    cin.get();

    try{
        f1.get(); //在线程中发生了异常, get() 时抛出
    }
    catch(const exception& e)
    {
        std::cerr << "EXCEPTION: " << e.what() << std::endl;
    }

    return 0;
}
int main()
{

    std::cout << "starting 2 operation..\n";

    auto f1 = async(std::launch::async, []{doSomething('.');});
    auto f2 = async(std::launch::async,[]{doSomething('+');});

    //确保至少有一个任务已经启动
    if (f1.wait_for(chrono::seconds(0)) != future_status::deferred ||
                f2.wait_for(chrono::seconds(0)) != future_status::deferred)
    {
        //轮询两个任务是否完成  若没有 if 判断 while 可能一直循环下去
        while( f1.wait_for(chrono::seconds(0)) != future_status::ready &&
                f2.wait_for(chrono::seconds(0)) != future_status::ready)
        {
            this_thread::yield();
        }
    }

    cout.put('\n').flush();

    try{
        //不加get 异步任务有可能在主线程结束之前还没有执行完
        f1.get();
        f2.get();
    }
    catch (const exception& e){
        cout << "\nEXCEPTION: " << e.what() << endl;
    }

    cout << "\ndone" << endl;
    return 0;
}

Thread 和 Promise

std::thread

std::thread(function, arg1, arg2...)
立即创建新的线程, 失败时抛出std::system_error异常和错误码

利用thread创建的线程如果发生异常而未捕捉, 程序会立刻中止, 并调用std::terminate()

thread 的 join 和 detach 方法作用与 pthread 设置线程属性的函数相似

  • thread::joinable() 如果一个线程时 joinable 的 返回 true
  • thread::join() 等待线程退出, 如果对一个不是 joinable 的线程调用此函数, 抛出异常
  • thread::detach() 将线程设为可分离状态

对于 detach 的线程, 其有可能在主程序退出时, 其可能还在运行, 并且访问已经被析构的全局或静态变量(有点疑问 主程序退出了 创建的线程也退出了啊。。)

因此以引用方式传递变量和对象给线程带有极大的风险, 建议传值

另外, 如果调用 quick_exit() 结束程序并不会销毁全局或者静态变量

获取线程ID std::this_thread::get_id()

也可以用一个变量 std::thread::id threadid 保存线程ID

#include <thread>
#include <chrono>
#include <random>
#include <iostream>
#include <exception>
#include <pthread.h> 
using namespace std;

void doSomething( int num, char c)
{
    try{
        std::default_random_engine dre(c);
        std::uniform_int_distribution<int> id(10, 1000);

        for (int i = 0; i < num; ++i)
        {
            //每次等待随机时间 然后打印字符
            std::this_thread::sleep_for(chrono::milliseconds(id(dre)));
            cout.put(c).flush();
        }
        std::cout << "\nthread " << std::this_thread::get_id() 
            << " exit..." << std::endl; 
    }
    catch (const exception& e)
    {
        cerr<< "THREAD-EXCEPTION (thread " << this_thread::get_id() << ") "<< e.what() << endl;
    }
    catch (...){
        cerr<< "THREAD-EXCEPTION (thread " << this_thread::get_id() << ") "<< endl;
    }

}

int main()
{
    try
    {
        thread t1(doSomething, 5, '.');

        cout << "- started fg thread " << t1.get_id() << endl;

        for (int i = 0; i < 5; ++i)
        {
            thread t(doSomething, 10, 'a'+i);
            cout << "- detach started bg thread " << t.get_id() <<  endl;
            t.detach(); //设置5个可分离线程
        }

        cin.get();
        cout << "- join fg thread " << t1.get_id()  << endl;
        t1.join();
    }
    catch (const exception &e){
        cerr << "EXCEPTION: "<< e.what() << endl;
    }

    //pthread_exit((void*)0); //只结束线程, 不结束进程
    return 0;
}

输出1:

started fg thread 140056933353216
detach started bg thread 140056924960512
detach started bg thread 140056916567808
detach started bg thread 140056908175104
detach started bg thread 140056899782400
detach started bg thread 140056891389696
.abcdec.deeda.cb.ebac.adedebcbaabecdabecdcbceaebcdabdad
join fg thread 140056933353216

输出2:

started fg thread 140041721177856
detach started bg thread 140041712785152
detach started bg thread 140041704392448
detach started bg thread 140041695999744
detach started bg thread 140041687607040
detach started bg thread 140041679214336
.abcdec.de #在此按下回车
join fg thread 140041721177856
eda.cb.ebac.%
#此时主线程先于detach线程退出, detach线程成了僵尸线程
#如果 在main()函数返回之前加上 pthread_exit((void*)0); 只让线程结束而不是结束整个进程, 则5个detach线程就能完整执行

std::promise

std::promise<type> 用来存储一个在稍后异步访问的值或者异常, 常用来在两个线程间通信

promise 可以配合 thread 传递值或者异常

void thr_fun(std::promise<std::string>& p)
{
    try{
        //do somthing... 

        p.set_value(somevalue); //存储值

        //do somthing... 
    }
    catch(...){
        p.set_exception(std::current_exception());//存储异常
    }
}

int main()
{
    try{
        std::promise<std::string> p;
        std::thread t(thr_fun, std::ref(p));
        t.detach();

        //do somthing... 

        std::future<std::string> f(p.get_future());
        f.get(); //获取结果 或者

        //do somthing... 
    }
    catch(const std::exception& e) //获取异常
    {
        //do somthing... 
    }
}

对于promise, get_future() 操作只能调用一次, 再次调用会抛出异常


mutex

std::mutex mtx;

加锁解锁:mtx.lock(); mtx.unlock();

class lock_guard

std::lock_guard<std::mutex> lock(mtx) 用一个未加锁的mtx初始化, 并锁定, 或者

std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
将一个已经锁定的mtx过继给 std::lock_guard 后者在作用域结束时自动释放锁。

lock_guard 只能在初始化时进行加锁

std::try_lock(m1, m2...) 尝试对多个锁加锁

std::mutex m1;
std::mutex m2;

int idx = std::try_lock(m1, m2); //尝试对两个锁加锁
if (idx < 0){ //两个锁加锁成功
    std::lock_guard<std::mutex> lockM1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lockM2(m2, std::adopt_lock);
    ...
}//作用域结束,自动释放锁
else{
    //idx返回加锁失败的锁的序号, 以0作为起始序号
    std::cerr << "could not lock mutex m" << idx+1 << std::endl;
}

在同一个线程中对一个锁再次加锁会造成死锁(包括try_lock()), 此时可使用递归锁

class unique_lock

std::unique_lock<>std::lock_guard<> 拥有相同的接口的同时,又允许对一个锁指定 如果加锁和解锁,以及何时加锁。

  • std::unique_lcok<std::mutex> lock(mutex, std::try_to_lock);
    表示企图加锁 , 但不希望阻塞
  • std::unique_lcok<std::mutex> lock(mutex,std::chrono::seconds(1)); 在某个时间后加锁
  • std::unique_lcok<std::mutex> lock(mutex, std::defer_lock); 初始化锁, 但暂时不加锁

condition_variable

头文件<condition_variable>提供了两种条件变量类: condition_variablecondition_variable_any

condition_variable

std::condition_variable 被设置用来唤醒一个或所有等待的线程

由于wait()过程中会有隐式加锁解锁操作, condition_variable 所有对锁的操作都是基于 unique_lcok

在wait过程中会指向下面三个原子步骤:
1. 解锁 mutex 然后进入 wait 状态
2. 解除 wait 状态
3. 再次锁住 mutex

比较重要的成员函数([]代表参数可选) :

  • notify_one() 唤醒一个线程

  • notify_all() 唤醒全部等待的线程

  • wait(ul, pred) 使用unique_lock用来等待通知.

    注意, 由于有可能假醒(即条件变量还未设置,而wait就返回, 至于为什么, 不清楚), 还必须提供第二个检查 pred, 当条件变量被设置且pred返回真时,才能进行下一步操作

  • wait_for(ul, duration,[pred]) 等待一段时间, 或者一次唤醒后pred结果为真

  • wait_until(ul, timepoint, [pred]) 等待直到某个时间点, 或者某次唤醒后pred为真

wait_for() 和 wait_until() 的不接受 pred 的版本返回值属于一下枚举类:

  • std::cv_status::timeout 发生超时
  • std::cv_status::no_timeout 发生通知

其接受 pred 的版本返回值是 pred 的判断结果

condition_variable使用例子:

一个简单的生产者-消费者模型

#include <condition_variable>
#include <mutex>
#include <future>
#include <thread>
#include <iostream>
#include <queue>

std::queue<int> queue;
std::mutex queue_mutex;
std::condition_variable queue_cond_var;

void provider(int var)
{
    for (int i = 0; i < 5; ++i)
    {
        {
            std::lock_guard<std::mutex> lg(queue_mutex);
            queue.push(var++);
        }

        queue_cond_var.notify_one();

        std::this_thread::sleep_for(std::chrono::milliseconds(var));
    }
}

void consumer (int num)
{
    while(true)
    {
        int var;
        {
            std::unique_lock<std::mutex> ul(queue_mutex);
            queue_cond_var.wait(ul, []{return !queue.empty();});
            //当队列中没有数据时 程序在此处一直阻塞
            var = queue.front();
            queue.pop();
        }
        std::cout << "consumer " << num << ": " << var << std::endl;
    }
}


int main()
{
    auto p1 = std::async(std::launch::async, provider, 300);
    auto p2 = std::async(std::launch::async, provider, 400);
    auto p3 = std::async(std::launch::async, provider, 500);

    auto c1 = std::async(std::launch::async, consumer, 1);
    auto c2 = std::async(std::launch::async, consumer, 2);

    return 0;
}

atomic

头文件

std::atomic<type> 声明一个对象, 此对象的相关操作都是原子的, 因而可以简化对于线程访问同步变量的处理, 不用进行加锁解锁操作。

初始化:

std::atomic<int> ready(1); 用一个常量初始化一个 atomic 对象,若使用 atomic 的默认构造函数 std::atomic<int> ready;, 则后续对 atomic 对象的操作只允许: std::atomic_init(&read, num);

操作:

  • store() 赋新值
  • load() 取当前值的一个拷贝

另外还支持运算符操作,如: + - = -=  等等

#include <atomic>
#include <future>
#include <thread>
#include <chrono>
#include <iostream>

long data;
std::atomic<bool> readyFlag(false);

void provider(){
    std::cin.get();
    data = 42; //provide some data
    readyFlag.store(true);
}

void comsumer()
{
    while(!readyFlag.load()){
        std::cout.put('.').flush();
        std::this_thread::sleep_for(std::chrono::milliseconds(500)); //等待一小段时间
    }

    //数据准备好了
    std::cout << "\nvalue: " << data << std::endl;
}

int main()
{
    //auto解析为 std::future<void>
    auto p = std::async(std::launch::async, provider);
    auto c = std::async(std::launch::async, comsumer);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/wy11933/article/details/72836098