【C++】11新特性:std::thread、std::mutex和两种RAII方式的锁封装

一、std::thread

在C++11之前,开发多线程的程序,一般都是使用pthread_create来创建线程,繁琐且不易读,可以看一下它的函数原型:

int pthread_create(pthread_t* restrict tidp,const pthread_attr_t* restrict_attr,void* (*start_rtn)(void*),void *restrict arg);

输入参数:

  • 第一个参数为指向线程标识符的指针。
  • 第二个参数用来设置线程属性。
  • 第三个参数是线程运行函数的起始地址。
  • 最后一个参数是运行函数的参数。

可以看到pthread_create参数有点复杂,不利于开发。

因此,c++11引入了std::thread来创建线程,支持对线程join或者detach,不过在调用这两个函数之前,先用joinable()函数判断。

示例:

#include 
#include 
using namespace std;

int main()
{
   auto func1 = []()
   {
      cout << "is func1 running" << endl;
   };
   std::thread t(func1);
   if (t.joinable()) // 检查线程可否被join
   {
      t.detach();
   }
   auto func2 = [](int value)
   {
      cout << "is func2 running, "
           << "value is " << value << endl;
   };
   std::thread tt(func2, 20);
   if (tt.joinable())
   {
      tt.join();
   }
   return 0;
}

打印输出:

is func1 running
is func2 running, value is 20

上述代码中,函数func1func2运行在线程对象t和tt中,从刚创建对象开始就会新建一个线程用于执行函数。

  • 调用join函数将会阻塞主线程,直到线程函数执行结束,线程函数的返回值将会被忽略。

  • 如果不希望线程被阻塞执行,可以调用线程对象的detach函数,表示将线程和线程对象分离。

如果没有调用join或者detach函数,假如线程函数执行时间较长,此时线程对象的生命周期结束调用析构函数清理资源,这时可能会发生错误。

这里有两种解决办法,

  • 调用join(),保证线程函数的生命周期和线程对象的生命周期相同,

  • 调用detach(),将线程和线程对象分离,

这里需要注意,如果线程已经和对象分离,那就再也无法控制线程什么时候结束了,不能再通过join来等待线程执行完。

这里可以对thread进行封装(后边的lock也是用了这种封装方式),避免没有调用join或者detach可导致程序出错的情况出现:

#include 
#include 
using namespace std;

class ThreadGuard
{
public:
   enum class DesAction
   {
      join,
      detach
   };

   ThreadGuard(std::thread &&t, DesAction a) : m_thread(std::move(t)), m_action(a){};

   ~ThreadGuard()
   {
      if (m_thread.joinable())
      {
         if (m_action == DesAction::join)
         {
            m_thread.join();
         }
         else
         {
            m_thread.detach();
         }
      }
   }

   ThreadGuard(ThreadGuard &&) = default;
   ThreadGuard &operator=(ThreadGuard &&) = default;

   std::thread &get() { return m_thread; }

private:
   std::thread m_thread;
   DesAction m_action;
};

int main()
{
   ThreadGuard t(std::thread([]()
                             { std::cout << "thread guard " << std::endl; }),
                 ThreadGuard::DesAction::join);
   return 0;
}

c++11还提供了获取线程id,或者系统cpu个数,获取thread native_handle,使得线程休眠等功能

std::thread t(func);
cout << "当前线程ID " << t.get_id() << endl;
cout << "当前cpu个数 " << std::thread::hardware_concurrency() << endl;
auto handle = t.native_handle();// handle可用于pthread相关操作
std::this_thread::sleep_for(std::chrono::seconds(1));

二、std::mutex

std::mutex是一种线程同步的手段,用于保存多线程同时操作的共享数据。相比于以往的pthread_mutex_t方便了许多。

pthread_mutex_t示例:

pthread_mutex_t m_mutex;
pthread_mutex_lock(&m_mutex);//加锁
do_something()
pthread_mutex_unlock(&m_mutex);//释放锁

mutex分为四种:

  • std::mutex:独占的互斥量,不能递归使用,不带超时功能
  • std::recursive_mutex:递归互斥量,可重入,不带超时功能
  • std::timed_mutex:带超时的互斥量,不能递归
  • std::recursive_timed_mutex:带超时的互斥量,可以递归使用

std::recursive_mutex 不同点在于,当前线程可以重复对互斥量进行上锁,即递归上锁,但是这个操作有一个前提就是,调用std::recursive_mutexlockunlock次数得相同。当前线程持有std::recursive_mutex 时,可以重复调用std::recursive_mutex 不会阻塞。如果其他线程尝试持有std::recursive_mutex 的所有权,则会阻塞或者受到false的返回值。

下面拿std::mutexstd::timed_mutex举例,别的都是类似的使用方式:

std::mutex
  • mutex调用lock时,调用的线程将锁住该互斥量。这时候存在有三种情况:
    • 如果该互斥量被当前的线程进行锁住了,则会产生死锁。
    • 如果当前mutex被其他线程锁住了,则当前线程则会阻塞在这里。
    • 如果当前的mutex没有被锁住的情况,则将当前mutex进行锁住,直到调用unlock为止。
  • mutex调用try_lock时,表示当前线程尝试锁住该mutex,如果mutex被其他线程持有,则当前线程不会阻塞在这里,这时候也会出现三种情况:
    • 如果该互斥量被其他线程锁住了,则当前调用线程就返回false,并且不会阻塞。
    • 如果当前mutex被当前线程锁住了,则会产生死锁的情况。
    • 如果当前的mutex没有被其他线程锁住,则当前线程就锁住了mutex,直到调用unlock为止。
  • mutex调用un_lock时,解锁,当前线程释放对mutex的持有。

示例代码:

#include 
#include 
#include 

using namespace std;
std::mutex mutex_;

int main()
{
   auto func1 = [](int k)
   {
      mutex_.lock();
      for (int i = 0; i < k; ++i)
      {
         cout << i << " ";
      }
      cout << endl;
      mutex_.unlock();
   };
   std::thread threads[5];
   for (int i = 0; i < 5; ++i)
   {
      threads[i] = std::thread(func1, 200);
   }
   for (auto &th : threads)
   {
      th.join();
   }
   return 0;
}
std::timed_mutex
  • std::timed_mutex设置了等待超时的机制,之前的互斥量如果无法等待进入机会,会一直阻塞线程,使用std::timed_mutex可以为锁的等待设置一个超时值,一旦超时可以做其他事情。

  • 通过阅读源码发现,std::time_mutexstd::mutex多了两个操作:

    • try_lock_for() :尝试锁定互斥,若互斥在指定的时限时期中不可用则返回
    • try_lock_until(): 尝试锁定互斥,直至抵达指定时间点互斥不可用则返回

示例1:

// test try_lock_for()
#include 
#include 
#include 
#include 

std::timed_mutex test_mutex;

void func()
{
   auto now = std::chrono::steady_clock::now();
   if (test_mutex.try_lock_for(std::chrono::seconds(1)))
   {
      std::cout << "success!\n";

      test_mutex.unlock();
   }
   else
   {
      std::cout << "failed!\n";
   }

   std::cout << "hello world\n";
}

int main()
{
   std::lock_guard<std::timed_mutex> l(test_mutex);
   std::thread t(func);
   t.join();
}

打印输出:

failed!
hello world

示例2:

// test try_lock_until()
#include  // std::cout
#include    // std::thread
#include     // std::mutex, std::unique_lock
#include 

using namespace std;

std::timed_mutex test_mutex;

void func()
{
   std::cout << "sub thread start\n";
   auto now = std::chrono::steady_clock::now();
   test_mutex.try_lock_until(now + std::chrono::seconds(5));
   std::cout << "sub thread end\n";
}

int main()
{
   std::cout << "main start\n";
   std::lock_guard<std::timed_mutex> l(test_mutex);
   std::thread t(func);
   t.join();
   std::cout << "main end\n";
}

打印输出:

main start
sub thread start
sub thread end
main end

在示例1和示例2中,用到了std::lock_guard主要是为了测试子线程一直未获取锁的情况,相当于锁被主线程拿了,等到main函数结束才释放。

三、两种RAII方式的锁封装

c++11主要有std::lock_guardstd::unique_lock两种锁封装的方式,可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。

std::lock_guard在介绍std::timed_mutex时其实已经展示了使用示例。std::unique_lock的使用方式类似。

示例:

#include 
#include 
#include 
#include 

using namespace std;
std::mutex mutex_;

int main() {
    auto func1 = [](int k) {
        // std::lock_guard lock(mutex_);
        std::unique_lock<std::mutex> lock(mutex_);
        for (int i = 0; i < k; ++i) {
            cout << i << " ";
        }
        cout << endl;
    };
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(func1, 200);
    }
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}

std::lock_gurad相比于std::unique_lock更加轻量级,少了一些成员函数,std::unique_lock类有unlock函数,可以手动释放锁,所以条件变量都配合std::unique_lock使用,而不是std::lock_guard,因为条件变量在wait时需要有手动释放锁的能力。

你可能感兴趣的:(【C++】,c++,开发语言,算法)