c++11 thread学习笔记

C++11 线程学习笔记

C++11 中创建线程的方式

在c++11中有三种创建线程的方式:

  • 函数指针方式
  • 函数对象方式
  • Lambda函数
    创建函数对象之前,首先需要引入线程的头文件, 如果你是在linux下VSCode中使用线程,需要在tasks.json文件的args属性中添加"-pthread".

下面的例子展示了使用函数指针创建线程的方式:

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

void thread_function()
{
  for (int i = 0; i < 10000; ++i)
    ;
  std::cout << "thread_function Executing\n";
}

// create thread using function pointer
void createByFunPointer()
{
  std::thread threadObj(thread_function);
  for (int i = 0; i < 10000; ++i)
    ;
  std::cout << "Display From MainThread\n";
  threadObj.join();
  std::cout << "Exit of Main function\n";
}

下面的例子展示了使用函数对象创建一个线程对象:

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

class DisplayThread
{
public:
  void operator()()
  {
    for (int i = 0; i < 10000; ++i)
      std::cout << "Display Thread Executing\n";
  }
};

void createByFunObj()
{
  thread threadObj((DisplayThread()));    // if there is no (), the compiler will fail.
  for (int i = 0; i < 10000; ++i)
    std::cout << "Display From Main Thread\n";
  std::cout << "Waiting for Thread to complete\n";
  threadObj.join();
  std::cout << "Exiting from Main Thread\n";
}

下列例子展示了用lambda函数创建线程:

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

void createByLambda()
{
  thread threadObj([]() -> void {
    for (int i = 0; i < 10000; ++i)
      std::cout << "Display Thread Executing\n";
  });
  for (int i = 0; i < 10000; ++i)
    std::cout
        << "Display From Main Thread\n";
  std::cout << "Waiting for Thread to complete\n";
  threadObj.join();
  std::cout << "Exiting from Main Thread\n";
}

线程的id

每一个线程对象都有一个属于自己的线程id, 获取线程id的方法如下:

std::thread::get_id();

如果想获取当前运行线程的id,使用的方法如下:

std::this_thread::get_id();

线程id所属的类型为std::thread::id,这是一个对象,可以在命令行中打印出来.

线程的joinable函数, join函数和detach函数

线程的joinable函数返回布尔类型,告知调用方该函数是否处于执行状态.线程的默认构造函数构造的线程对象初始的时候joinable返回的结果是false,通过上述三种形式调用的线程对象,初始化状态joinable状态返回的一定是true. 一旦调用了该线程的join函数或者detach函数,其joinable函数返回的结果一定是false.

当一个线程对象被创建后,线程对象就开始启动执行.线程的join函数,可以理解为等待该线程执行完毕,因此,当一个线程对象跳出其作用域之前,必须要调用线程对象的join函数或者detach函数,否则程序在这个线程的析构函数中会调用std::terminate,然后抛出异常.

对于join函数,可以简单的理解为,等待该函数执行完毕.而detach函数的意义,可以理解为让该线程继续以守护线程的形式运行.detach函数实际上表示将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。

注意

  • join函数或者detach函数只能执行一次,不能多次执行,如果不确定是否可以join或者detach,就先调用joinable函数,如果返回true,则可以调用join或者detach.

为线程传递参数

线程所需要的参数,在线程创建的时候进行传递即可,示例如下:

static int g_r = 0;
template<typename T>
void add(const T& t1, const T& t2){
  g_r = t1+t2;
}

void AddThread(){
  thread t(add<int>,3, 4);
  t.join();
  std::cout<<"g_r is "<<g_r<<std::endl;
}

注意

  • 向线程传递参数的时候一定要注意,如果向线程传递了局部变量,但是线程执行的过程中局部变量已经被销毁了,会引起意外的错误,示例如下:

    #include <iostream>
    #include <thread>
    void newThreadCallback(int * p)
    {
        std::cout<<"Inside Thread :  "" : p = "<<p<<std::endl;
        std::chrono::milliseconds dura( 1000 );
        std::this_thread::sleep_for( dura );
        *p = 19;
    }
    void startNewThread()
    {
        int i = 10;
        std::cout<<"Inside Main Thread :  "" : i = "<<i<<std::endl;
        std::thread t(newThreadCallback,&i);
        t.detach();
        std::cout<<"Inside Main Thread :  "" : i = "<<i<<std::endl;
    }
    int main()
    {
        startNewThread();
        std::chrono::milliseconds dura( 2000 );
        std::this_thread::sleep_for( dura );
        return 0;
    }
    
  • 传递指针的时候也要非常注意, 有可能线程执行过程中指针指向的数据量被其他线程销毁,也会引起以外的错误.示例如下:

    #include <iostream>
    #include <thread>
    void newThreadCallback(int * p)
    {
        std::cout<<"Inside Thread :  "" : p = "<<p<<std::endl;
        std::chrono::milliseconds dura( 1000 );
        std::this_thread::sleep_for( dura );
        *p = 19;
    }
    void startNewThread()
    {
        int * p = new int();
        *p = 10;
        std::cout<<"Inside Main Thread :  "" : *p = "<<*p<<std::endl;
        std::thread t(newThreadCallback,p);
        t.detach();
        delete p;
        p = NULL;
    }
    int main()
    {
        startNewThread();
        std::chrono::milliseconds dura( 2000 );
        std::this_thread::sleep_for( dura );
        return 0;
    }
    

    如果向线程传递引用,会发生什么情况呢? 看如下一个示例:

    #include <iostream>
    #include <thread>
    void threadCallback(int const & x)
    {
        int & y = const_cast<int &>(x);
        y++;
        std::cout<<"Inside Thread x = "<<x<<std::endl;
    }
    int main()
    {
        int x = 9;
        std::cout<<"In Main Thread : Before Thread Start x = "<<x<<std::endl;
        std::thread threadObj(threadCallback, x);
        threadObj.join();
        std::cout<<"In Main Thread : After Thread Joins x = "<<x<<std::endl;
        return 0;
    }
    

    输出的结果是

    In Main Thread : Before Thread Start x = 9
    Inside Thread x = 10
    In Main Thread : After Thread Joins x = 9

结果和预想中不一样,在threadCallback中对x的修改,并不会对主线程可见,真正的原因是,传递参数的时候threadCallback收到的参数x指向的是一块临时变量而非main函数中的x. 但是这并不表明不能向函数传递引用,实际上传递引用时需要用到std::ref, 示例如下:

#include <iostream>
#include <thread>
void threadCallback(int const & x)
{
    int & y = const_cast<int &>(x);
    y++;
    std::cout<<"Inside Thread x = "<<x<<std::endl;
}
int main()
{
    int x = 9;
    std::cout<<"In Main Thread : Before Thread Start x = "<<x<<std::endl;
    std::thread threadObj(threadCallback,std::ref(x));
    threadObj.join();
    std::cout<<"In Main Thread : After Thread Joins x = "<<x<<std::endl;
    return 0;
}

输出的结果是

In Main Thread : Before Thread Start x = 9
Inside Thread x = 10
In Main Thread : After Thread Joins x = 9

如何向线程传递类对象的函数呢?示例如下:

#include <iostream>
#include <thread>
class DummyClass {
public:
    DummyClass()
    {}
    DummyClass(const DummyClass & obj)
    {}
    void sampleMemberFunction(int x)
    {
        std::cout<<"Inside sampleMemberFunction "<<x<<std::endl;
    }
};
int main() {
 
    DummyClass dummyObj;
    int x = 10;
    std::thread threadObj(&DummyClass::sampleMemberFunction,&dummyObj, x);
    threadObj.join();
    return 0;
}
多线程之间的数据共享
Race Condition

启用多个线程,多个线程访问同一变量时,会导致意外的结果.下面的例子展示了5个线程分别对Wallet中的mMoney变量进行修改, 由于没有设定访问次序,每个线程都可以同时修改变量,导致最终变量的结果并非我们所想.

// A example for race condition(竞争条件)
class Wallet
{
  int mMoney;

public:
  Wallet() : mMoney(0) {}
  int getMoney() { return mMoney; }
  void addMoney(const int money)
  {
    for (int i = 1; i <= money; ++i)
      mMoney++;
    std::cout << "sub Thread: money in wallet is " << mMoney << std::endl;
  }
};

void raceContition()
{
  Wallet walletObj;
  std::vector<thread> threads;
  for (int i = 0; i < 5; ++i)
    threads.push_back(std::thread(&Wallet::addMoney, &walletObj, 100000));

  for (size_t i = 0; i < threads.size(); ++i)
    threads[i].join();
  std::cout << "main Thread: money in wallet is " << walletObj.getMoney() << std::endl;
}

运行结果如下图,最终运行结果并非是500000. 因为多个线程分别并行的从内存中将mMoney放入寄存器,在寄存器中完成mMoney++操作,然后写入内存,前一个线程尚未完全完成操作时,下一个线程紧接着开始执行, 多个线程的并行操作导致了意料之外的结果.

money in wallet is 86114

money in wallet is 142908

money in wallet is 209027

money in wallet is 257885

money in wallet is 297470

main Thread money in wallet is 297470

std::mutex

那么如何才能解决多线程之间并发写数据导致的问题呢?一种可行的方案是使用互斥变量std::mutex, 使用mutex必须要添加头文件. mutex有两个重要的函数:lock()和unlock().前面的例子中,在每次修改wallet中的money时,线程先对该变量锁定,每次写回内存后再交给下一个线程进行操作,这样就可以完成money的互斥访问,只需要对Wallet类进行修改即可,修改后Wallet类如下:

class Wallet
{
  int mMoney;
  std::mutex mutex;  // 互斥信号量

public:
  Wallet() : mMoney(0) {}
  int getMoney() { return mMoney; }
  void addMoney(const int money)
  {
    mutex.lock();
    for (int i = 1; i <= money; ++i)
      mMoney++;
    mutex.unlock();
    std::cout << "sub Thread: money in wallet is " << mMoney << std::endl;
  }
};
std::lock_guard

假如在使用mutex的时候,如果忘记unlock了,就会导致下一个访问变量的线程一直不能访问mMoney,线程会一直等待.为了避免这种情况,我们需要使用std::lock_guard, lock_guard是一个类模板,帮我们实现mutex的RAII, 它对mutex额外一层的封装, 在构造函数中进行lock, 析构函数中进行unlock, 示例如下:

class Wallet2
{
  int mMoney;
  std::mutex mutex; // 互斥信号量

public:
  Wallet2() : mMoney(0) {}
  int getMoney() { return mMoney; }
  void addMoney(const int money)
  {
    {
      std::lock_guard<std::mutex> lockGuard(mutex); // lock mutex
      for (int i = 1; i <= money; ++i)
        mMoney++;

      std::cout << "sub Thread: money in wallet is " << mMoney << std::endl;
    }
  }
};
Event Handling

有时候一个线程的执行需要一些条件,当条件满足时,线程才会执行,例如在网络环境下完成如下三个任务:

  1. 和服务器完成握手.
  2. 从xml文件中载入数据.
  3. 处理载入的数据.

第1个任务不依赖于第2和第3个任务,但是第3个任务依赖于第2个任务, 这意味着任务1和任务2可以并行执行以提升性能.如何完成多线程设计呢? 我们可以设计2个线程,线程1完成如下任务:

  • 与服务器完成握手.
  • 等待线程2从xml中载入数据.
  • 处理数据.

线程2完成如下任务:

  • 载入xml中的数据.
  • 通知线程1数据载入已经完成.

两个线程的交互如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tdsV3YGg-1577341839615)(…/pic/thread1.png)]

线程1完成与服务器的交互之后,等待线程2成功载入数据后的发出信号,随后开始处理数据.那么线程2成功载入数据之后,如何通知线程1呢? 一种方式是使用一个全局变量,初始化为false,当线程1完成与服务器交互之后,持续检查全局变量是否为true,如果为true则开始处理数据.线程2载入数据之后,将全局变量设置为true,表示数据载入已经完成,线程1则可以执行后续的数据处理,示例如下:

class Application{
    std::mutex m_mutex;
    bool m_bDataLoaded;
    public:
    Application(){
        m_bDataLoaded = false;
    }
    void loadData(){
        // simulate loading data.
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout<<"Loading Data from XML\n";

        // Lock the data structure
        std::lock_guard<std::mutex> guard(m_mutex);

        // set flag to true, means data is loaded.
        m_bDataLoaded = true;
    }

    void mainTask(){
        std::cout<<"Do Some Handshaking\n";

        // Acquire the lock.
        m_mutex.lock();

        while(m_bDataLoaded != true){
            //  release the lock
            m_mutex.unlock();
            std::this_thread::sleep_for(std::chrono::microseconds(100));
            // Acquire the lock
            m_mutex.lock();
        }
        // Release the lock
        m_mutex.unlock();

        // processing data.
        std::cout<<"Do processing data\n";
    }
};

void example1(){
    Application app;
    std::thread thread_1(&Application::mainTask, &app);
    std::thread thread_2(&Application::loadData, &app);
    thread_1.join();
    thread_2.join();
}

这种实现方法的弊端在于,线程1需要不断的检查flag是否设置为true, 这降低了线程1的速度,也浪费了cpu 时钟周期, 如果通过某种方式使得线程1处于阻塞模式,当条件成熟后线程1自动被唤醒,这就会好很多,这就需要用到条件变量 Condition variable.

条件变量用于多个线程之间的信号传递, 一个或多个线程可以等待某个信号,这个信号可以由一个线程发出. 使用条件变量需要添加头文件<condition_variable>, 一个条件变量同样也需要一个mutex.针对上述例子,条件变量的使用方式如下:

  • 线程1中调用条件变量的wait函数,wait函数内部会对条件变量加锁,然后检查条件是否满足.
  • 如果条件不满足,则释放mutex, 线程进入阻塞模式并继续等待信号. 条件变量的wait函数以原子操作的形式完成查询.
  • 线程2载入数据后,向条件变量发送信号.
  • 当条件变量收到信号后,线程1会被唤醒,然后获取mutex检查条件是否满足或者是否是上级调用.
  • 如果是上级调用,则继续调用wait函数进入阻塞模式.
wait()函数

std::condition_variable最重要的函数是wait,该函数令当前线程进入阻塞模式,直到相关条件变量满足.它以原子操作的形式获取mutex,阻塞当前线程,并将该线程加入等待条件变量的队列中.当某些线程调用notify_one函数或者notify_all函数的时候,该线程会被唤醒, 被唤醒的时候条件未必满足,因此每次唤醒之后需要再次检查条件.

调用wait函数时,需要向其传递一个可调用函数,该函数用于判定是否是虚假唤醒或者条件已经满足.

当线程被解锁时,wait函数重新获取mutex并且检查条件是否满足,如果条件不满足,则释放mutex并且令线程进入阻塞状态,等待当前条件变量.

notify_one()函数

如果有多个线程在等待条件变量, notify_one()会从等待队列中选择队首线程并唤醒它.

notify_all()

如果多个线程在等待条件变量,notify_all()会唤醒所有等待该条件变量的线程.

针对前述的例子, 一个示例如下:

#include <iostream>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <functional>
#include <chrono>
using namespace std::placeholders;

class Application{
    std::mutex m_mutex;
    std::condition_variable m_condVar;
    bool m_bDataLoaded;
public:
    Application():m_bDataLoaded(false){}

    void loadData(){
        // simulate loading data
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout<<"Loading data from XML\n";
        // Lock the data structure
        std::lock_guard<std::mutex> guard(m_mutex);
        // Set the flag to true, means data is loaded.
        m_bDataLoaded = true;
        // Notify the condition variable
        m_condVar.notify_one();
    }

    bool isDataLoaded(){
        return m_bDataLoaded;
    }

    void mainTask(){
        std::cout<<"Do some handshaking\n";

        // Acquire the lock
        std::unique_lock<std::mutex> mlock(m_mutex);

        m_condVar.wait(mlock, std::bind(&Application::isDataLoaded, this));
        std::cout<<"Do processing on loaded data\n";
    }
};

int main(){
    Application app;
    std::thread thread_1(&Application::mainTask, &app);
    std::thread thread_2(&Application::loadData, &app);
    thread_1.join();
    thread_2.join();
    return 0;
}

为什么一定要对在线程被销毁之前调用join或者detach?

如果线程的析构函数调用之前没有调用join或者detach, 线程自动会调用std::terminate函数,而这个函数的目的是停止程序并且抛出异常, c++11 标准的草稿中写道:

" If joinable(), calls std::terminate(). Otherwise, has no effects. [ Note: Either implicitly detaching or joining a joinable() thread in its destructor could result in difficult to debug correctness (for detach) or performance (for join) bugs encountered only when an exception is raised. Thus the programmer must ensure that the destructor is never executed while the thread is still joinable. —end note ] "

参考

https://thispointer.com//c11-multithreading-part-4-data-sharing-and-race-conditions/

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值