C++11多线程

线程

例子

程序需要从main函数开始,同样线程也是从某个函数开始的(这个函数下文称为线程函数)。和pthread_create一样,C++11提供的线程类std::thread,在创建类变量的时候就产生一个线程,因此需要在std::thread的构造函数中传入线程函数作为参数。得益于C++11支持可变参数模板和完美转发,如果线程函数拥有参数,那么可以十分自然地通过std::thread的构造函数传递。


#include
#include
#include


void threadFunc(std::string &str, int a)
{
    str = "change by threadFunc";
    a = 13;
}

int main()
{
    std::string str("main");
    int a = 9;
    std::thread th(threadFunc, std::ref(str), a);

    th.join();

    std::cout<<"str = " << str << std::endl;
    std::cout<<"a = " << a << std::endl;

    return 0;
}

上面例子的输出为:

str = change by threadFunc 
a = 9

线程函数不仅支持普通函数,还可以是类的成员函数和lambda表达式。如下:

#include
#include
#include


class ThreadObject
{
public:

    void threadFunc(const std::string &str, double d)
    {
        std::cout << "ThreadObject::threadFunc: str = " << str << "\t d = " << d << std::endl;
    }


    void operator () (const std::string &str, int i)
    {
        std::cout << "TheadObject::operator (): str = " << str << "\t i = " << i << std::endl;
    }

};

int main()
{
    int score = 99;
    std::thread th1([&score]() {
        std::cout << "score = " << score << std::endl; });

    th1.join();

    ThreadObject to;
    std::thread th2(&ThreadObject::threadFunc, &to, "xiaoming", 98);
    th2.join();

    std::thread th3(std::ref(to), "xiaohua", 97);
    th3.join();

    return 0;
}

上面例子输出为:

score = 99
ThreadObject::threadFunc: str = xiaoming    d = 98
TheadObject::operator (): str = xiaohua     i = 97

管理线程

在上面的例子中已经看到,std::thread在构造函数中创建线程。根据RAII基本法,在构造函数申请资源,并且在析构函数中释放资源。 但std::thread的析构函数能释放线程资源吗?线程和std::thread是两个不同的实体。线程是操作系统层面的,而std::thread变量则是C++层面的,两者有不同的生命周期。从代码层面来说,std::thread变量在主函数中被析构了,可能线程函数还没运行完毕。如果std::thread变量在线程运行完毕前就析构了,线程资源交由谁管理?

容易想到的一个解决方案是保证std::thread变量的生命周期大于线程本身。上面的例子就是这样做的:主线程通过调用join()函数,等待线程运行完毕。主线程调用join函数后会进入等待状态,直至次线程运行完毕后,主线程才从join函数返回。不同于pthread_join可以获取线程函数的返回值,std::thread的join函数并不能获取线程函数的返回值

“启动一个线程,然后暂停当前线程”,这种处理方式想想就觉得不对(当然如果同时启动多个线程,然后才一个个调用join还是可取的)。为此,std::thread还提供另外一个函数detach。调用detach函数后,std::thread变量会和其关联的线程失去关联,由C++运行库释放相关的线程资源。

join函数或者detach函数都必须在std::thread变量析构之前调用。否则std::thread的析构函数将调用std::terminate()终止整个进程。如果std::thread变量在析构之前,线程就已经执行完毕了呢?一样会挂!

std::mutex

C++ 11提供了4中不同的锁,这里只介绍std::mutex。std::mutex只有几个成员函数,我们这里只需关注lock和unlock函数即可。加锁解锁相当简单,看下面例子。

#include
#include
#include
#include
#include


std::mutex g_mutex;


void doThreadFunc(int n, char c)
{
    g_mutex.lock();

    std::vector<int> vec(n, c);
    std::copy(vec.begin(), vec.end(), std::ostream_iterator<char>(std::cout, ""));
    std::cout << std::endl;

    g_mutex.unlock();
}


template<typename ... Args >
void threadFunc(Args&& ... args)
{
    try
    {
        doThreadFunc(std::forward(args)...);
    }
    catch (...)
    {
    }
}




int main()
{
    std::thread th1([] {threadFunc(10, '*'); });//这么蹩脚的写法是因为threadFunc需要先实例化
    std::thread th2([] {threadFunc(5, '#'); });

    th1.join();
    th2.join();

    return 0;
}

上面例子的一个可能输出为

**********
#####

管理锁

std::mutex的加锁和解锁相当简单,无需多言。但上面例子有一个bug:如果第一个进入循环体的线程在构造vec的时候抛异常,其申请的锁将无法得到解锁,第二个线程就永远无法申请到锁。

std::lock_guard

上面的问题是由于资源没有得到正确的管理,需要使用RAII大法进行管理。C++11提供了std::lock_guard模板类,在构造函数中调用lock,在析构函数中调用unlock。lock_gurad之所以是模板类,是因为C++11提供4中不同的锁。

#include
#include
#include
#include
#include


std::mutex g_mutex;


void doThreadFunc(int n, char c)
{
    std::lock_guard<std::mutex> lg(g_mutex);

    std::vector<int> vec(n, c);
    std::copy(vec.begin(), vec.end(), std::ostream_iterator<char>(std::cout, ""));
    std::cout << std::endl;
}

template<typename ... Args >
void threadFunc(Args&& ... args)
{
    try
    {
        doThreadFunc(std::forward(args)...);
    }
    catch (...)
    {
    }
}

int main()
{
    std::thread th1([] {threadFunc(10, '*'); });//这么蹩脚的写法是因为threadFunc需要先实例化
    std::thread th2([] {threadFunc(5, '#'); });

    th1.join();
    th2.join();

    return 0;
}

std::unique_lock

虽然std::lock_guard完美展现了RAII,但std::lock_guard限制得太死了,只有构造和析构函数,没法通过它的成员函数加锁和解锁。为此,C++11提供了灵活的std:unique_lock模板类。std::unique_lock提供lock和unlock函数,因此可以在适当的时候加解锁。这样可以降低锁的粒度。

默认情况下,std::unique_lock的构造函数会对mutex进行加锁,在析构的时候会对mutex进行解锁。为了避免重复解锁,std::unique_lock需要记录内部mutex的状态,在析构函数中根据mutex的状态决定是否需要解锁。

#include
#include
#include
#include
#include
#include
#include
#include
#include

std::mutex g_mutex;


void threadFunc(size_t num)
{
    //C++ 11保证局部static只会被一个线程初始化
    static std::default_random_engine random(time(nullptr));
    static std::uniform_int_distribution<int> dist(0, 100);

    std::vector<int> vec;
    vec.reserve(num);

    std::unique_lock<std::mutex> ul(g_mutex);
    std::generate_n(std::back_inserter(vec), num, [] {return dist(random); });

    ul.unlock();//解锁,降低锁的粒度

    int sum = std::accumulate(vec.begin(), vec.end(), 0);
    double avg = 1.0*sum / vec.size();

    ul.lock();
    std::cout << "thread id " << std::this_thread::get_id() << " obtains avg num = " << avg << std::endl;

    //ul的析构函数会解锁
}


int main()
{
    std::thread th1(threadFunc, 100);
    std::thread th2(threadFunc, 200);

    th1.join();
    th2.join();

    return 0;
}

上面程序的一个可能输出为:

thread id 24408 obtains avg num = 48.15
thread id 28664 obtains avg num = 52.82

上面的例子中,虽然去掉ul.unlock()和ul.lock()也是可以的,但通过适当的unlock和lock可以降低锁的粒度。

延迟加锁

如果看过std::unique_lock模板类声明的读者会发现,它还重载了几个构造函数。这里简单介绍一下defer_lock_t这个重载。

还记得学操作系统时的“哲学家进餐”问题吗?解决的方法之一是同时对左右两边的餐具进行加锁。C++11提供了一个模板函数std::lock()使得很容易原子地对多个锁进行加锁。

template <class Mutex1, class Mutex2, class... Mutexes>
void lock (Mutex1& a, Mutex2& b, Mutexes&... cde);

又是模板,虽然模板参数写的是Mutex1、Mutex2,但实际上std::lock函数只要求参数有lock操作即可,也就是说可以传一个std::mutex或者std::unique_lock变量给std::lock。std::lock_guard变量则不行,因为其没有lock()函数。


class BigData
{
public:

    void swap(BigData &bg)
    {
        if (this == &bg)
            return;

        std::unique_lock<std::mutex> ul1(m_mutex, std::defer_lock);
        std::unique_lock<std::mutex> ul2(bg.m_mutex, std::defer_lock);

        std::lock(ul1, ul2);
        m_vec.swap(bg.m_vec);
    }

private:
    std::mutex m_mutex;
    std::vector<int> m_vec;
};

上面代码中,std::defer_lock是告诉std::unique_lock的构造函数,在构造函数不要给mutex上锁,延迟上锁,后面会上锁(std::lock函数上锁)。上面的swap函数中,std::lock函数负责上锁,解锁仍然由std::unique_lock的析构函数完成。

除了std::defer_lock,还可以用std::adopt_lock作为第二个参数。std::adopt_lock和std::defer_lock相反,它表示已经上锁了,不劳你动手加锁了。std::lock_guard也是支持std::adopt_lock的,但不支持std::defer_lock,估计是因为std::lock_guard并内部变量记录锁的状态,它只知道在构造函数加锁(或者由adopt_lock指明无需加锁),在析构函数解锁。

对于使用了std::defer_lock的std::unique_lock,以后手动加锁时要通过std::unique_lock类的lock()函数,而不std::mutex的lock()函数,因为std::unique_lock需要记录mutex的加锁情况。下面代码就是一个作死例子。

#include
#include
#include


std::mutex g_mutex;

void threadFun1()
{
    std::unique_lock<std::mutex> ul(g_mutex, std::defer_lock);

    g_mutex.lock();//作死

    std::cout << " in thread Fun1 " << std::endl;
    //由于std::unique_lock并没有记录到g_mutex已经上锁了,所以析构函数并不会调用unlock解锁
}


void threadFun2()
{
    std::lock_guard<std::mutex> lg(g_mutex);
    std::cout << "threadFunc2 get the lock" << std::endl;
}


int main()
{
    std::thread th1(threadFun1);
    th1.join();

    std::thread th2(threadFun2);
    th2.join();

    std::cout << "finish 2 threads" << std::endl;
    return 0;
}

上面代码输出只有

in thread Fun1

条件变量

虽然操作系统这门课重点介绍了信号量,但C和C++世界基本不使用信号量,而是条件变量,实际上是用条件变量实现和信号量相同的功能。信号量和条件变量本质是一种通知,一个线程通知另外的线程。std::condition_value有notify和notify_all两种通知方式。

在具体介绍通知方式之前,先回忆一下大学换教室的情景:上课的时候,如果老师发现教室设备坏了,会申请换到另外一个教室。如果老师只是对学生喊一声“到xxx教室”,那么在场的学生都能收到这个这个信息。但那些迟到的同学却错过了老师这句话而没有收到换到哪个教室这个信息;如果老师“到xxx教室”写在黑板上,那么迟到的同学仍然能收到换到哪个教室的信息,直到有人把它擦掉。这里说的喊一声和写到黑板上实际上就是边缘触发和水平触发。

信号量采用的是水平触发,而条件变量则是边缘触发。为了能让条件变量的信息通知到迟到的线程,可以使用另外一个变量记录之。发信息的线程负责写变量,收信息的线程负责读这个变量。显然需要使用一个锁保护这个变量。因此,为了实现信号量相同的功能,需要使用条件变量+存储变量+锁。下面是一个经典的生产者-消费者例子

#include
#include
#include
#include
#include


std::mutex g_mutex;
std::condition_variable cond;

std::list<int> alist;

void threadFun1()
{
    std::unique_lock<std::mutex> ul(g_mutex);
    while (alist.empty())
    {
        cond.wait(ul);
    }

    std::cout << "threadFun1 get the value : " << alist.front() << std::endl;
    alist.pop_front();
}


void threadFun2()
{
    std::lock_guard<std::mutex> lg(g_mutex);
    alist.push_back(13);

    cond.notify_one();
}


int main()
{
    std::thread th1(threadFun1);
    std::thread th2(threadFun2);

    th1.join();
    th2.join();

    return 0;
}

上面例子的输出为:

threadFun1 get the value : 13

上面代码中,如果线程1是晚于线程2执行,那么进入它不会进入循环体中;如果它早于线程2执行,那么它会进入到循环体中,condition_variable的wait()函数会对ul调用unlock()进行解锁,然后进入休眠状态。这个解锁操作很重要,因为它使得线程2能够获取锁资源,进而往list写入值。当线程2调用notify_one()后,线程1会从wait中醒来,并尝试获取锁。如果获取到锁,那么它将从wait函数中返回。因此线程1从循环体中出来后,它已经是加锁状态了,可以安全地访问alist变量。

如果有多个线程执行函数theadFun1,线程2 调用notify_one()后,只有一个线程能从wait()中醒来,

线程1必须先判断alist是否为空,如果为空才调用wait(),否则它将沉睡与wait中无法醒来,因为再也没有其他线程会用notify()唤醒它了。上面例子之所以用一个while循环而不是if,是因为存在虚假唤醒情景。

线程函数返回值

前面提及了std::thread::join并不像pthread_join那样,可以获取线程函数的返回值。但如果确实有需要获取线程函数的返回值,怎么才能做到呢?

考虑一个情景:有一个巨大的整型数组,现需要求数组元素的平均值。另开一个线程求平均值然后返回平均值是很自然的做法。但如何返回平均值是一个问题。

一个土方法是次线程将平均值写到一个全局变量中,然后主线程读取即可。但主线程怎么知道读取的是平均值,还是全局变量的初始化值。得有一个途径通知主线程,次线程已经将平均值写到全局变量了,你去读吧。怎么通知?当然可以用前面提及的条件变量,但这明显是杀鸡用牛刀。

那么是不是可以join呢?主线程等待次线程结束后,再去读取全局变量即可。join是等待次线程结束,而结束有很多种原因,比如正常结束和抛异常提前终止。对于后者,并不能保证join返回后,读取全局变量得到的就是平均值。

std::promise

C++11给出的解决方案是使用std::promise和std::future。

#include
#include
#include
#include
#include
#include

void threadFun(const std::vector<int> &big_vec, std::promise<double> prom)
{
    double sum = std::accumulate(big_vec.begin(), big_vec.end(), 0.0);

    double avg = 0;
    if (!big_vec.empty())
        avg = sum / big_vec.size();

    prom.set_value(avg);
}


int main()
{
    std::promise<double> prom;
    std::future<double> fu = prom.get_future();

    std::vector<int> vec{ 1, 2, 3, 4, 5, 6 };
    std::thread th(threadFun, std::ref(vec), std::move(prom));
    th.detach();

    double avg = fu.get();

    std::cout << "avg = " << avg << std::endl;

    return 0;
}

输出为:

avg = 3.5

在线程函数threadFun调用prom.set_value(avg)之前,主线程一直停留在fu.get()上。std::promise和std::future配套使用,往std::promise变量写入值,就能从其关联的std::future变量获取写入的值。明显,通过std::promise和std::future,很容易获取到线程的返回值。std::promise作为誓言,由次线程保证会写入值。std::future,主线程在未来某个时刻能够获取到值。这名字起得够意思。

但如果线程函数在调用set_value()之前就抛异常(或者return了),fu.get()能获取返回值吗?future和promise能否处理这种情况?

std::promise通过析构函数处理这种情况。如果在析构std::promise变量时,还没对std::pormise变量进行设置,那么析构函数就会为其关联的std::future存储一个std::future_error异常。此时,std::future的get()函数会抛出一个std::futre_error异常。

因此需要保证promise变量的生命周期要小于线程,否则无法利用std::promise的析构函数做一些保证。下面代码就是一个反例,主线程将永远停留在get()函数中。

#include
#include
#include
#include
#include
#include

void threadFun(const std::vector<int> big_vec, std::promise<double> &prom)
{
    double sum = std::accumulate(big_vec.begin(), big_vec.end(), 0.0);

    double avg = sum / big_vec.size();
}


int main()
{
    std::promise<double> prom;
    std::future<double> fu = prom.get_future();

    std::vector<int> vec{ 1, 2, 3, 4, 5, 6 };
    std::thread th(threadFun, vec, std::ref(prom));
    th.detach();

    double avg = fu.get();

    std::cout << "avg = " << avg << std::endl;

    return 0;
}

值得注意的是:std::future是一次性的。std::promise只能调用一次get_future,std::future也只能调用一次get()。 如果想在多个线程中共享一个std::promise的设置值,可以使用std::shared_future。

std::packaged_task

每次都需要在线程函数中增加一个额外的std::promise变量作为参数,这明显是破坏风水的。代码的阅读者也会觉得纳闷,为什么无端端会有这个参数的?有什么办法可以解决这个问题呢?

一个解决方案是对std::promise进行包装,增加一层抽象。在包装体里面有一个std::promise成员变量,并且包装体的构造函数需要用户定义的线程函数作为参数。当线程调用该包装体时,包装体先调用用户定义的线程函数,获取其返回值,最后将该返回值作为std::promise::set_value的参数。代码实现如下:

#include
#include
#include
#include
#include
#include

double calcAvg(const std::vector<int> &vec)
{
    double sum = std::accumulate(vec.begin(), vec.end(), 0.0);
    double avg = 0;
    if (!vec.empty())
        avg = sum / vec.size();

    return avg;
}

template<typename R, typename ... Args>
class PackageTask
{
private:
    using Fun = std::function;

public:
    template<typename ... Ts>
    explicit PackageTask(Ts&& ... args)
        : m_fun(std::forward(args)...)
    {}


    PackageTask(const PackageTask&) = delete;
    PackageTask& operator = (const PackageTask&) = delete;

    PackageTask(PackageTask &&) = default;
    PackageTask& operator = (PackageTask&&) = default;

    std::future get_future() { return m_prom.get_future(); }

    void operator ()(Args&& ... args)
    {
        R ret = m_fun(std::forward(args)...);
        m_prom.set_value(ret);
    }

private:
    std::promise m_prom;
    Fun m_fun;
};


int main()
{
    PackageTask<double, const std::vector<int>&> p(calcAvg);
    std::future<double> fu = p.get_future();

    std::vector<int> vec{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    std::thread th(std::move(p), std::ref(vec));
    th.detach();

    double avg = fu.get();
    std::cout << "avg = " << avg << std::endl;

    return 0;
}

例子输出:

avg = 5.5

C++11提供的std::packaged_task就是这样的一层封装,当然功能更加强大。搬一个en.cppreference.com的例子

#include 
#include 
#include 
#include 
#include 

// unique function to avoid disambiguating the std::pow overload set
int f(int x, int y) { return std::pow(x,y); }

void task_lambda()
{
    std::packaged_task<int(int,int)> task([](int a, int b) {
        return std::pow(a, b); 
    });
    std::future<int> result = task.get_future();

    task(2, 9);

    std::cout << "task_lambda:\t" << result.get() << '\n';
}

void task_bind()
{
    std::packaged_task<int()> task(std::bind(f, 2, 11));
    std::future<int> result = task.get_future();

    task();

    std::cout << "task_bind:\t" << result.get() << '\n';
}

void task_thread()
{
    std::packaged_task<int(int,int)> task(f);
    std::future<int> result = task.get_future();

    std::thread task_td(std::move(task), 2, 10);
    task_td.join();

    std::cout << "task_thread:\t" << result.get() << '\n';
}

int main()
{
    task_lambda();
    task_bind();
    task_thread();
}

有了std::packaged_task,线程函数就可以直接返回一个值。这样显得更加自然。从上面例子也可以看到,std::packaged_task并非一定要作为std::thread的参数,它完全可以在主线程中调用。

std::async

虽然有了std::packaged_task后,获取线程函数的返回值会简洁许多。但异步计算平均值还需要定义一个std::packaged_task和std::thread变量,增加了些许复杂度。能否在std::packaged_task和std::thread的基础上再进行一次封装,使得使用时更加简洁。当然是有的,std::async就是完成这样工作的。std::async无需考虑线程创建和线程间如何传值这些底层的问题,使用者只需专注于编写异步函数并返回适当的结果即可。

#include
#include
#include
#include
#include

double calcAvg(const std::vector<int> &vec)
{
    double sum = std::accumulate(vec.begin(), vec.end(), 0.0);
    double avg = 0;
    if (!vec.empty())
        avg = sum / vec.size();

    return avg;
}


int main()
{
    std::vector<int> vec{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    std::future<double> fu = std::async(calcAvg, std::ref(vec));

    double avg = fu.get();
    std::cout << "avg = " << avg << std::endl;

    return 0;
}

你可能感兴趣的:(C/C++,C++11)