C++ 多线程编程(二) 各种各样的锁

这篇具有很好参考价值的文章主要介绍了C++ 多线程编程(二) 各种各样的锁。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

 

目录

前言

一、基本锁

1. 互斥锁(mutex)

2. 定时互斥锁(timed_mutex)

3. 条件变量 (condition_variable)

4. 读写锁 (shared_mutex)

5. 递归锁(recursive_mutex)

6. 自旋锁 (spinlock)

二、RAII锁

1. lock_guard

2. unique_lock

3. shared_lock

三、信号量

总结



前言

多线程编程一个重要的问题就是数据竞争,多个线程同时获取一份数据的使用权,如果不加以控制,必然会导致程序的崩溃。

锁(mutex),就是用来调度各线程使用共享数据的中介。锁有已锁和未锁两个状态,处于未锁状态的锁,线程可以将它锁上,此时只能由该线程访问共享数据;如果锁处于已锁状态,则需要等待别的线程解锁之后,才能上锁并使用数据,这样就避免了多个线程同时访问一份数据。如果不好理解,可以类比以下厕所的一个坑位。。。

本篇文章主要对C++现有的锁进行介绍,由于锁的种类繁多,而且相关文章已经非常多了,本文不再细讲各种锁的技术细节,而是更注重于各种锁的由来,以及他们针对的问题,如此便能针对自己面临的问题,选择合适的锁

今天是2023年4月21号,目前我使用的是C++20,因此在这篇文章,我们记录C++20中各类的锁。主要包括基本锁和RAII锁,基本锁包括互斥锁 (mutex),定时互斥锁 (timed_mutex),条件变量 (condition_variable),读写锁 (shared_mutex),递归锁 (recursive_mutex),自旋锁 (spinlock)。RAII锁是基本基本锁实现的更加智能的锁。


一、基本锁

1. 互斥锁(mutex)

起始版本 C++11
头文件 <mutex>
接口

void lock();

锁定互斥锁,若另一线程已锁定互斥锁,则到 lock 的调用将阻塞执行,直至获得锁。

bool try_lock();

尝试锁定互斥锁,成功上锁返回true,若另一线程已锁定互斥锁,则返回false ,不会阻塞

void unlock();

解锁互斥锁。

 描述:互斥锁是最基本、简单的锁,只有三个接口,lock()上锁,unlock()解锁,try_lock()尝试上锁。lock()只会进行一次上锁操作,如果失败了(其他线程正在占用),就会进入睡眠状态并阻塞线程,等到互斥锁解锁之后,系统会将其唤醒并进行上锁操作(当然,这时候也不一定能成功)。try_lock()上锁失败会返回false,并继续执行后面的代码。

针对问题:为了初步解决多线程之间数据竞争的问题。

使用场景:由于是最基本的锁,在一些复杂场景中难以满足需求,因此多用于一些不算复杂的情况。

2. 定时互斥锁(timed_mutex)

起始版本 C++11
头文件 <mutex>
接口

void lock();

锁定互斥锁,若另一线程已锁定互斥锁,则到 lock 的调用将阻塞执行,直至获得锁。

bool try_lock();

尝试锁定互斥锁,成功上锁返回true,若另一线程已锁定互斥锁,则返回false,不会阻塞

void unlock();

解锁互斥锁。

template< class Rep, class Period >
bool try_lock_for( const std::chrono::duration<Rep,Period>& timeout_duration );

尝试加锁,如果锁被占用,阻塞最久 timeout_duration 这个由用户输入的时间,如果在此期间未得到锁,则返回false;成功获得锁时返回 true 。

template< class Clock, class Duration >
bool try_lock_until( const std::chrono::time_point<Clock,Duration>& timeout_time );

尝试获取互斥锁。阻塞直至抵达指定的 timeout_time 或得到锁,成功得到锁时返回ture,失败时返回false。

 描述:相对于mutex而言,timed_mutex增加了try_lock_fortry_lock_until两个接口,前者在尝试获取锁失败时会等待指定时间,在此期间会不断尝试获取锁;后者会获取锁失败时会等待至指定时刻。如果忽视这两个接口,那么timed_mutexmutex无异。

针对问题:为了缓解mutex不够灵活的问题,增加了可选的等待时延。

3. 条件变量 (condition_variable)

起始版本 C++11
头文件 <condition_variable>
接口

void wait( std::unique_lock<std::mutex>& lock );

等待并阻塞线程,直到别的线程进行通知,wait( )会释放lock

template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );

pred是一个lambda函数,返回值为bool,此时的wait等价于

while (!pred()) {
    wait(lock);
}

wait_for(time)    函数原型太长了,就没有全写出来,wait_until也是。

最多等待time时间,就解除阻塞。

wait_until(time)

最多等待至time时刻,就解除阻塞。

void notify_one( ) noexcept;

知一个正在wait的线程

void notify_all( ) noexcept;

通知所有正在wait的线程

描述:条件变量的作用是用于多线程之间的线程同步。线程同步是指线程间需要按照预定的先后顺序进行的行为,比如我想要线程1完成了某个步骤之后,才允许线程2开始工作,这个时候就可以使用条件变量来达到目的。具体操作就可以是,线程2调用wait函数进行等待,线程1调用notify函数进行通知,这样就能保证线程1和线程2的顺序。

针对问题:上边说的线程同步问题,用互斥锁mutex也能实现,但是并不优美。这里我引用别人的一个解释:

以一个生产者消费者的例子来看,生产者和消费者通过一个队列连接,因为队列属于共享变量,所以在访问队列时需要加锁。生产者向队列中放入消息的时间是不一定的,因为消费者不知道队列什么时候有消息,所以只能不停循环判断或者sleep一段时间,不停循环会浪费cpu资源,如果sleep那么要sleep多久,sleep太短又会浪费资源,sleep太长又会导致消息消费不及时。

应用场景:多用于生产消费队列中。

相关文章:

 C++11条件变量condition_variable详解

为什么互斥锁和条件变量要一起使用

4. 读写锁 (shared_mutex)

起始版本 C++17
头文件 <shared_mutex>
接口

void lock();

排他式锁定互斥。若另一线程已锁定互斥,则到 lock 的调用将阻塞执行,直至获得锁。这里说的“另一线程已锁定互斥”,不仅可能是别的线程提前调用了lock,也可能是别的线程提前调用了shared_lock函数,这也是shared_mutex特殊的地方,有两种上锁方式。

bool try_lock();

尝试排他式锁定互斥。立即返回,成功获得锁时返回 true ,否则返回 false

void unlock();

解锁排他式互斥。

void lock_shared();

以共享式锁定互斥。如果另一线程以排他式锁定互斥,则会阻塞,直到获得锁;如果另一线程或者多个线程以共享式锁定了互斥,则调用者同样会获得锁。

bool try_lock_shared();

尝试共享式锁定互斥。立即返回,成功获得锁时返回 true ,否则返回 false

void unlock_shared();

解锁共享互斥。

描述:shared_mutex有两种上锁方式,一种是排他式,另一种是共享式。排他式上锁同一时间只允许一个线程拥有锁,共享式上锁允许多个线程拥有锁。

针对问题:对于一个线程写,多个线程读的场景,mutex的效率很低。因为不仅读与写之间要加锁,读与读之间也要加锁,但是读与读之间的加锁是不必要的,毕竟它不会改变数据,于是就产生了可以同时读的需求。

适用场景:有多个读线程存在的时候,可以考虑读写锁。

相关文章:

C++多线程——读写锁shared_lock/shared_mutex

5. 递归锁(recursive_mutex)

起始版本 C++11
头文件 <mutex>
接口

void lock();

锁定互斥。若另一线程已锁定互斥,则到lock的调用将阻塞执行,直至获得锁。在同一线程中,可以多次调用lock,不会造成死锁,但是要调用相应次数的unlock

bool try_lock();

尝试锁定互斥。立即返回。成功获得锁时返true ,否则返回 false

void unlock();

解锁互斥。要与lock调用的次数一致才能完成解锁。

描述:recursive_mutexmutex 唯一的区别就在于它可以在同一个线程里多次加锁。

针对问题:想象这样一个场景,函数A调用了函数B,而且函数A和B都访问了一份共享数据,这样就可能造成死锁。

适用场景:共享数据存在递归调用的时候。

相关文章:

递归锁recursive_mutex的原理以及使用

6. 自旋锁 (spinlock)

C++标准库目前没有实现自旋锁。

描述:自旋锁与互斥锁(mutex)的区别在于,mutex调用lock之后,会进入睡眠状态,等到锁可用了,再由cpu唤醒,再次获取锁;而自旋锁不会进去睡眠状态,会一直尝试获取锁。这种一直尝试获取锁的行为很耗cpu资源,所以要用在合适的场景。

用mutex就可以实现一个简单的自旋锁:

while(!mutex.try_lock()){}

针对问题:考虑这样一种情况,如果线程对共享资源占用时间非常短,也就是说mutex.lock()等待的时间非常短,那么CPU将线程挂起再唤醒所耗费的资源可能要大于一直尝试加锁。

适用场景:每个线程对共享资源占用时间非常短的情况。

相关文章:

c++之理解自旋锁

二、RAII锁

首先,什么是RAII?

RAII全称是Resource Acquisition Is Initialization,翻译过来是资源获取即初始化,RAII机制用于管理资源的申请和释放。对于资源,我们通常经历三个过程,申请,使用,释放,这里的资源不仅仅是内存,也可以是文件、socket、锁等等。但是我们往往只关注资源的申请和使用,而忘了释放,这不仅会导致内存泄漏,可能还会导致业务逻辑的错误。c++之父给出了解决问题的方案:RAII,它充分的利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。

简而言之就是,将资源(内存、socket、锁等等)与一个局部变量绑定,这样就可以避免我们忘记释放资源,智能指针也是这样的思想。

RAII锁本质上都是模板类,模板类型是这种锁,也可以理解为对锁的进一步封装。

1. lock_guard

起始版本 C++11
头文件 <mutex>
原型

template< class Mutex >
class lock_guard;

构造函数

explicit lock_guard( mutex_type& m );

等于调用 m.lock() 。

析构函数

~lock_guard();

等效地调用 m.unlock()

 描述:lock_guard的构造函数需要传入一个锁,我们称这个锁为关联锁,然后在构造函数内部进行加锁,在析构函数中进行解锁。

针对问题:针对用户可能忘记解锁的问题。

适用场景:建议用RAII锁替换一般的锁,这样更加安全。

相关文章:

   走进C++11  RAII风格锁std::lock_guard/std::unique_lock

2. unique_lock

起始版本 C++11
头文件 <mutex>
构造函数

unique_lock的构造函数比较多,包含了默认构造函数,移动构造函数,带锁构造函数,甚至还可以选择加锁的方式,我列几个:

unique_lock() noexcept;     默认构造函数,没法直接加锁,需要先调用=或者swap;

explicit unique_lock( mutex_type& m );   带锁的构造函数,会调用m.lock();

unique_lock( mutex_type& m, std::defer_lock_t t ) ;  只与锁进行关联,不执行上锁; 

unique_lock( mutex_type& m, std::try_to_lock_t t );  相当于调用m.try_lock();

.........更多

析构函数

~unique_lock();

若当前线程拥有关联互斥且获得了其所有权,则解锁互斥。

上锁

void lock();    等同于m.lock(),m是与unique_lock关联的锁;

bool try_lock();   等同于m.try_lock();

bool try_lock_for(time);  等同于m.try_lock_for(time);

bool try_lock_until(time); 等同于m.bool try_lock_until();

void unlock();    解锁

修改unique_lock

void swap( unique_lock& other ) noexcept;     与另一 std::unique_lock 交换状态

mutex_type* release() noexcept;      将关联互斥解关联而不解锁它

其他

mutex_type* mutex() const noexcept;   返回指向关联互斥的指针,或若无关联锁则null

bool owns_lock() const noexcept;  检查当前线程是否占有关联锁。

描述:unique_lock是lock_guard的加强版,首先在构造函数中可以选择是否与锁关联,以及对锁的操作类型;其次它还可以像锁一样调用lock(),try_lock(),unlock()等函数,这使得unique_lock更加灵活;最后它增加了swap,release,owns_lock这些非常实用的接口。

针对问题:lock_guard的问题在于不够灵活,这个不够灵活首先体现在不能自由的释放锁,必须等到跳出作用域的时候调用析构函数才能解锁;再有就是lock_guard只提供了构造函数和析构函数,没有任何功能接口。

适用场景:建议用unique_lock替换lock_guard;

相关文章:

   走进C++11 RAII风格锁std::lock_guard/std::unique_lock

3. shared_lock

起始版本 C++14
头文件 <shared_mutex>
构造函数

shared_mutex的构造函数比较多,包含了默认构造函数,移动构造函数,带锁构造函数,甚至还可以选择加锁的方式,我列几个:

shared_mutex() noexcept;     默认构造函数,没法直接加锁,需要先调用=或者swap;

explicit shared_mutex( mutex_type& m );   带锁的构造函数,会调用m.lock();

shared_mutex( mutex_type& m,
std::defer_lock_t
t ) ;  只与锁进行关联,不执行上锁; 

shared_mutex( mutex_type& m, std::try_to_lock_t t );  相当于调用m.try_lock();

.........更多

析构函数

~shared_mutex();

若当前线程拥有关联互斥且获得了其所有权,则解锁互斥。

上锁

void lock();    等同于m.lock_shared(),m是与unique_lock关联的锁;

bool try_lock();   等同于m.try_lock_shared();

bool try_lock_for(time);  等同于m.try_lock_shared_for(time);

bool try_lock_until(time); 等同于m.bool try_lock_shared_until();

void unlock();   解锁互斥, 等同于m.unlock_shared()

修改shared_mutex

void swap( shared_mutex& other ) noexcept;     与另一 std::shared_mutex 交换状态

mutex_type* release() noexcept;      将关联互斥解关联而不解锁它

其他

mutex_type* mutex() const noexcept;   返回指向关联互斥的指针,或若无关联锁则null

bool owns_lock() const noexcept;  检查当前线程是否占有关联锁。

描述:上边这个表可以看出来,shared_lock与unique_lock的接口一模一样,他们唯一的区别在于,unique_lock是以排他式上锁,同一时刻只允许一个线程获取锁;而shared_mutex是以共享式上锁,允许多个线程同时获得锁。此外,shared_mutex只能与shared_mutex关联,因为别的锁压根没有lock_shared接口。

针对问题:shared_mutex算是对unique_lock的补充,因为unique_lock在存在多个只读线程的情况时,会有较大的性能损失,因为只读线程之间加锁是不必要的。

适用场景:与shared_mutex一样。

相关文章:

C++多线程——读写锁shared_lock/shared_mutex

三、信号量

描述:

信号量 (semaphore) 是一种轻量的同步原件,主要用处是控制对共享资源的并发访问数,说白点就是控制同一时间访问某一资源的线程数。比如网吧,就那么多位置,满了网管就不会给你开机子,必须等有人下机才能上机,这里的网吧就是共享资源,他有并发数量限制,网管就类似信号量的功能,限制网吧的同时访问数量。

信号量不是C++语言特有的概念,而是计算机学的概念,C++在C++20里才在标准库里对其进行了实现。

信号量内部会维护一个计数器,每当有线程访问资源,就将计数器减1;当有线程结束访问,就将计数器加1。如果计数器不大于0,那么新的访问请求就需要等待。

C++20提供了两种信号量,std::counting_semaphore和std::binary_semaphore,由于binary_semaphore是counting_semaphore的特例,因此这里主要介绍counting_semaphore。

起始版本 C++20
头文件 <semaphore>
构造函数

constexpr explicit counting_semaphore( std::ptrdiff_t desired );

创建一个计数器的值为 desired的信号量。

接口

void acquire();

若内部计数器大于 ​0​ 则尝试将它减少 1 ,否则阻塞直至它大于 ​0​ 。

在线程请求访问共享资源的时候调用。

void release( std::ptrdiff_t update = 1 );

将内部计数器的值增加 update 。在线程结束访问资源的时候,可以调用。或者你想增加资源的并发数,也可以调用。

bool try_acquire() noexcept;

尝试将内部计数器减少 1 ,成功返回true,失败返回false,不会阻塞线程。

template<class Rep, class Period>
bool try_acquire_for( const std::chrono::duration<Rep, Period>&  time );

尝试将计数器减1,如果失败则阻塞time时长

template<class Clock, class Duration>
bool try_acquire_until( const std::chrono::time_point<Clock, Duration>& abs_time );

尝试将计数器减1,如果失败则阻塞至abs_time时刻

constexpr std::ptrdiff_t max() noexcept;

返回最大并发数

针对问题:对于多个线程同时访问某个资源,shared_mutex是可以做到的,但是它不能控制并发的访问数量,而实际中很多计算机资源都存在并发限制,所以我们需要控制对这类资源的并发访问数量。

适用场景:适用于需要控制对资源的并发访问数量,比如线程池中控制线程数量,限制网络连接数等等。


总结

C++20还增加了闩 (latch) 与屏障 (barrier),暂时不写了,下次再写。文章来源地址https://www.toymoban.com/news/detail-617197.html

到了这里,关于C++ 多线程编程(二) 各种各样的锁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 前端 | (二)各种各样的常用标签 | 尚硅谷前端html+css零基础教程2023最新

    学习来源 :尚硅谷前端html+css零基础教程,2023最新前端开发html5+css3视频 系列笔记 : 【HTML4】(一)前端简介 【HTML4】(二)各种各样的常用标签 【HTML4】(三)表单及HTML4收尾 【CSS2】(四)CSS基础及CSS选择器 【CSS2】(五)CSS三大特性及常用属性 【CSS2】(六)CSS盒子模型

    2024年02月16日
    浏览(56)
  • 编程(39)----------多线程中的锁

    假设一个这样的场景: 在多线程的代码中, 需要在不同的线程中对同一个变量进行操作. 那此时就会出现问题: 多线程是并发进行的, 也就是说代码运行的时候, 俩个线程会同时对一个变量进行操作, 这样就会涉及到多线程的安全问题: 在这个代码中, 两个线程会分别对count进行自

    2024年02月07日
    浏览(45)
  • Java - JUC(java.util.concurrent)包详解,其下的锁、安全集合类、线程池相关、线程创建相关和线程辅助类、阻塞队列

    JUC是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题 java.lang.Thread.State tools(工具类):又叫信号量三组工具类,包含有 CountDownLatch(闭锁) 是一个同步辅助类,在完成一组正在其他线程中

    2024年02月05日
    浏览(35)
  • C++ 多线程编程和线程池

    c++ 多线程需要包含thread头文件 多线程调用函数代码如下 子线程和主线程同时执行,当子线程没执行完,主线程结束时,程序就会报错 join,主线程等待子线程执行完成后再执行,只能使用一次 detach,主程序结束,子线程会在后台执行 joinable,判断线程是否可以调用join和de

    2024年01月20日
    浏览(79)
  • C++线程入门:轻松并发编程

            在现代计算机应用程序中,我们经常需要处理并发任务,这就需要使用多线程来实现。C++是一种功能强大的编程语言,提供了丰富的线程支持,使得并发编程变得相对容易。         C++ 线程是一种多线程编程模型,可以在同一个程序中同时执行多个独立的任务

    2024年02月04日
    浏览(41)
  • C++ 多线程编程(三) 获取线程的返回值——future

    C++11标准库增加了获取线程返回值的方法,头文件为future,主要包括 future 、 promise 、 packaged_task 、 async 四个类。 那么,了解一下各个类的构成以及功能。 future是一个模板类,它是传输线程返回值(也称为 共享状态 )的媒介,也可以理解为线程返回的结果就安置在future中。

    2024年02月02日
    浏览(45)
  • C++ 并发编程实战 第二章 线程管控

    线程通过构建 std::thread 对象而自动启动 ,该对象指明线程要运行的任务。 对应复杂的任务,可以使用函数对象。 一旦启动了线程,我们就需明确是要等待它结束(与之汇合 join() ),还是任由它独自运行(与之分离 detach() ) ❗❗❗ 同一个线程的 .join() 方法不能被重复调用

    2023年04月08日
    浏览(42)
  • 《C++并发编程实战》读书笔记(1):线程管控

    包含头文件 thread 后,通过构建 std::thread 对象启动线程,任何可调用类型都适用于 std::thread 。 启动线程后,需要明确是等待它结束、还是任由它独自运行: 调用成员函数 join() 会先等待线程结束,然后隶属于该线程的任何存储空间都会被清除, std::thread 对象不再关联到已结

    2024年02月10日
    浏览(41)
  • C++ 多线程之OpenMP并行编程使用详解

    总结OpenMP使用详解 本文转载自:https://blog.csdn.net/AAAA202012/article/details/123665617?spm=1001.2014.3001.5506   OpenMP(Open Multi-Processing)是一种用于共享内存并行系统的多线程程序设计方案, 支持的编程语言包括C、C++和Fortran。 OpenMP提供了对并行算法的高层抽象描述, 通过线程实现并行化

    2024年02月06日
    浏览(42)
  • 《C++并发编程实战》读书笔记(2):线程间共享数据

    在C++中,我们通过构造 std::mutex 的实例来创建互斥量,调用成员函数 lock() 对其加锁,调用 unlock() 解锁。但通常更推荐的做法是使用标准库提供的类模板 std::lock_guard ,它针对互斥量实现了RAII手法:在构造时给互斥量加锁,析构时解锁。两个类都在头文件 mutex 里声明。 假设

    2024年02月10日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包