2

std::thread线程库详解(2)

 3 years ago
source link: http://www.cnblogs.com/ink19/p/std_thread-2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

目录

  • 最基本的锁 std::mutex
  • 递归锁 std::recursive_mutex
  • 共享锁 std::shared_mutex (C++17)

简介

上一篇博文中,介绍了一下如何创建一个线程,分别是 std::threadstd::jthread (C++20) 。这两种方法相似, std::jthread 相对来说,更加方便一些,具体可以再看看原来的博文, std::thread线程详解(1)

这一次,我将介绍一下,多线程的锁。锁在多线程中是使用非常广泛的。是多线程中最常见的同步方式。主要介绍的锁有 mutexrecursive_mutexshared_mutex

最基本的锁 std::mutex

使用

std::mutex 是最基本的锁,也是最常见的锁。它提供了最基本的多线程编程同步方法。

using namespace std::chrono_literals;

std::mutex g_mutex;

void thread_func() {
    g_mutex.lock();
    std::cout << "Thread out 1: " << std::this_thread::get_id() << std::endl;;
    std::this_thread::sleep_for(1s);
    std::cout << "Thread out 2: " << std::this_thread::get_id() << std::endl;;
    g_mutex.unlock();
}

int main() {
    std::cout << "Mutex Test." << std::endl;
    std::thread thread1(thread_func);
    std::thread thread2(thread_func);
    thread1.join();
    thread2.join();
    return 0;
}

以上示例中,只有一个线程函数 thread_func ,它的工作很简单:

首先对 g_mutex 加锁,然后输出一段字符串,接着休眠1s,输出第二段字符串,最后对 g_mutex 进行解锁。

输出结果如下:

uuEzeqR.png!mobile

锁的本质是解决多线程对同一资源竞争读写的问题。这里我们的资源是标准输出 std::cout 。锁的存在让输出有序,可预测了。

方法和属性

  • lock() 为对象加锁,如果已经被锁了,则阻塞线程;
  • try_lock() 尝试加锁,如果已经被加锁,则返回false,否则将对其进行加锁并返回true;
  • unlock() 为对象解锁,通常和加锁( lock()try_lock() )成对出现;
  • native_handle() 返回锁的POSIX标准对象。

递归锁 std::recursive_mutex

std::recursive_mutex 是一个递归锁,方法和使用都和 std::mutex 类似。唯一的不同是, std::mutex 在同一时间,只允许加锁一次,而 std::revursive_mutex 允许同一线程下进行多次加锁。如:

// 定义递归锁
std::recursive_mutex g_mutex;

// 线程函数
void thread_func(int thread_id, int time) {
    g_mutex.lock();
    std::cout << "Thread " << thread_id << ": " << time << std::endl;
    if (time != 0) thread_func(thread_id, time - 1);
    g_mutex.unlock();
}

// 初始化线程
std::thread thread1(thread_func, 1, 3);
std::thread thread2(thread_func, 2, 4);

这一次的方法和之前的略有不同,为了更加直观的观察不同的线程,这次是在输入的时候输入一个标志来区分不同的线程。可以清楚的看到,这是一个递归函数,每次调用的时候都将time减少1,直到其变为0。需要注意的是,在递归的时候并没有释放锁,而是直接进入,因此在第二层遍历的时候,又会对 g_mutex 进行一次加锁,如果是普通的锁,次数将会阻塞进程,变成死锁。但是此时使用的是递归锁,它允许在同一个线程,多次加锁,因此这个程序可以成功运行,并获得输出。

aqQVFn.png!mobile

递归锁的方法和普通锁的方法类似。

共享锁 std::shared_mutex (C++17)

std::shared_mutex 在C++14已经存在了,但是在C++14中的 std::shared_mutex 是带timing的版本的读写锁(也就是说,C++14中的 std::shared_mutex 等于C++17中的 std::shared_timed_mutex )。读写锁有两种加锁的方式,一种是 shared_lock() ,另一种 lock()shared_lock 是读模式,而 lock 是写模式。读写锁允许多个读加锁,而写加锁和其他所有加锁互斥。即同一时间下:

  • 允许多个线程同时读;
  • 只允许一个线程写;
  • 写的时候不允许读,读的时候不允许写。

示例:

// 共享锁
std::shared_mutex g_mutex;

// 读线程 1
void thread_read_1_func(int thread_id) {
    // 第一个获取读权限
    g_mutex.lock_shared();
    std::cout << "Read thread " << thread_id << " out 1." << std::endl;
    // 睡眠2s,等待读线程2,获取读权限,确认可以多个线程进行读加锁
    std::this_thread::sleep_for(2s);
    std::cout << "Read thread " << thread_id << " out 2." << std::endl;
    // 解锁读
    g_mutex.unlock_shared();
}

void thread_read_2_func(int thread_id) {
    // 睡眠500ms,确保读线程1先获取锁
    std::this_thread::sleep_for(500ms);
    g_mutex.lock_shared();
    std::cout << "Read thread " << thread_id << " out 1."  << std::endl;
    std::this_thread::sleep_for(3s);
    std::cout << "Read thread " << thread_id << " out 2."  << std::endl;
    g_mutex.unlock_shared();
}

void thread_write_1_func(int thread_id) {
    // 确保读线程先获得锁,确认读写互斥
    std::this_thread::sleep_for(300ms);
    g_mutex.lock();
    std::cout << "Write thread " << thread_id << " out 1."  << std::endl;
    g_mutex.unlock();
}

其输出为:

FNbua2A.png!mobile

带超时的锁

上面介绍的所有的锁,都带有超时版本。即 timed_mutexrecursive_timed_mutexshared_timed_mutex 。他们使用时,和普通版本类似,不过 try_lock 方法多了两个超时的版本 try_lock_fortry_lock_until 。调用这一函数时,如果锁已经被获取了,线程将会阻塞一段时间,如果这一段时间内,获取到了锁则返回 true ,否则返回 false

这里我们只介绍 timed_mutex ,其他的类似。

void thread_func(int thread_id) {
    if (!g_mutex.try_lock_for(0.5s)) return;
    std::cout << "Thread out 1: " << thread_id << std::endl;;
    std::this_thread::sleep_for(1s);
    std::cout << "Thread out 2: " << thread_id << std::endl;;
    g_mutex.unlock();
    g_mutex.native_handle();
}

其输出为:

BzqYBnY.png!mobile

可以看到,这里只有一个线程有输出,另一个线程,在等待0.5s后直接退出了(没有获取到锁)。

总结

本文主要介绍了三种不同的锁,普通锁,递归锁,读写锁。三个锁有着不一样的使用方法,但是可以确定的是,过多的使用锁,会导致程序中的串行部分过多,并行效果不好。因此对于锁的使用,需要尽量的克制,尽量的合理。

下一篇文章将介绍锁的管理。

博客原文: https://www.cnblogs.com/ink19/p/std_thread-2.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK