C++ 多线程2 - mutex

  • C++98标准中并没有线程库的存在。
  • C++11中才提供了多线程的标准库,提供了threadmutexcondition_variableatomic等相关对象及功能功能。


概述

  • C++ 11中与 Mutex 相关的类(包括锁类型)和函数都声明在 <mutex> 头文件中。
  • #include <mutex>
  • ☃互斥对象 有四种类型:
          I. std::mutex,最基本的 Mutex 类。
          II. std::recursive_mutex,递归 Mutex 类。
          III. std::time_mutex,定时 Mutex 类。
          IV. std::recursive_timed_mutex,定时递归 Mutex 类。
  • ☃ 锁对象 有两种类型:
          I. std::lock_guard,RAII 机制,方便线程对互斥量上锁。
          II. std::unique_lock,RAII 机制,方便线程对互斥量上锁,提供了更好的上锁和解锁控制。
  • ☃ Other types:
          I. std::once_flag
          II. std::adopt_lock_t
          III. std::defer_lock_t
          IV. std::try_to_lock_t
  • ☃ 函数:
          I. std::try_lock,尝试同时对多个互斥对象 上锁。
          II. std::adopt_lock_t
          III. std::defer_lock_t
          IV. std::try_to_lock_t

1 互斥对象

1.1 mutex 构造函数

  • std::mutex 是C++11 中最基本的互斥量,std::mutex对象提供独占所有权:不支持递归地对 std::mutex 对象上锁。
  • std::mutex 不支持 copy、move 构造,仅可以使用默认构造。
constexpr mutex() noexcept;			// default
mutex(const mutex&) = delete;		// 不支持copy、move 构造

1.2 mutex 成员函数

-std::mutex 包含以下成员函数:

void lock();						// 1. 获取锁
native_handle_type native_handle();	// 2. 实现定义的原生句柄对象。 
bool try_lock();					// 3. 尝试锁住 mutex对象
void unlock();						// 4. 释放对互斥锁的所有权。
  • lock(); 调用线程锁定 std::mutex(互斥锁) 对象,必要时阻塞:
          I. 如果互斥锁当前未被任何线程锁定,则调用线程将锁定它(从此时开始,直到调用 unlock,该线程一直拥有该互斥锁)。
          II. 如果互斥锁当前被另一个线程锁定,当前的调用线程阻塞,直到被另一个线程解锁。
          III. 如果互斥锁被当前调用此函数的同一线程锁定,则会产生死锁。
  • unlock(): 解锁,释放锁对象的所有权。
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

std::mutex mtx; 
void print_thread_id (int id) {
    
    
    mtx.lock();
    this_thread::sleep_for(chrono::milliseconds(300));
    std::cout << "thread #" << id << '\n';
    mtx.unlock();
}

int main () {
    
    
    std::thread threads[10];
    
    // spawn 10 threads:
    for (int i=0; i<10; ++i)
        threads[i] = std::thread(print_thread_id,i+1);

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

    return 0;
}

thread #1
thread #2
thread #3
thread #4
thread #5
thread #6
thread #7
thread #8
thread #10
thread #9

  • 每一个线程获取锁后,会sleep 300毫秒,因此后续的线程会同时申请锁,导致打印顺序并不是升序。
  • try_lock(): 尝试锁定互斥锁,但不阻塞:
          I. 如果互斥锁当前没有被任何线程锁定,则调用线程将锁定它。
          II. 如果互斥锁当前被另一个线程锁定,则该函数将失败并返回false,而不阻塞(调用线程继续执行)。
          III.如果互斥锁当前被调用此函数的同一线程锁定,则会产生死锁(具有未定义的行为)。
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

volatile int count_(0);              // 1. 全局变量
std::mutex mtx;
void attempt_10k_increases () {
    
    
    for (int i=0; i<10000; ++i) {
    
    
        if (mtx.try_lock()) {
    
           // 2. 只有获得锁时才会 ++count 
            ++count_;
            mtx.unlock();
        }
    }
}

int main () {
    
    
    std::thread threads[10];

    // 3. 10个线程
    for (int i=0; i<10; ++i)
        threads[i] = std::thread(attempt_10k_increases);

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

    std::cout << "Count: " << count_ << std::endl;
    return 0;
}

Count: 9522

  • 10 个线程同时执行,线程内部使用 try_lock()函数获取锁,获取后 ++count;
  • 但由于同一时间只能有一个线程能拥有锁,因此最终count 值在1w 左右。

1.3 other

  • recursive_mutex:与 mutex 对象所包含的成员函数相同,不同之处在于recursive_mutex允许同一个线程对互斥量多次上锁(即递归上锁),
  • time_mutex
  • recursive_timed_mutex
  • timed_mutex

2 锁对象

2.1 lock_guard

  • lock_guard 遵循RAII 来处理资源, 定义如下:
template <class Mutex> class lock_guard;

RAII也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。

  • lock_guard的职责就是管理互斥对象mutex,在其构造函数中进行加锁,在其析构函数中进行解锁。
  • 最终的结果就是:创建即加锁,生命周期结束自动解锁。因此使用 lock_guard() 可以免去 lock()unlock()
  • lock_guard() 成员函数仅有 构造函数、析构函数。其构造函数如下:
explicit lock_guard(mutex_type& m);				// 1. locking
lock_guard(mutex_type& m, adopt_lock_t tag);	// 2. 双参数-adopting
lock_guard(const lock_guard&) = delete;			// 3. 复制构造
  • 双参数构造函数第一个参数为 mutex 对象,第二个参数为 adopt_lock 标识,表示构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定。

    测试代码:

std::mutex m;
void func_1() {
    
    
    lock_guard<mutex> g1(m);                                // 1. 构造函数只包含 mutex对象

    std::cout << "func_1 get lock." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "func_1 unlock." << std::endl;             // 作用域结束,自动解锁
}

void func_2() {
    
    
    m.lock();                                               // 手动锁定
    lock_guard<mutex> g2(m,adopt_lock);                     // 2. 构造函数包含两个参数
    std::cout << "func_2 get lock." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "func_2 unlock." << std::endl;
}

int main() {
    
    
    std::thread thread_1(func_1);
    std::thread thread_2(func_2);
    thread_1.join();
    thread_2.join();

    return 0;
}

func_1 get lock.
func_1 unlock.
func_2 get lock.
func_2 unlock.

  • 可以看出,thread_1 sleep 1秒钟,期间未释放锁,thread_2 .loc(); 阻塞。
  • thread_1 运行结束,释放锁,thread_2运行。

2.2 unique_lock 构造

  • unique_lock以独占所有权的方式 管理 mutex 对象,所谓独占所有权,就是没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权。
  • lock_guard 一样,unique_lock 对象也能保证在其自身析构时它所管理的 Mutex 对象能够被正确地解锁(即使没有显式地调用 unlock 函数)。构造函数如下所示:
unique_lock() noexcept;									// 1. default
explicit unique_lock(mutex_type& m);					// 2. locking
unique_lock(mutex_type& m, try_to_lock_t tag);			// 3. try-locking
unique_lock(mutex_type& m, defer_lock_t tag) noexcept;	// 4. deferred
unique_lock(mutex_type& m, adopt_lock_t tag);			// 5. adopting

template <class Rep, class Period>						// 6. locking for
unique_lock(mutex_type& m, const chrono::duration<Rep,Period>& rel_time);

template <class Clock, class Duration>					// 7. locking until
unique_lock(mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time);

unique_lock(const unique_lock&) = delete;				// 8. copy [deleted]
unique_lock(unique_lock&& x);							// 8. move
  1. 新创建的 unique_lock 对象不管理任何 Mutex 对象。
  2. 新创建的 unique_lock 对象管理 m 对象,并通过调用 m.lock() 来获取锁(若未获取、则阻塞)。
  3. 新创建的 unique_lock 对象管理 m 对象,并通过调用 m.try_lock() 对尝试获取锁,失败并不阻塞。
  4. 新创建的 unique_lock 对象管理 m 对象,而不锁定 m 对象。
  5. 新创建的 unique_lock 对象管理 m 对象, m 应该是一个已经被当前线程锁住的 Mutex 对象。(并且当前新创建的 unique_lock 对象拥有对锁(Lock)的所有权)。
  6. 新创建的 unique_lock 对象管理 m 对象,并通过调用 m.try_lock_for(rel_time) 来锁住 Mutex对象 一段时间。
  7. 新创建的 unique_lock 对象管理 m 对象,并通过调用 m.try_lock_until(abs_time) 来在某个时间点之前锁住 Mutex 对象。
  8. unique_lock 对象 不存在复制构造,移动构造可正常使用。

    测试代码:

std::mutex mtx;                         // mutex对象
void print_thread_id (int id) {
    
             // 1. unique_lock对象获取 mutex的管理权,而不锁定它
    std::unique_lock<std::mutex> lck(mtx,std::defer_lock);
    
    lck.lock();                         // 2. 获取锁,阻塞
    std::cout << "thread #" << id << '\n';
    lck.unlock();
}

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

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

    return 0;
}

thread #1
thread #4
thread #5
thread #6
thread #3
thread #7
thread #2
thread #8
thread #9
thread #10

  • 以上例子为 unique_lock 第四个构造函数的使用,获取mutex对象的管理权,并不去锁定它。

2.2 unique_lock 构造

  • unique_lock 提供了以下成员函数:
void lock();								// 1. 手动加锁,未获取锁会阻塞
mutex_type* mutex() const noexcept;			// 2. Get mutex: 返回指向mutex的指针
operator=(unique_lock&& x);					// 3.1 移动赋值构造
operator=(const unique_lock&) = delete;		// 3.2 复制构造 删除

// 4. 返回是否拥有锁,可将 unique_guard 对象作为判断参数,if(unique_guard)
explicit operator bool() const noexcept;	

bool owns_lock() const noexcept;			// 5. 返回对象是否拥有锁
mutex_type* release() noexcept;				// 6. 释放 mutex 所有权,返回mutex指针
void swap(unique_lock& x) noexcept;			// 7. 交换两个 unique_guard 对象的内容

bool try_lock();							// 8. 尝试获取锁,返回状态
bool try_lock_for(chrono...& rel_time);		// 9. 在后续一段时间内尝试锁定互斥锁
bool try_lock_until(chrono...& abs_time);	// 10.直至某一时刻为止,一直尝试锁定互斥锁
void unlock();								// 11.释放锁,对象状态为false,若调用该函数前
											//    对象状态已经为false,则抛出异常
  • 看了以上11个成员函数,感觉很像 unique_ptrrelease() 很像。
  • 上一小节的例子中使用了unique_locklock()unlock()。此处使用测试代码 对 函数 try_locktry_lock_fortry_lock_until功能进行讲解。
std::timed_mutex mtx;

void fireworks () {
    
    
    std::unique_lock<std::timed_mutex> lck(mtx,std::defer_lock);

    while (!lck.try_lock_for(std::chrono::milliseconds(200))) {
    
         // 1. 等待200毫秒
        std::cout << "-";
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));   // 2. 延迟1000毫秒
    std::cout << "*\n";
}

int main () {
    
    
    std::thread threads[10];
    // spawn 10 threads:
    for (int i=0; i<10; ++i)
        threads[i] = std::thread(fireworks);

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

    return 0;
}

------------------------------------*
----------------------------------------*
-----------------------------------*
------------------------------*
-------------------------*
--------------------*
---------------*
----------*
-----*
*

  • try_*() 函数功能相似,上述例子使用了 try_lock_for()

猜你喜欢

转载自blog.csdn.net/u013271656/article/details/114878829