C++11多线程笔记

Posted 心泻

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11多线程笔记相关的知识,希望对你有一定的参考价值。

链接:https://www.bilibili.com/video/BV1ut411y7u5?p=3


1. 创建线程的数量:通过std::thread::hardware_concurrency()来查看


2. 子线程和主线程共享内存:

  ① 通过添加ref的引用:std::ref(var);

  ② 通过指针

  

3. 通过std::move(var)来给子线程传递参数可以避免资源拷贝


4. 主线程和子线程同时使用一个资源时(注意是同时),避免资源竞争,需要使用mutex


  ① 最初始的用法:

  //下面对共享资源cout进行上锁:

    std::mutex mu;

    void shared_print(string msg)

    {

      mu.lock();

      cout << msg << endl;

      mu.unlock();

    }

    

    ② 如果上面的cout << msg << endl抛出异常,则锁永远被锁住,无法解锁。-> 使用lock_guard,在其生命周期结束时候自动解锁

    std::lock_guard<std::mutex> guard(mu); // RAII

    

    ③ 但是上述做法不是线程安全的:即cout是一个全局的,仍然可以在其他地方不通过锁就可以访问。

    针对这样一般使用:通过一个类,维护想要使用的资源以及一把锁。这个时候那么该资源就完全被锁保护了。注意不要向外界返回要保护的资源。

    

    ⑤ 需要注意,使用mutex只是避免同时使用一个资源(data race),而不一定是线程安全的。例如:

    class stack

    {

        int* _data;

        std::mutex _mu;

    public:       

        int& top();//返回值

        void pop();//弹出值

    }

    

    void function(stack& st)//该函数会被很多线程使用

    {

        int value = st.top();

        st.pop();

        // ... value的处理

    }

    分析这段代码不是线程安全的原因:thread1在使用top()后立即thread2使用top(),导致同一个值处理两次,然后thread1再pop(),接着thread2再pop(),导致弹出了一个还没有处理的值。

    所以解决方案是:将取值top和弹出pop操作结合在一个函数里面。

    

5. 死锁

  

  ① 产生:例如使用两把锁时

  class ResourceMu

  {

    std::mutex _mu1;

    std::mutex _mu2;

    ofstream _f;

  public:

    ResourceMu(){_f.open("file.tex");}

    void shared_print_1(string s, int v)

    {

      std::lock_guard<std::mutex> locker_1(_mu1);

      std::lock_guard<std::mutex> locker_2(_mu2);

      _f << "From " << s << ":" << v << endl; 

    }

    void shared_print_2(string s, int v)

    {

      std::lock_guard<std::mutex> locker_2(_mu2);

      std::lock_guard<std::mutex> locker_1(_mu1);

      _f << "From " << s << ":" << v << endl; 

    }

  };

  

  void function_thread(ResourceMu& rm) // 子线程调用

  {

    for(int i = 0; i < 100; ++i)

    {

      rm.shared_print_1("thread", i);

    }

  }


  void function_main(ResourceMu& rm) // 主线程调用

  {

    for(int i = 0; i > -100; --i)

    {

      rm.shared_print_2("mian", i);

    }

  }


  int main()

  {

    ResourceMu rm;

    std::thread t1(function_thread, std::ref(rm));

    function_main();

    

    t1.join();

    return 0;

  }


  ② 避免这种情况的死锁:保证每次上锁的顺序一致

C++提供了保证上锁顺序的机制:std::lock(_mu1, _mu2);


  std::lock(_mu1, _mu2);

  std::lock_guard<std::mutex> locker_1(_mu1, std::adopt_lock);

  std::lock_guard<std::mutex> locker_2(_mu2, std::adopt_lock);

  

6. unique_lock<std::mutex> 比起lock_guard更有灵活性: 

  ① 可以直接解锁

  std::unique_lock<std::mutex> locker(_mu);

  //...需要上锁的代码

  locker.unlock();

  //...不需要上锁的代码

  

  ② 可以先创建锁而不上锁:

  std::unique_lock<std::mutex> locker(_mu, std::defer_lock);

  //...不需要上锁的代码

  lockder.lock();

  //...需要上锁的代码


  ③ unique_lock和lock_guard都不可以被复制,但是unique_lock可以被move

  

  ④ unique_lock具有灵活性的代价是性能相较而言更低。

      

7. Lazy Initialization(Initialization Upon First Use Idiom)

  

  ① 什么是Lazy Initialization

  class LogFile

  {

    std::mutex _mu;

    std::mutex _mu_open;

    ofstream _f;

  public:

    LogFile(){_f.open("Log.txt");};//每次创建类则打开一个文件,但是我们想要的是在使用shared_print函数时候才打开文件

    void shared_print(){...}

  }

  将打开文件操作移动到shared_print()中,即为Lazy Initialization;

  

  ② 考虑线程安全:

  void shared_print(string id, int value)

  {

    //打开文件:只被打开一次

    if(!_f.isopen())

    {

      std::unique_lock<std::mutex> locker_open(_mu_open);//这不是线程安全的(*)

      _f.open("Log.txt");

    }

    

    //...操作_f的代码,会调用多次

    std::unique<std::mutex> locker(_mu);

    ...

  }

  分析为什么不是线程安全的:thread1发现_if未打开,所以给_mu_open上锁(还未打开文件),此时执行至thread2发现文件未打开,则等待给_mu_open上锁,一旦thread1打开文件并释放锁之后,thread2再次打开文件。


  ③ 解决上述问题:

  void shared_print(string id, int value)

  {

    //打开文件:只被打开一次

    {

      std::unique_lock<std::mutex> locker_open(_mu_open);//该代码则保证文件只打开一次,是线程安全的。

      if(!_f.isopen())

      {

        _f.open("Log.txt");

      }

    }

 

    //...操作_f的代码,会调用多次

    std::unique<std::mutex> locker(_mu);

    ...

  }

  

  引入多次上锁解锁的操作带来的资源浪费问题:

  

  ④ 解决上述问题,使用once_flag

  std::once_flag _flag;

  std::call_once(_flag, [&](){_f.open("Log.txt");}); //thread safe and efficient.

      

8. 条件变量    

  ① 生产者消费者模型引入的问题

  void function_1()//生产者

  {

    int count = 10;

    while(count > 0)

    {

      std::unique_lock<std::mutex> locker(mu);

      q.push_front(count);

      locker.unlock();

      std::this_thread::sleep_for(chrono::seconde(1));

      --count;

    }

  }

  void function_2()//消费者

  {

    int data = 0;

    while(data != 1)//该模型中消费者将一直循环判断q是否为空,导致低效

    {

      std::unique_lock<std::mutex> locker(mu);

      if(!q.empty())

      {

        data = q.back();

        q.pop_back();

        locker.unlock();

        cout << "thread2 got a number from thread1 :" << data << endl;

      }

      else

      {

        locker.unlock();

        std::this_thread::sleep_for(chrono::milliseconds(10));//通过等待减少循环次数

      }

    }

  }

  其中的问题是消费者一直循环判断导致的低效问题,可以通过在q为空时候等待的方式来减少循环。但是并不好判断需要等待多久。


  ② 引入条件变量

  void function_1()

  {

    int count = 10;

    while(count > 0)

    {

      std::unique_lock<std::mutex> locker(mu);

      q.push_front(count);

      locker.unlock();

      cond.notify_one();//当生产一个数据后,给出通知

      std::this_thread::sleep_for(chrono::seconds(1));

      --count;

    }

  }

  void function_2()

  {

    int data = 0;

    while(data != 1)

    {

      std::unique_lock<std::mutex> locker(mu);

      cond.wait(locker);//等待唤醒

      data = q.back();

      q.pop_back();

      locker.unlock();

      cout << "Thread 2 got a value from Thread1: " << data << endl;

    }

  }

  ·添加条件变量:std::condition_variable cond;

  ·在function_1中,添加cond.notify_one();

  ·在function_2中,删除对q.empty()的判断,而使用cond.wait(locker); 为什么wait需要locker参数?因为cond.wait将进入sleep状态,无需锁住任何资源,需要对locker解锁。当被唤醒时候,再次上锁

  ·针对条件变量,注意需要使用unique_lock而不是lock_guard,因为会对locker多次解锁和上锁。

  ·防止function_2可能自己唤醒(spurious wake):cond.wait(locker, [](){return !q.empty();});

  

9. Future and Promise      

  ① 父线程从子线程获得数据

  常规做法是:

  void function(int N, int& x)

  {

    int res = 1;

    for(int i = N; i > 1; --i)

      res *= i;

    x = res;

  }

  int main()

  {

    int x;

    std::thread t 1(function, 4, std::ref(x));

    

    t1.join();

    return 0;

  }

  此时,由于父线程和子线程共同使用x,则需要使用mutex,同时,需要保证由子线程产出x后父线程才获取x,所以需要条件变量。

  

  ② 更好的做法是:

  使用函数std::async(); 其第一个参数是可调用对象,后面的参数是可调用对象的参数(和线程参数一致)。其返回一个std::future<T>对象,T是可调用对象的返回值类型。

  int function(int N)

  {

    int res = 1;

    for(int i = N; i > 1; --i)

      res *= i;

    return res;

  }

  int main()

  {

    int x;

    std::future<int> fu = std::async(function, 4);//async是否创建一个新线程取决于参数:std::launch::deferred参数将不会使async函数创建新线程,使用std::launch::async则会创建新线程,或者使用两者的和|

    x = fu.get();//注意,get只能被调用一次

    

    return 0;

  }


  ③ 通过使用promise给出一个future,将父线程的值传给子线程

  int Myfunction(std::future<int>& f)//需要使用引用,因为future不能被复制,或者使用shared_future

  {

    int res = 1;

    int N = f.get();//只能调用一次

    for(int i = 1; i <= N; ++i)

    {

      res *= i;

    }

    return res;

  }

  int main()

  {

    int x; 

    

    //并不知道要给Myfunction传入什么值

    std::promise<int> p;

    std::future<int> f = p.get_future();

    

    //std::future<int> fu = std::async(Myfunction, 4);//此时并不知道是否需要传入4,则通过promise在未来决定

    std::future<int> fu = std::async(Myfunction, std::ref(f));

    

    //...执行一些代码后才知道需要给Myfunction传入什么值

    p.set_value(4);

    

    x = fu.get();

    

    return 0;

  }


  ④ future和promise 与 unique_lock一样,只能move不能复制 

  上述引入的问题:如果多个线程调用Myfunction,则会多次调用f.get()导致编译失败。使用shared_future可以避免该问题。

  std::shared_future<int> sf = f.share();


10. Callable object总结

  ① thread对象:std::thread t1(callableOjb, param)

  ② async函数:std::async(std::launch::async, callableObj, param)

  ③ bing函数:std::bind(callableObj, param)

  ④ call_once函数:std::call_once(once_flag, callableObj, param)


11. 针对可调用对象的用法总结:

  class A

  {

   public:

    void f(int x, char c){}

    long g(double x){return 0;}

    int operator()(int N){return 0;}

  };

  void foo(int x){}

  int main()

  {

    A a;

    std::thread t1(a, 6); //复制a对象,执行operator()(int N);

    std::thread t2(std::ref(a), 6); //引用a对象,执行operator()(int N);

    std::thread t3(std::move(a), 6); //移动a对象,执行operator()(int N),注意a不能再在主线程中被使用;

    

    std::thread t4(A(), 6); //创建临时对象;

    std::thread t5(&A::f, a, 8, 'w'); // 复制a对象,执行a.f();

    std::thread t6(&A::f, &a, 8, 'w'); // 引用a对象,执行a.f();

    

    std::thread t7([](int x){return x*x;}, 6); //使用lamda函数;

    std::thread t8(foo, 7);

  }


12. packaged task  


  ① 引入packaged task

  int factorial(int N)

  {

    int res = 1;

    for(int i = N; i > 1; --i)

      res *= i;

    return res;

  }

  int main()

  {

    std::packaged_task<int(int)> t(factorial);//注意不能像thread一样传入factorial的参数,需要使用bind:std::bind(factorial, 6);

    ///...do something els

    t(6);

    int x = t.get_future().get();

    

    return 0;

  }

  注意,使用bind后,需要变成:packaged_task<int()>,同时调用时候只能t();

  使用bind时候也可以这样:

  auto t = bind(factorial, 6);

  //...

  t();

  

  ② package_task的应用场景

  std::deque<std::packaged_task<int()>> task_q;//任务队列

  std::mutex mu;//task_q被主线程和子线程共同使用,需要锁防止同时使用

  std::condition_variable cond;

  int factorial(int N)

  {

    int res = 1;

    for(int i = N; i > 1; --i)

      res *= i;

    return res;

  }

 

  void thread_1()//在某个线程中执行任务

  {

    std::packaged_task<int()> t;

    {

      std::unique_lock<std::mutex> locker(mu);

      cond.wait(locker, [](){return !task_q.empty();});//当任务列表里面有任务才唤醒

      t = std::move(task_q.front()); 

      task_q.pop_front();

    }

    t();

  }


  int main()

  {

    std::thread t1(thread_1);

    std::packaged_task<int()> t(bind(factorial, 6));

    std::future<int> fu = t.get_future();//获取任务执行结果

    {

      std::unique_lock<std::mutex> locker(mu);

      task_q.push_back(std::move(t));

    }

    cond.notify_one();

    

    cout << fu.get() << endl;//查看结果

    t1.join();

    return 0;

  }


13. 获得future总结

  ① promise::get_future();

  ② package_task::get_future();

  ③ async() return a future;

  

  


以上是关于C++11多线程笔记的主要内容,如果未能解决你的问题,请参考以下文章

C++ 多线程学习笔记:读者-写者问题模拟

毕向东Java视频学习笔记Day11-Day13 多线程

11.2-全栈Java笔记:Java中如何实现多线程

11.1-全栈Java笔记:多线程技术的基本概念

[多线程]C++11多线程用法整理

C++11 多线程