C++实战专题-多线程编程基础

C++实战专题-多线程编程基础

文章目录

1 简介

1.1 学习多线程编程的意义

C++直到2011年C++标准委员会发布C++11标准以后,才正式支持多线程编程,在此之前都只能调用系统API进行多线程编程,对于跨平台应用程序,只能通过封装不同平台的API来实现跨平台编程,这和JAVA、C#天生支持多线程相比对于C++开发人员简直是一场噩梦。如今C++多线程他来了,他终于来了,姗姗来迟。随着国产化浪潮迅速升温,跨平台应用程序将越来越多,公司提供的多线程库功能过于单薄对于复杂业务场景实现起来很复杂、配套文档很少、无法查看源码,遇到问题无法通过百度解决。曾经不少应用程序使用了系统API,这对于应用程序跨平台带来了很大麻烦,所以从现在起我们需要摒弃使用平台API创建多线程的做法,转而使用C++11标准库的多线程接口编写多线程程序。C++标准库多线程设计到的知识点非常之多,没有那么多时间、精力全部展开来讲,所以本文将通过一些小的示例将多线程常用的一些知识点讲清楚,也算是自我学习的一次总结,希望读者也能有所收获。

1.2 多线程的基本概念

多线程编程相关的概念很多,包括线程、进程、并发和并行等,对这些概念我们需要有一个基本的了解,请看下面表格的名词说明。

名词说明
并发当有两个或更多的任务同时发生,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发。
并行在操作系统中,若干个程序段同时在系统中运行,这些程序的执行在时间上是重叠的,一个程序段的执行尚未结束,另一个程序段的执行已经开始,无论从微观还是宏观上看,程序都是一起执行的。也就是说并发是指在同一个时间段内,两个或多个程序执行,有时间上的重叠,这种方式我们称之为并行。
进程进程就是正在运行的一个可执行程序,打开我们的任务管理器,在进程中可以看到各式各样的进程,微信,网易云音乐都是一个进程,程序一旦被执行,那么他就是一个进程。进程是系统进行资源分配和调度的单位。
线程线程是进程中执行的道路,进程中的多个线程就是并发在执行的。一个进程可以包含有多个线程,但至少有一个主线程,线程是操作系统可识别的最小执行和调度单位。计算机将资源分配给进程,进程中的线程共享这些资源,所以利用多线程可以极大的提高进程的运行效率。

表1.2-1 多线程相关概念

2 多线程的基本使用

2.1 创建线程方法介绍

2.1.1 全局函数创建线程

#include <iostream>
#include <thread>
#include <string>

using namespace std;

void threadFunc1()
{
    cout << "The thread1 starts running!" << endl;
    // 执行业务逻辑
    cout << "The thread1 end running!" << endl;
}

void threadFunc2(int number)
{
    cout << "The thread2 starts running!" << endl;
    for (int index = 0; index < number; ++index)
    {
        cout << "The thread is working!"<< endl;
    }
    cout << "The thread2 end running!" << endl;
}

void threadFunc3(int& number)
{
    cout << "The thread3 starts running!" << endl;
    number = 666;
    cout << "The thread3 end running!" << endl;
}

int main(int argc, char *argv[])
{
    // 传递无参数函数
    std::thread th1(threadFunc1);
    th1.join();

    // 传递有参函数
    std::thread th2(threadFunc2, 2000);
    th2.join();

    // 按照引用传递有参函数
    int num = 100;
    std::thread th3(threadFunc3, std::ref(num));
    th3.join();
    cout << "num: " << num << endl;

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例2.1.1-1全局函数创建线程示例

​ Thread构造出对象以后线程就已经开始运行了,通过调用join()函数阻塞程序等待线程执行完成,避免在主线程退出时,子线程还未执行完成主线程退出导致程序异常。本示例展示了通过传递无参数函数、传递有参函数和按照引用传递有参函数三种方式创建多线程,基本涵盖了大部分应用场景。

2.1.2 类对象创建线程

#include <iostream>
#include <thread>
#include <string>

using namespace std;

class Business
{
public:
    Business(int value) : m_nValue(value){}

    void operator()()
    {
        cout <<"[O]Thread running!value="<<m_nValue<< endl;
    }

    void printfFunc1(int number)
    {
        cout <<"[P]Thread running!value="<<number<< endl;
    }

    void printfFunc2(int& number)
    {
        number = 888;
    }

private:
    int m_nValue;
};

int main(int argc, char *argv[])
{
    Business bs(100);
    // 类对象仿函数作为线程体
    std::thread th(bs);
    th.join();

    // 类对象成员函数作为线程体,并按值传递参数
    std::thread pf1(&Business::printfFunc1, &bs, 200);
    pf1.join();

    // 类对象成员函数作为线程体,并按引用传递参数
    int num = 111;
    std::thread pf2(&Business::printfFunc2, &bs, std::ref(num));
    pf2.join();
    cout << "num: " << num << endl;

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例2.1.2-1类对象创建线程示例

该示例展示了通过调用类对象的仿函数和成员函数创建线程的方法,这种方式在实际开发中使用的最普遍,需要我们重点掌握。如果我们的线程需要调用当前类的成员函数作为函数体,第二个参数我们可以直接传this指针。

2.1.3 用lambda表达式创建线程

#include <iostream>
#include <thread>
#include <string>

using namespace std;

int main(int argc, char *argv[])
{
    auto function1 = [] {
        cout << "Thread running!" << endl;
    };

    std::thread th1(function1);
    th1.join();

    auto function2 = [](int& a) {
        a = 100;
    };

    int num = 200;
    std::thread th2(function2, std::ref(num));
    th2.join();
    cout << "num: " << num << endl;

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例2.1.3-1 lambda表达式创建线程示例

​ 本文通过不带参数和带参数的lambda表达式创建多线程,当需要传递引用参数时必须使用std::ref()进行传递,否则编译会报错。

2.2 线程管理接口说明

2.2.1 join()和joinable()函数使用说明

主线程和子线程都是独立运行的所以可能会出现主线程先运行结束、子线程先运行结束和主子线程同时结束这三种情况,对于主线程先运行结束的这种情况程序极易发生异常,所以想写出一个稳定的多线程程序,我们必须保证子线程比主线程先退出。join()函数是一个等待子线程执行完成的函数,只有子线程完成以后主线程程序才会接着往下运行。joinable()是一个判断线程是否能被join的函数,一个线程只能被join()一次,调用多次会崩溃,一个线程被调用detach()以后同样不能再调用join()函数。join()函数的正确用法是,先调用joinable()进行判断,如果返回true则调用join(),否则不能。

#include <iostream>
#include <thread>
#include <string>

using namespace std;

void threadFunc()
{
    cout << "The thread starts running!" << endl;
    // 执行业务逻辑
    cout << "The thread end running!" << endl;
}

int main(int argc, char *argv[])
{
    std::thread th(threadFunc);
    if (th.joinable())
    {
        th.join();
    }

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例2.2.1-1 join()和joinable()使用示例

​ 上面的示例展示了如何正确使用thread的join()函数,调用join()函数之前先通过joinable()判断当前线程是否可以join()。

2.2.2 detach()函数使用说明

detach()称为分离线程函数,使用detach()函数会让线程在后台运行,即说明主线程不会等待子线程运行结束才结束,程序一旦调用detach()函数以后,不能再调用线程的join()函数,否则程序会崩溃。

#include <iostream>
#include <thread>
#include <string>

using namespace std;

void threadFunc()
{
    cout << "The thread starts running!" << endl;
    // 执行业务逻辑
    cout << "The thread end running!" << endl;
}

int main(int argc, char *argv[])
{
    std::thread th(threadFunc);
    th.detach();

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例2.2.2-1 detach()使用使用示例

一般情况下不要使用detach()函数,要想程序稳定运行我们需要保证thread析构之前运行完,在一些特殊的情况下我们可以detach(),比如说运行一个独立的线程清理系统缓存,检测系统目录,这种情况下线程体没有使用到成员变量,不存在引用的变量失效导致程序崩溃的问题。

2.2.3 get_id()函数使用说明

线程id是个数字,每个线程(不管是主线程还是子线程)实际上都对应着一个数字,而且每个线程对应的这个数字都不一样,线程id可以用C++标准库里的函数来std::this_thread::get_id()来获取。

2.3 线程易错点总结

我们在创建线程以后,需要明确是要调用join()等待线程执行结束,还是调用detach()让其自主运行,如果std::thread对象销毁之前还没有做出决定,std::thread的析构函数会调用std::terminate()让程序终止。因此我们在线程对象析构之前,必须做出选择。如果不等待线程,就必须保证线程结束之前,线程可访问数据的有效性,否则会导致应用程序的异常终止。

3 互斥量std::mutex使用介绍

3.1 互斥量std::mutex的基本用法

std::mutex类是能用于保护共享数据免受从多个线程同时访问的同步手段,我们称之为互斥量。互斥量就是个类对象,我们可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。我们在使用互斥量的时候要小心,保护数据不多也不少,少了达不到效果,多了影响效率。

#include <iostream>
#include <map>
#include <string>
#include <chrono>
#include <thread>
#include <mutex>

using namespace std;

std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex;

void save_page(const std::string &url)
{
    // 模拟长页面读取
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::string result = "fake content";

    g_pages_mutex.lock();
    g_pages[url] = result;
    g_pages_mutex.unlock();
}

int main()
{
    std::thread t1(save_page, "http://foo");
    std::thread t2(save_page, "http://bar");
    t1.join();
    t2.join();

    // 现在访问g_pages是安全的,因为线程t1/t2生命周期已结束
    for (const auto &pair : g_pages) 
    {
        std::cout << pair.first << " => " << pair.second << endl;
    }

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.1-1互斥量std::mutex的基本用法示例

3.2 std::lock_guard类模板使用

既然有了std::mutex为什么又要整出来一个std::lock_guard?因为std::mutex需要我们手动lock()和unlock(),在代码分支较多时很容易忘记解锁造成死锁,所以一般在实际开发中我们直接使用std::mutex的情况比较少,一般我们都是配合std::lock_guard或者std::unique_lock进行使用。std::lock_guard这个类是一个互斥量的包装类,用来提供自动为互斥量上锁和解锁的功能,简化了多线程编程。std::lock_guard构造函数执行了std::mutex::lock(),在作用域结束时,调用析构函数执行mutex::unlock(),这样就能避免忘记调用mutex::unlock()而导致死锁的情况。

#include <thread>
#include <mutex>
#include <iostream>

using namespace std;

int g_count = 0;
std::mutex g_count_mutex;

void threadBody()
{
    std::lock_guard<std::mutex> lock(g_count_mutex);
    ++g_count;
    
cout << "ThreadID: " << std::this_thread::get_id() 
<<", " << g_count << endl;
}

int main()
{
    std::cout << "main: " << g_count << endl;

    std::thread t1(threadBody);
    std::thread t2(threadBody);

    t1.join();
    t2.join();

    cout << "main: " << g_count << endl;

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.2-1 std::lock_guard类模板使用示例

std::lock_guardstd::mutex创建lock对象以后就会自动调用std::mutex的lock()函数,当lock对象离开作用域析构的时候就会自动调用std::mutex的unlock()函数,这是对std::mutex的一种改进,避免了单独使用std::mutex容易忘记调用unlock()的问题,在实际开发中配合std::lock_guard使用是最常用的做法,他能满足我们绝大多数业务场景。

3.2.1 std::lock_guard的std::adopt_lock参数

std::adopt_lock是个结构体对象,起一个标志作用,表示这个互斥量已经lock(),不需要在std::lock_guardstd::mutex构造函数里对std::mutex对象进行再次lock()了。如果std::lock_guard创建对象时使用了std::adopt_lock参数,一定要确保std::mutex已经加锁了,否则程序运行会报错。

#include <thread>
#include <mutex>
#include <iostream>

using namespace std;

std::mutex mux;
int num = 0;

void func()
{
    // 必须先lock,否则报错
    mux.lock();
    // 处理业务逻辑
    std::lock_guard<std::mutex> lock(mux, std::adopt_lock);
    ++num;
    cout << "num: " << num << endl;
}

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

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

std::lock_guard的std::adopt_lock参数使用场景是程序已经使用mutex.lock()锁定互斥量,但是调用mutex.unlock()的场景比较复杂、逻辑分支较多容易出错的场景,可以通过将std::mutex托管给lock_guard实现自动解锁。

3.3 std::unique_lock类模板使用

既然有了std::lock_guard为什么又又冒出来一个std::unique_lock?这是因为std::lock_guard只能在构造函数中调用lock(),在析构中调用unlock(),我们认为它还不够灵活,我们希望有一种既能具有std::lock_guard的优点又能更灵活的调用lock()和unlock()的东西,所以std::unique_lock应运而生了。std::unique_lock灵活性提升付出的代价是效率上的损耗,因为它内部需要维护锁的状态,所以效率要比std::lock_guard低一点,因此在实际应用的过程中,如果不是十分必要的,我们选用std::lock_guard足以。

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>

using namespace std;

class MessageQueue
{
public:

    void receiveMessageQueue()
    {
        for (int i = 0; i < 10000; i++)
        {
            cout << "receiveMessageQueue()插入一个元素:" << i << endl;
    
            std::unique_lock<std::mutex> sbguard(msgMutex);
            msgRecvQueue.push_back(i);
        }
    }

    bool processMessageCallback(int &command)
    {
        std::unique_lock<std::mutex> sbguard(msgMutex);
        if (!msgRecvQueue.empty())
        {
            int command = msgRecvQueue.front();
            msgRecvQueue.pop_front();
                                     
            return true;
        }

        return false;
    }

    void dispatchMessageQueue()
    {
        int command = 0;
        for (int i = 0; i < 10000; i++)
        {
            bool result = processMessageCallback(command);
            if (result == true)
            {
                cout << "dispatchQueue()取出一个元素" << endl;
            }
            else
            {
                cout << "receiveQueue()目前消息队列为空" << i << endl;
            }
        }
        cout << "end!" << endl;
    }

private:
    std::list<int> msgRecvQueue;
    std::mutex msgMutex;
};

int main()
{
    MessageQueue mq;

   thread dispatchThread(&MessageQueue::dispatchMessageQueue, &mq);
   thread receiveThread(&MessageQueue::receiveMessageQueue, &mq);

    dispatchThread.join();
    receiveThread.join();

    cout << "主线程执行!" << endl;

    return 0;
}

std::unique_lock默认参数情况下和std::lock_guard没什么区别,两者可以互换使用,但是加上第二参数以后,情况发生了变化,下面我们来详细讲讲unique_lock的第二个参数。

3.3.1 std::unique_lock的std::adopt_lock参数

std::unique_lock第二参数带std::adopt_lock标记,意思就是告诉std::unique_lock不要在构造函数中lock()这个mutex,因为这个mutex已经被lock()了。切忌如果std::unique_lock使用了std::adopt_lock标记,一定要确保mutex已经上锁了,否则程序运行时会崩溃。

#include <thread>
#include <mutex>
#include <iostream>

using namespace std;

std::mutex mux;
int num = 0;

void func()
{
    //必须先lock,否则程序会崩溃
    mux.lock();
    // do something
    std::unique_lock<std::mutex> lock(mux, std::adopt_lock);
    ++num;
    cout << "num: " << num << endl;
}

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

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.3.1-1 std::unique_lock的std::adopt_lock使用示例

3.3.2 std::unique_lock的std::defer_lock参数

std::defer_lock的意思就是构造函数没有给std::mutex加锁,初始化了一个没有加锁的std::mutex,在后续需要加锁的地方手动上锁,std::unique_lock出作用域以后自动解锁。

#include <thread>
#include <mutex>
#include <iostream>

using namespace std;

std::mutex mux;
int num = 0;

void func()
{
    // 创建一个不加锁的unique_lock对象,在需要时加锁
    std::unique_lock<std::mutex> lock(mux, std::defer_lock);
    lock.lock();
    ++num;
    cout << "num: " << num << endl;
}

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

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.3.2-1 std::unique_lock的std::defer_lock使用示例

3.3.3 std::unique_lock的std::try_to_lock参数

std::try_to_lock表示尝试性对std::mutex进行加锁,如果加锁成功就返回true,如果加锁失败就不阻塞而直接返回false。

#include <thread>
#include <mutex>
#include <iostream>

using namespace std;

std::mutex mux;
int num = 0;

void func()
{
    // 尝试去获取一把锁
    std::unique_lock<std::mutex> templock(mux, std::try_to_lock);
    if (templock.owns_lock())
    {
        cout << "Lock attempt succeeded" << endl;
    }
    ++num;
    cout << "num: " << num << endl;
}

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

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.3.3-1 unique_lock的try_to_lock使用示例

3.3.4 unique_lock的成员函数

  • lock()函数

为std::unique_lock关联的互斥量上锁,等同于mutex()->lock(),一般配合unique_lock的std::defer_lock参数使用。

//创建一个没有加锁的templock
std::unique_lock<std::mutex> templock(_mutex, std::defer_lock);

//对templock进行加锁操作
templock.lock();

​ 示例3.3.4-1 unique_lock的lock()函数使用示例

  • unlock()函数

unlock()函数为std::unique_lock关联的互斥量解锁,等同于mutex()->unlock(),一般配合std::unique_lock的std::defer_lock参数使用。

//创建一个没有加锁的templock
std::unique_lock<std::mutex> templock(_mutex, std::defer_lock);

//对templock进行加锁操作
templock.lock();

// 处理共享代码……
tmplock.unlock();

// 处理非共享代码……

​ 示例3.3.4-2 unique_lock的unlock()函数使用示例

  • try_lock()函数

try_lock()函数和lock()函数的作用是一样的,都是给互斥量上锁,不同的是lock()如果获取不到互斥量会一直阻塞直到获取到互斥量,而try_lock()是尝试获取互斥量,如果获取不到互斥量会立即返回不阻塞。

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

volatile int g_count(0);

class Business
{
public:
    void testLock()
    {
        for (int i = 0; i < 10000; ++i)
        {
            std::unique_lock<std::mutex> unique_mtx(m_mtx);
            ++g_count;
        }
    }

    void testTryLock()
    {
        for (int i = 0; i < 10000; ++i)
        {
            std::unique_lock<std::mutex> unique_mtx(m_mtx, std::defer_lock);
            if (unique_mtx.try_lock())
            {
                ++g_count;
            }
        }
    }

private:
    std::mutex m_mtx;
};

int main(int argc, char *argv[])
{
    Business bs;
    thread testTryLocktd[10];
    for (int i = 0; i < 10; ++i)
    {
        testTryLocktd[i] = thread(&Business::testTryLock, &bs);
    }
    for (auto &th : testTryLocktd)
    {
        th.join();
    }
    cout << g_count << endl;

    g_count = 0;
    thread testLockth[10];
    for (int i = 0; i < 10; ++i)
    {
        testLockth[i] = thread(&Business::testLock, &bs);
    }
    for (auto &th : testLockth)
    {
        th.join();
    }
    cout << g_count << endl;

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.3.4-3 unique_lock的try_lock()函数使用示例

这个程序执行多次我们会发现lock()的效率比try_to_lock()更高,所以一般情况下尽量采用lock()加锁。

在这里插入图片描述

​ 图3.3.4-4 unique_lock的try_lock()函数示例执行结果

  • try_lock_for()函数

try_lock_for()函数需要unique_lock搭配std:: timed_mutex互斥量进行使用,mutex在没有获取到锁时线程会一直阻塞,timed_mutex带超时功能,在规定的等待时间内,没有获取锁,线程不会一直阻塞,代码会继续执行。

#include <iostream>
#include <chrono>    
#include <thread>  
#include <mutex>

using namespace std;

std::timed_mutex mtx;

void fireworks(int i)
{
    // 等待锁定:每个线程每200ms打印一次”-“
    while (!mtx.try_lock_for(std::chrono::milliseconds(200))) 
    {
        std::cout << i;
    }
    // 有锁了等待1s,然后该线程打印“*”
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    std::cout << "* thread " << i << "\n";
    mtx.unlock();
}

int main()
{
    std::thread threads[10];
    // 生成10个线程
    for (int i = 0; i < 10; ++i)
    {
        threads[i] = std::thread(fireworks, i);
    }

    for (auto& th : threads)
    {
        th.join();
    }

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.3.4-7 unique_lock的swap ()函数使用示例

  • try_lock_until()函数

try_lock_until()函数需要unique_lock搭配std:: timed_mutex互斥量进行使用,它尝试锁定互斥锁直到某一个时间点。

#include <thread>
#include <mutex>
#include <iostream>

using namespace std;

std::timed_mutex mux;
int num = 0;

void func()
{
    auto now = std::chrono::high_resolution_clock::now();
    std::unique_lock<std::timed_mutex> lock(mux, std::defer_lock);
    if (lock.try_lock_until(now + std::chrono::milliseconds(15)))
    {
        cout << "正在处理共享代码......" << endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));

        lock.unlock();
    }
}

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

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.3.4-6 unique_lock的try_lock_until()函数使用示例

  • swap()函数

swap()函数将关联的互斥量和所有权状态与指定对象的状态进行交换。

#include <thread>
#include <mutex>
#include <iostream>

using namespace std;

std::mutex mux1;
std::mutex mux2;

int num = 0;

void func()
{
    std::unique_lock<std::mutex> lock1(mux1);
    std::unique_lock<std::mutex> lock2(mux2);
    lock1.swap(lock2);

    ++num;
    cout << "num: " << num << endl;
}

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

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.3.4-7 unique_lock的swap ()函数使用示例

  • release()函数

release()函数返回它所管理的mutex对象指针并释放所有权,也就是unique_lock和mutex不在有关联。严格区分release()和unlock()这两个成员函数的区别,unlock()只是让unique_lock所管理的mutex解锁而不是解除两者的关联关系。

std::unique_lock<std::mutex> sbguard(mux);
std::mutex *ptx = sbguard.release();

​ 示例3.3.4-8 unique_lock的swap ()函数使用示例

此时sbguard和mux的关联关系解除了,ptx现在就有责任自己解锁mux了。

  • owns_lock()函数

owns_lock()函数用于判断std::unique_lock管理的互斥量是否拥有锁。

#include <thread>
#include <mutex>
#include <iostream>
#include <vector>

using namespace std;

std::mutex mux;
int num = 0;

void func()
{
    std::unique_lock<std::mutex> lock(mux, std::try_to_lock);
    ++num;
    if (lock.owns_lock())
    {
        cout << "*";
    }
    else
    {
        cout << "x";
    }
}

int main()
{
    std::vector<std::thread> threads;
    for (int i = 0; i < 500; ++i)
    {
        threads.emplace_back(func);
    }

    for (auto& th : threads)
    {
        th.join();
    }

    cout << endl;
    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.3.4-9 std::unique_lock的owns_lock() ()函数使用示例

std::unique_lock这些成员函数并不复杂,但是在什么场景下使用这些函数,需要我们反复斟酌。使用std::unique_lock并对互斥量lock之后,可以随时unlock。当需要访问共享数据的时候,我们可以再次调用lock来加锁,而笔者要强调的是,lock之后不需要再次unlock,即便忘记了unlock也无关紧要,std::unique_lock会在离开作用域的时候检查关联的std::mutex是否lock,如果lock,std:::unique_lock会自动unlock。

3.3.5 unique_lock的所有权转移

一般来说,std::unique_lock和std::mutex是绑定在一起的,std::mutex是通过std::unique_lock来管理的。

#include <iostream>
#include <string>
#include <thread>
#include <mutex>

using namespace std;

int i = 0;
std::mutex mux;

void func1(int a) 
{
    while (1) 
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(400));
        std::unique_lock<std::mutex> unilock(mux);
        //所有权传递,类似智能指针,
        //std::unique_lock<std::mutex> nj(unilock); 这样不行,不能复制
        std::unique_lock<std::mutex> newunilock(std::move(unilock));
        i++;
        std::cout << i << std::endl;
        std::cout << "Sub thread id = " << std::this_thread::get_id()  << std::endl;
    }
}

void func2(int a) 
{
    while (1)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(400));
        std::unique_lock<std::mutex> unilock(mux);
        i--;
        std::cout << i << std::endl;
        std::cout << "Sub thread id = " << std::this_thread::get_id() << std::endl;
    }
}

int main()
{
    std::thread o1(func1, 1);
    std::thread o2(func2, 1);
    o1.join();
    o2.join();
    cout << "Main thread = " << std::this_thread::get_id() << endl;

    return 0;
}

​ 示例3.3.4-9 std::unique_lock的所有权转移使用示例

3.4 std::lock_guard和std::unique_lock的区别

std::lock_guard是RAII模板类的简单实现,功能简单,在构造函数中进行加锁,析构函数中进行解锁。std::unique_lock是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。std::unique_lock比std::lock_guard使用更加灵活,功能更加强大。使用std::unique_lock需要付出更多的时间、性能和内存成本,在效率上std::lock_guard更胜一筹。

3.5 std::lock()用法

std::lock()可以同时锁定给定的多个可锁对象 lock1、lock2、… 、lockn,内部用免死锁算法避免死锁。如果互斥量中有一个没锁住,它就阻塞等待,当所有互斥量都锁住,才能继续往下执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)。

#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <functional>
#include <chrono>
#include <string>

using namespace std;

class Employee 
{
public:
    Employee(std::string id) : id(id) 
    {
    }

    std::string output() const
    {
        std::string ret = "Employee " + id + " has lunch partners: ";
        for (const auto& partner : lunch_parameter)
        {
            ret += partner + " ";
        }
            
        return ret;
    }

    std::string getid()
    {
        return id;
    }

public:
    std::mutex m;
    std::vector<std::string> lunch_parameter;

private:
    std::string id;
};

void sendEmail(Employee &e1, Employee &e2)
{
    // 模拟耗时的发邮件操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
}

void workFunc(Employee &e1, Employee &e2)
{
    static std::mutex io_mutex;
    {
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << e1.getid() << " and " << e2.getid() 
                  << " are waiting for locks" << std::endl;
    }

    // 用lock获得二个锁,而不担心对assign_lunch_partner的其他调用会死锁
    {
        std::lock(e1.m, e2.m);
        std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
        std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
        {
            std::lock_guard<std::mutex> lk(io_mutex);
            std::cout << e1.getid() << " and " << e2.getid() 
<< " got locks" << std::endl;
        }
        e1.lunch_parameter.push_back(e2.getid());
        e2.lunch_parameter.push_back(e1.getid());
    }
    sendEmail(e1, e2);
    sendEmail(e2, e1);
}

int main(int argc, char *argv[])
{
Employee alice("alice"), bob("bob");
Employee christina("christina"), dave("dave");

    // 在平行线程指派,因为发邮件给用户告知午餐指派,会消耗长时间
    std::vector<std::thread> threads;
    threads.emplace_back(workFunc, std::ref(alice), std::ref(bob));
    threads.emplace_back(workFunc, std::ref(christina), std::ref(bob));
    threads.emplace_back(workFunc, std::ref(christina),
std::ref(alice));
    threads.emplace_back(workFunc, std::ref(dave), std::ref(bob));

    for (auto &thread : threads)
    {
        thread.join();
    }
    std::cout << alice.output() << endl
               << bob.output() << endl
               << christina.output() << endl 
               << dave.output() << endl;

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例3.5-1 std::lock()用法示例

3.6 互斥量死锁问题总结

程序中使用多个互斥量时,如果允许一个线程一直占有第一个互斥量A,并且试图锁住第二个互斥量B时处于阻塞状态,但是拥有第二个互斥量B的线程也在试图锁住第一个互斥量A,这就形成了循环等待的条件,最终导致死锁产生。因为两个线程都在互相请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是产生死锁。解决死锁的一条准则是保证互斥量在多个线程中lock的顺序一致,一般情况下就不会死锁***。

4 std:: call_once()的使用

4.1 std::call_once()的基本用法

std::call_once()是一个函数模板,该函数的第一个参数为标记,第二个参数是一个函数名。它的主要功能是能够保证绑定的函数只被调用一次,即便是在多线程的环境下也是如此。具备互斥量的能力,而且比互斥量消耗的资源更少、更高效。**std::call_once()需要与std:😗*once_flag标记结合使用,**std:😗*once_flag是一个结构,**std:😗*call_once()就是通过标记来决定函数是否执行,调用成功后,就把标记设置为一种已调用状态。我们在开发过程中有些函数在整个程序的生命周期中只需要调用一次,这种我们一般情况下是通过设置标记位来实现的,如今C++11标准为我们引入了新的机制来解决这个问题。

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;
std::once_flag flag1, flag2;

void simple_do_once()
{
    std::call_once(flag1, []() {
        cout << "Simple example: called once" << endl;
    });
}

void throwfunction(bool bThrow)
{
    if (bThrow)
{
	 // 这会出现多于一次
        cout << "throw: call_once will retry." << endl; 
        throw std::exception();
    }
cout << "Didn't throw, call_once will not attempt again." 
<< endl; // 保证一次
}

void do_once(bool bThrow)
{
    try 
    {
        std::call_once(flag2, throwfunction, bThrow);
    }
    catch (...) 
    {
        cout << "Throw other exceptions." << endl;
    }
}

int main(int argc, char *argv[])
{
    std::thread st1(simple_do_once);
    std::thread st2(simple_do_once);
    std::thread st3(simple_do_once);
    std::thread st4(simple_do_once);
    st1.join();
    st2.join();
    st3.join();
    st4.join();

    std::thread t1(do_once, true);
    std::thread t2(do_once, true);
    std::thread t3(do_once, false);
    std::thread t4(do_once, true);
    t1.join();
    t2.join();
    t3.join();
    t4.join();

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例4.1-1 std::call_once()用法示例

4.2 std::call_once()在单例模式中的应用

在以往我们实现单例的过程中都是通过私有化构造函数并加锁的方式实现单例类的实例化的,如今我们使用call_once()来实现单例类实例的创建。下面请看示例代码:

#include <iostream>
#include <mutex>
#include <vector>

using namespace std;

static once_flag g_flag;

class Singleton
{
public:

    //两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕
    static Singleton* getInstance()
    {
        std::call_once(g_flag, createInstance);
        return instance;
    }

    void print()
    {
		
    }

protected:
    static void createInstance()//call_once保证其只被调用一次
    {
        cout << "Create instance" << endl;
        static ObjectRelease _release;
        instance = new Singleton();
    }

    class ObjectRelease
    {
        public:
            ~ObjectRelease()
            {
                if (Singleton::instance)
                {
                    delete Singleton::instance;
                    Singleton::instance = nullptr;
                }
            }
    };

public:
    Singleton() {}

private:
    static Singleton* instance;

};
Singleton* Singleton::instance = nullptr;

void createInstance()
{
    cout << "Start creating instance." << endl;
    Singleton::getInstance();
    cout << "End creating instance." << endl;
}

int main(void)
{
    std::vector<std::thread> threads;
    Singleton* instance = Singleton::getInstance();
    for (int index = 0; index < 10; ++index)
    {
        threads.push_back(std::thread(createInstance));
    }

    for (auto &item : threads)
    {
        item.join();
    }

    Singleton* instance1 = Singleton::getInstance();
    Singleton* instance2 = Singleton::getInstance();
    if (instance1 == instance2)
    {
        cout << "They are the same instance." << endl;
    }
    else
    {
        cout << "They are not the same instance." << endl;
    }

    cout << "Press 'q' to quit" << endl;
    while (std::cin.get() != 'q');

    return 0;
}

​ 示例4.2-1 std::call_once()在单例模式中的应用示例

当前示例使用std::call_once()实现单例模式,并且使用内部类解决了单例对象内存的释放问题,当前单例模式实现方式可以作为单例类的通用模板在实际开发中进行复用。

5 std::condition_variable条件变量

5.1 std::condition_variable基本概念

std::condition_variable是条件变量,当std::condition_variable对象的某个wait()函数被调用的时候,它使用std::unique_lock(通过std::mutex) 来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的std::condition_variable对象上调用了notify函数来唤醒当前线程。std::condition_variable对象通常使用std::unique_lockstd::mutex来等待,如果需要使用另外的lockable类型,可以使用std::condition_variable_any类,本文后面会讲到std::condition_variable_any的用法。
在这里插入图片描述
图5.1-1 条件变量的wait线程基本流程

5.2 std::condition_variable成员方法介绍

5.2.1 std::condition_variable成员函数wait()

当前线程调用wait()后将被阻塞,直到另外某个线程调用notify_one或者notify_all唤醒当前线程;当线程被阻塞时,该函数会自动调用std::mutex的unlock()释放锁,使得其它被阻塞在锁竞争上的线程得以继续执行。一旦当前线程获得通知(notify,通常是另外某个线程调用notify_one或者notify_all唤醒了当前线程),wait()函数也是自动调用std::mutex的lock()。wait分为无条件被阻塞和带条件的被阻塞两种。

无条件被阻塞:调用该函数前,当前线程应该已经对unique_lock lck完成了加锁。所有使用同一个条件变量的线程必须在wait()函数中使用同一个unique_lock。该wait()函数内部会自动调用lck.unlock()对互斥锁解锁,使得其他被阻塞在互斥锁上的线程恢复执行。使用本函数被阻塞的当前线程在获得通知(notified,通过别的线程调用notify_one或者notify_all函数)而被唤醒后,wait()函数恢复执行并自动调用lck.lock()对互斥锁加锁。

带条件的被阻塞:wait()函数设置了谓词(Predicate),只有当pred条件为false时调用该wait()函数才会阻塞当前线程,并且在收到其它线程的通知后只有当pred为true时才会被解除阻塞。因此,等效于while (!pred()) wait(lck)。

std::mutex mutex;
std::condition_variable cv;

// 条件变量与临界区有关,用来获取和释放一个锁,因此通常会和mutex联用。
std::unique_lock lock(mutex);
// 此处会释放lock,然后在cv上等待,直到其它线程通过cv.notify_xxx来唤醒当前线程,cv被唤醒后会再次对lock进行上锁,然后wait函数才会返回。
// wait返回后可以安全的使用mutex保护的临界区内的数据。此时mutex仍为上锁状态
cv.wait(lock)

​ 示例5.2.1-1 std::condition_variable成员函数wait()使用示例

5.2.2 std::condition_variable成员函数wait_for()

与wait()类似,只是wait_for()可以指定一个时间段,在当前线程收到通知或者指定的时间超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了其它线程的通知,wait_for()返回,剩下的步骤和wait()类似。

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>

using namespace std;

condition_variable cv;
int value;

void do_read_value()
{
    cin >> value;
    //只有键盘敲入一个字符,才往下执行cv.notify_one();
    cv.notify_one();
}

int main ()
{
    cout << "Please, enter an integer (I'll be printing dots): \n";
    thread th(do_read_value);
    mutex mtx;
    //加锁互斥量
unique_lock<mutex> lck(mtx);

    //这里wait_for堵塞到这一行,解锁互斥量。
    //当超时1s的时候,相当于收到了通知信号,就被唤醒干活了。 加锁互斥量
    //while语句满足就执行打印.
    //然后再次循环再wait等待1s,循环反复。
    //收到notify_one()的时候,不满足cv_status::timeout,就会退出循环。
    //这个时候不断尝试加锁互斥量,加锁成功往下执行。加锁不成功不断尝试加锁。
    while (cv.wait_for(lck,chrono::seconds(1)) == cv_status::timeout)
    { 
            cout << '.';
            cout.flush();
    }                                                                                               cout << "You entered: " << value << '\n';
    th.join();
    return 0;
}

​ 示例5.2.2-1 std::condition_variable成员函数wait_for()使用示例

5.2.3 std::condition_variable成员函数wait_until()

与wait_for类似,只是wait_until可以指定一个时间点,在当前线程收到通知或者指定的时间点超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了其它线程的通知,wait_until返回,剩下的处理步骤和wait类似。

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <condition_variable>
#include <ctime>
#include <iomanip>

using namespace std;

std::mutex g_mutex;
std::condition_variable g_cv_test;
bool g_btest = false;

void thread_wait_until()
{
    cout << "condition variable wait_until function test." << endl;
    unique_lock<std::mutex> lk(g_mutex);
    auto now_time = chrono::system_clock::now();
    time_t ntime = chrono::system_clock::to_time_t(now_time);
    cout << "current time:" << put_time(localtime(&ntime), "%F %T") << endl;
    g_cv_test.wait_until(lk, now_time + chrono::seconds(10), []() { return g_btest; });

    now_time = chrono::system_clock::now();
    ntime = chrono::system_clock::to_time_t(now_time);
    cout << "wait_until continue." << endl;
    cout << "current time:" << put_time(localtime(&ntime), "%F %T") << endl;
}

void thread_notify_one()
{
    cout << "thread notify one start." << endl;
    this_thread::sleep_for(chrono::seconds(5));
    cout << "notify wait thread continue." << endl;
    unique_lock<mutex> lk(g_mutex);
    g_btest = true;
    g_cv_test.notify_one();
}

int main(int argc, char** argv)
{
    thread trd_wait_until(thread_wait_until);
    thread trd_cvtest_notifyone(thread_notify_one);
    trd_wait_until.join();
    trd_cvtest_notifyone.join();

    system("pause");

    return 0;
}

​ 示例5.2.3-1 std::condition_variable成员函数wait_until ()使用示例

5.2.4 std::condition_variable成员函数notify_one()

唤醒某个wait线程,如果当前没有等待线程,则该函数什么也不做;如果同时存在多个等待线程,则唤醒某个线程是不确定的。

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <condition_variable>

using namespace std;

std::mutex g_mutex;
std::condition_variable g_cv_test;
bool g_btest = false;

void thread_wait_for()
{
    cout << "wait_for function test." << endl;
    unique_lock<std::mutex> lk(g_mutex);
    while (!g_cv_test.wait_for(lk, std::chrono::seconds(5), []() {return g_btest; }))
    {
        cout << "wait_for thread waiting……" << endl;
    }

    cout << "wait_for continue." << endl;
}

void thread_notify_one()
{
    cout << "thread notify one start." << endl;
    this_thread::sleep_for(chrono::seconds(5));
    cout << "notify wait thread continue." << endl;
    unique_lock<mutex> lk(g_mutex);
    g_btest = true;
    g_cv_test.notify_one();
}

int main(int argc, char** argv)
{
    thread trd_wait_for(thread_wait_for);
    this_thread::sleep_for(std::chrono::seconds(20));
    thread trd_cvtest_notifyone(thread_notify_one);
    trd_wait_for.join();
    trd_cvtest_notifyone.join();

    system("pause");

    return 0;
}

​ 示例5.2.4-1 std::condition_variable成员函数notify_one ()使用示例

5.2.5 std::condition_variable成员函数notify_all()

notify_all会唤醒所有等待队列中阻塞的线程,如果当前没有等待线程,则该函数什么也不做。存在多个等待线程的情况下,调用该函数会存在锁竞争,同时只有一个线程能够获得锁,其余未获得锁的线程会继续获取锁而不会再次阻塞,当持有锁的线程释放锁时,这些线程中的一个线程继续获得锁,如此往复下去。请看下面的代码示例:

#include <iostream>           
#include <thread>             
#include <mutex>              
#include <condition_variable> 
#include <chrono>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) 
{
    std::unique_lock<std::mutex> lck(mtx);
    while (!ready)
    {
        cv.wait(lck);
    }

    //放慢执行过程,查看执行细节
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
    
    //输出线程ID 
    std::cout << "thread " << id << '\n';
}

void go() 
{
    std::unique_lock<std::mutex> lck(mtx);
    ready = true;
    cv.notify_all();
}

int main()
{
    std::thread threads[10];
    for (int i = 0; i < 10; ++i)
    {
        threads[i] = std::thread(print_id, i);
    }

    std::cout << "10 threads ready to race..." << std::endl;
    go();                       

    for (auto& th : threads)
    {
        th.join();
    }

    getchar();

    return 0;
}

​ 示例5.2.5-1 std::condition_variable成员函数notify_all ()使用示例

5.3 std::condition_variable应用场景

假设我们在买票时,线程A买票,如果发现没有余票,则会调用wait方法,线程进入等待队列中,线程B进行退票操作,余票数量加一,然后调用notify方法通知等待线程,此时线程A被唤醒执行购票操作。

5.4 多线程虚假唤醒问题

5.4.1 什么是虚假唤醒

虚假唤醒是一种现象,它只会出现在多线程环境中,指的是在多线程环境下,多个线程等待在同一个条件上,等到条件满足时,所有等待的线程都被唤醒,但由于多个线程执行的顺序不同,后面竞争到锁的线程在获得时间片时条件已经不再满足,线程应该继续睡眠但是却继续往下运行的一种现象。

5.4.2 虚假唤醒的解决方法

我们通过while来对wait进行条件判断,具体示例可以百度一下,限于篇幅不再展开说明了。

6 std::async和std:: future

6.1 std::async的基本用法说明

std::async是一个函数模板,用来启动一个异步任务,启动一个异步任务之后,它返回一个std::future类模板对象。启动异步任务就是自动创建一个线程,并开始执行对应的线程入口函数,线程执行完成以后它返回一个std::future对象,这个std::future对象中就含有线程入口函数所返回的结果,我们可以通过调用std::future对象的成员函数get()来获取结果。我们称std::future提供了一种访问异步操作结果的机制,就是说这个结果你可能没办法马上拿到,但是在不久的将来,这个线程执行完毕的时候,你就能够拿到结果了,所以我们可以这么理解:future中保存着一个值,这个值是在将来的某个时刻能够拿到。

6.2 std::async启动参数说明

6.2.1 std::async的std::lunch::deferred参数

std::lunch::deferred表示线程入口函数的调用会被延迟,一直到std::future的wait()或者get()函数被调用时(由主线程调用)才会执行,如果wait()或者get()没有被调用,则不会执行。实际使用中我们会发现这种方式并没有创建新线程,是在主线程中调用的线程入口函数。我们看下面的代码示例:

#include <iostream>
#include <future>
using namespace std;

int threadBody() 
{
    cout << "start threadid = " << this_thread::get_id() << endl;
    this_thread::sleep_for(std::chrono::milliseconds(5000));
    cout << "end threadid = " << std::this_thread::get_id() << endl;

    return 5;
}

int main()
{
    cout << "main threadid = " << std::this_thread::get_id() << endl;
    future<int> result = async(std::launch::deferred, threadBody);
cout << "continue........" << endl;
//卡在这里等待mythread()执行完毕,拿到结果
    cout << result.get() << endl; 
    cout << "good luck" << endl;

    getchar();

    return 0;
}

​ 示例6.2.1-1 std::async的std::lunch::deferred参数使用示例

程序执行结果如下图所示:
在这里插入图片描述
​ 图6.2.1-1 std::async的std::lunch::deferred参数执行结果

6.2.2 std::async的std::launch::async参数

std::launch::async在调用async函数的时候就开始创建新线程。下面是代码示例:

#include <iostream>
#include <future>
using namespace std;

int threadBody()
{
    cout << "start threadid = " << this_thread::get_id() << endl;
    this_thread::sleep_for(chrono::milliseconds(5000));
    cout << "end threadid = " << this_thread::get_id() << endl;

    return 5;
}

int main() 
{
    cout << "main threadid = " << std::this_thread::get_id() << endl;
    future<int> result = async(std::launch::async, threadBody);
    cout << "continue......" << endl;
    cout << result.get() << endl;
    cout << "good luck" << endl;

    getchar();

    return 0;
}

示例6.2.2-1 std::async的std::launch::async参数使用示例

程序执行结果如下图所示:
在这里插入图片描述

​ 图6.2.2-1 std::async的std::lunch:: async参数执行结果

6.2.3 std::async的默认启动参数

若没有指定策略,则会执行默认策略,将会由操作系统决定是否启动新的线程。

6.3 std::future的成员函数说明

6.3.1 std::future的get()函数

std::future的get()函数会等待线程执行结束并返回结果,拿不到结果它就会一直阻塞等待,效果和 join()很像,不同的是get()能够拿到线程的执行结果,而join()不能。其中一点还需要注意,get()函数只能调用一次。如果想线程的执行结果被多个线程获取,可以使用std::shared_future代替std::future达到多次通过get()获取值的目的。

6.3.2 std::future的wait函数

std::future对象的wait()成员函数,用于等待线程返回,本身并不返回结果,这个效果和std::thread的join()更像。

6.3.3 std::future的wait_for()函数

wait_for()阻塞当前流程,等待异步任务运行一段时间后返回其状态 std::future_status,状态是枚举值:deferred(异步操作还没开始)、ready(异步操作已经完成)、timeout(异步操作超时)。

#include <iostream>
#include <future>

using namespace std;

int threadBody(int arg)
{
    cout << arg << endl;
    cout << "Start threadid:" << std::this_thread::get_id() << endl;
    //让线程体等待5s
    std::this_thread::sleep_for(std::chrono::milliseconds(5000));
    cout << "End threadid:" << std::this_thread::get_id() << endl;

    return 5;
}

int main()
{
    cout << "Main thread id:" << std::this_thread::get_id() << endl;
    std::future<int> result = std::async(threadBody, 3);
    cout << "continue..." << endl;

    //设置等待3s,决定程序在当前行阻塞时间
    future_status status = result.wait_for(std::chrono::seconds(3));
    if (status == std::future_status::timeout)
    {
        cout << "timeout..." << endl;
    }
    else if (status == std::future_status::ready)
    {
        cout << "ready..." << endl;
    }

    getchar();

    return 0;
}

​ 图6.3.3-1 std::future的wait_for()函数使用示例

7 std::promise

7.1 std::promise的基本使用

std::promise是C++11并发编程中常用的一个类,常配合std::future使用。其作用是在一个线程A中保存一个类型为typename T的值,可供相绑定的std::future对象在另一线程B中获取。std::future可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段。下面我们看一个std::promise的使用示例:
#include <iostream>
#include <future>
#include <chrono>
#include <functional>

//声明一个可调对象T
using T = std::function<int(int)>;

int testFunction(int iVal)
{
    std::cout << "Value is:" << iVal << std::endl;
    return iVal + 232;
}

void threadBody1(std::promise<T> &p)
{
    //为了突出效果,可以使线程休眠5s
    std::this_thread::sleep_for(std::chrono::seconds(5));

    std::cout << "传入函数testFunction" << std::endl;

    //传入函数testFunction
    p.set_value(std::bind(&testFunction, std::placeholders::_1));
}

void threadBody2(std::future<T> &f)
{
    //阻塞函数,直到收到相关联的std::promise对象传入的数据
    auto fun = f.get();     //iVal = 233

    int iVal = fun(1);

    std::cout << "收到函数并运行,结果:" << iVal << std::endl;
}

int main()
{
    //声明一个promise对象pr1,其保存的值类型为int
    std::promise<T> pr1;
    //声明一个future对象fu1,并通过promise的get_future()函数与pr1绑定
    std::future<T> fu1 = pr1.get_future();

    //创建一个线程t1,将函数threadBody1及对象pr1放在线程里面执行
    std::thread t1(threadBody1, std::ref(pr1));
    //创建一个线程t2,将函数threadBody2及对象fu1放在线程里面执行
    std::thread t2(threadBody2, std::ref(fu1));

    //阻塞至线程结束
    t1.join();
    t2.join();

    getchar();

    return 1;
}

​ 示例7-1 std::promise使用示例

可以看到std::future对象fu1先是通过std::promise的函数get_future()与std::promise对象pr1相绑定,pr1在线程t1中通过set_value()传入共享数据,fu1在线程t2中通过阻塞函数get()获取到传入的数据。

8 std::packaged_task异步任务

8.1 std::packaged_task基本概念

std::packaged_task通过名字我们可以知道,它是用来将任务打包的一个工具,通过packaged_task可以把任何可调用对象(函数、lambda表达式、bind表达式、函数对象)包装起来,方便将来作为线程的入口函数调用。

8.2 std::packaged_task基本使用

8.2.1 std::packaged_task包装函数作为可调用对象

下面我们看一个std::packaged_task包装函数作为可调用对象的代码示例:

#include <thread>
#include <iostream>
#include <future>
using namespace std;

int threadBody(int mypar)
{
    cout << mypar << endl;
    cout << "Start threadid = " << this_thread::get_id() << endl;
    std::chrono::milliseconds dura(5000);
    std::this_thread::sleep_for(dura);
    cout << "End threadid = " << std::this_thread::get_id() << endl;
    return 5;
}

int main()
{
    cout << "Main threadid = " << std::this_thread::get_id() << endl;
    //我们把函数mythread通过packaged_task包装起来
    //参数是一个int,返回值类型是int
    std::packaged_task<int(int)> mypt(threadBody);
    std::thread t1(std::ref(mypt), 1);
    t1.join();
    std::future<int> result = mypt.get_future();
//std::future对象里包含有线程入口函数的返回结果
cout << result.get() << endl;

    getchar();

    return 0;
}

​ 示例8.2.1-1 std::packaged_task包装函数作为可调用对象使用示例

8.2.2 std::packaged_task包装lambda表达式作为可调用对象

下面我们看一个std::packaged_task包装lambda表达式作为可调用对象的代码示例:

#include <thread>
#include <iostream>
#include <future>
using namespace std;

int main() 
{
    cout << "main threadid = " << std::this_thread::get_id() << endl;
    std::packaged_task<int(int)> mypt([](int mypar) {
        cout << mypar << endl;
        cout << "Start threadid = " << this_thread::get_id() << endl;
        std::chrono::milliseconds dura(5000);
        std::this_thread::sleep_for(dura);
        cout << "End threadid = " << this_thread::get_id() << endl;
        return 5;
    });

    std::thread t1(std::ref(mypt), 1);
    t1.join();
    //std::future对象里包含有线程入口函数的返回结果
    std::future<int> result = mypt.get_future();
    cout << result.get() << endl;

    cout << "good luck" << endl;

    return 0;
}

​ 示例8.2.2-1 std::packaged_task包装lambda表达式作为可调用对象使用示例

8.3 std::packaged_task应用场景

既然有了std::promise为什么还需要std::packaged_task,从上面的代码示例可以看出,当保存的数据类型是可调对象时,使用std::packaged_task比std::promise更简洁。

9 总结

多线程编程是一个复杂的编程技术,要想写出稳定的、可扩展的、易于维护的多线程程序并不容易。对于多线程编程工具类的选用始终遵循极简、稳定原则,不要轻易使用自己不熟悉的多线程编程模型,因为多线程程序调试起来比较复杂、定位问题难度大。如果需要使用自己不熟悉的编程模型,建议先通过简单的demo充分了解模型的特性以后再将代码应用到业务代码中。本文涉及的内容非常之多,有些知识点只是浅尝辄止,需要深入学习的可以求助度娘。

参考资料

  • 21
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奥修的灵魂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值