首页 > 其他分享 >多线程(互斥锁,条件变量,虚假唤醒)知识点总结

多线程(互斥锁,条件变量,虚假唤醒)知识点总结

时间:2024-01-05 23:32:34浏览次数:36  
标签:std 知识点 lock 阻塞 互斥 线程 mutex 多线程

互斥锁mutex

C++11一共提出四种互斥锁

std::mutex:独占的互斥锁,不能递归使用 std::timed_mutex:带超时的独占互斥锁,不能递归使用 std::recursive_mutex:递归互斥锁,不带超时功能 std::recursive_timed_mutex:带超时的递归互斥锁

1. mutex

mutex有三个成员函数: void lock(); bool try_lock(); void unlock();

1.1 lock() lock()函数用于给临界区加锁,并且只能有一个线程获得锁的所有权

独占互斥锁对象有俩种状态: 锁定和未锁定。

lock 函数用于阻塞地锁定互斥量。 当一个线程调用 lock 时,如果互斥量当前没有被其他线程锁定,那么调用线程将成功锁定互斥量,继续执行后续代码。 如果互斥量已经被其他线程锁定,那么调用线程将阻塞,直到互斥量可用。 std::mutex myMutex;

void myFunction() { myMutex.lock(); // 互斥区域 myMutex.unlock(); } 1.2 try_lock() try_lock 函数是 std::mutex 的成员函数,它尝试非阻塞地锁定互斥量。 当一个线程调用 try_lock 时,如果互斥量当前没有被其他线程锁定,那么调用线程将成功锁定互斥量,继续执行后续代码。 如果互斥量已经被其他线程锁定,那么 try_lock 不会阻塞,而是返回一个失败的状态,让调用线程知道互斥量当前不可用。 std::mutex myMutex;

void myFunction() { if (myMutex.try_lock()) { // 互斥区域 myMutex.unlock(); } else { // 互斥量已被锁定,处理失败的情况 } } 1.3 unlock() 当互斥锁被锁定之后可以通过unlock()进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。

1.4 std::lock_guard(轻锁) lock_guard是C++11新增的一个模板类,使用这个类,可以简化互斥锁lock()和unlock()的写法,同时也更安全。 因为其基于 RAII原则的设计,通过在构造函数中锁定互斥锁,在析构函数中解锁互斥锁,确保在作用域结束时互斥锁一定被正确解锁,

#include <iostream> #include <mutex>

std::mutex myMutex;

void someFunction() { std::lock_guardstd::mutex lock(myMutex); // 在作用域内自动锁定和解锁

// 互斥区域
std::cout << "Critical section protected by lock_guard" << std::endl;

} // 在作用域结束时,lock_guard 的析构函数会自动解锁互斥锁 自动锁定和解锁: 一旦 std::lock_guard 对象被创建,它会自动锁定互斥锁。在作用域结束时,不论是正常结束还是异常结束,std::lock_guard 的析构函数都会被调用,自动解锁互斥锁。 不能手动解锁: std::lock_guard 的设计目的是为了简化互斥锁的使用,因此它没有提供手动解锁的方法。互斥锁的锁定和解锁完全由 std::lock_guard 管理,确保锁定互斥锁的时间段与 std::lock_guard 对象的生命周期一致。 lock_guard缺点:不能手动解锁,只有在大括号{}结束的时候才会调用析构函数解锁,这导致如果{}定义域的范围很大的话,锁的粒度也会很大,很大程度上影响效率

为了解决上述缺点,unique_lock就出现了

1.5 std::unique_lock(重锁) 跟lock_guard对比:

优点:可以利用其unlock()函数主动解锁

缺点:效率偏低,毕竟unique_lock内部会维护一个锁的状态

#include <iostream> #include <mutex>

std::mutex myMutex;

void someFunction() { std::unique_lockstd::mutex ul(m); // unique_lock 方式上锁 std::cout << "another worker thread is running..." << std::endl; // 这里可以写一些需要互斥保护的代码 std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "another worker thread is done." << std::endl; ul.unlock(); // 手动释放锁 //do something... } // 如果锁未释放,unique_lock 会在此自动释放锁

2. std::recursive_mutex

递归互斥锁,允许同一线程多次对互斥量进行锁定,防止死锁。

下面这个例子中会由于重复加锁导致死锁

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

struct Calculate
{
    Calculate() : m_i(6) {}
    //重复加锁
    void mul(int x)
    {
        lock_guard<mutex> locker(m_mutex);
        m_i *= x;
    }
    
    void div(int x)
    {
        lock_guard<mutex> locker(m_mutex);
        m_i /= x;
    }
    
    //第一次加锁
    void both(int x, int y)
    {
        lock_guard<mutex> locker(m_mutex);
        mul(x);
        div(y);
    }

    int m_i;
    mutex m_mutex;
};

int main()
{
    Calculate cal;
    cal.both(6, 3);
    return 0;
}

上面的程序中执行了cal.both(6, 3);调用之后,程序就会发生死锁,在both()中已经对互斥锁加锁了,继续调用mult()函数,已经得到互斥锁所有权的线程再次获取这个互斥锁的所有权就会造成死锁(在C++中程序会异常退出,使用C库函数会导致这个互斥锁永远无法被解锁,最终阻塞所有的线程)。要解决这个死锁的问题,一个简单的办法就是使用递归互斥锁std::recursive_mutex,它允许一个线程多次获得互斥锁的所有权。

虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是还是建议少用,主要原因如下:

使用递归互斥锁的场景往往都是可以简化的,使用递归互斥锁很容易放纵复杂逻辑的产生,从而导致bug的产生 递归互斥锁比非递归互斥锁效率要低一些。 递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权,但最大次数并未具体说明,一旦超过一定的次数,就会抛出std::system错误。

3. std::timed_mutex

std::timed_mutex是超时独占互斥锁,主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置了一个超时时长,超时后线程就可以解除阻塞去做其他事情了。

std::timed_mutex比std::_mutex多了两个成员函数:try_lock_for()和try_lock_until():

void lock();
bool try_lock();
void unlock();

// std::timed_mutex比std::_mutex多出的两个成员函数
template <class Rep, class Period>
  bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);

template <class Clock, class Duration>
  bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
try_lock_for函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度
try_lock_until函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点
关于两个函数的返回值:当得到互斥锁的所有权之后,函数会马上解除阻塞,返回true,如果阻塞的时长用完或者到达指定的时间点之后,函数也会解除阻塞,返回false
以下示例程序演示std::timed_mutex的使用:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

timed_mutex g_mutex;

void work()
{
    chrono::seconds timeout(1);
    while (true)
    {
        // 通过阻塞一定的时长来争取得到互斥锁所有权
        if (g_mutex.try_lock_for(timeout))
        {
            cout << "The current thread 's ID is: " << this_thread::get_id() 
                << ", get the mutex..." << endl;
            // 模拟处理任务用了一定的时长
            this_thread::sleep_for(chrono::seconds(3));
            // 互斥锁解锁
            g_mutex.unlock();
            break;
        }
        else
        {
            cout << "The current thread 's ID is: " << this_thread::get_id() 
                << ",not get the mutex..." << endl;
            // 模拟处理其他任务用了一定的时长
            this_thread::sleep_for(chrono::milliseconds(50));
        }
    }
}

int main()
{
    thread t1(work);
    thread t2(work);

    t1.join();
    t2.join();

    return 0;
}
/*
===========output==========
The current thread 's ID is: 2, get the mutex...
The current thread 's ID is: 3,not get the mutex...
The current thread 's ID is: 3,not get the mutex...
The current thread 's ID is: 3, get the mutex...

==== Program exited with exit code: 0 ====
Time elapsed: 000:06.250 (MM:SS.MS)
Press any key to continue...
*/

关于递归超时互斥锁std::recursive_timed_mutex的使用方式和std::timed_mutex是一样的,只不过它可以允许一个线程多次获得互斥锁所有权,而std::timed_mutex只允许线程获取一次互斥锁所有权。另外,递归超时互斥锁std::recursive_timed_mutex也拥有和std::recursive_mutex一样的弊端,不建议频繁使用。

条件变量

条件变量是C++11提供的另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来使用,C++11提供了两种条件变量:

标签:std,知识点,lock,阻塞,互斥,线程,mutex,多线程
From: https://blog.51cto.com/u_14882565/9120405

相关文章

  • 【Redis技术专区】「原理分析」深入探索和分析Redis6.0为何需要启用多线程
    背景介绍在Redis6.0版本中,引入了多线程技术,这是为了进一步提高Redis的性能和并发处理能力。通过启用多线程,Redis能够同时处理多个客户端请求,有效地利用多核处理器资源,提高系统的吞吐量和响应速度。开启多线程可以在处理阻塞操作时提供更好的性能,例如慢查询、持久化操作等。此外,多......
  • 异构编程模型知识点总结
    如何理解“异构”异构环境指的是计算系统中包含不同类型和架构的计算资源的情况。这些计算资源可能拥有不同的体系结构、处理器类型、内存层次结构、加速器等。在异构环境中,系统可以包含多个不同类型的硬件设备,例如:CPU(CentralProcessingUnit):通用的中央处理器,负责执行通用计算......
  • Golang Defer 必会知识点
    Golang中的一个关键字,用于延迟执行指定的函数调用。在程序执行到defer语句时,该函数调用会被推迟到当前函数返回之前执行,无论当前函数是正常返回还是发生异常退出。Defer语句可以用来在函数执行完毕后清理资源,确保资源的释放不会被遗漏。通过使用defer,我们能够更好地管理和控......
  • 多线程
    NSThreadNSThread是iOS和macOS中用于多线程编程的类。它封装了线程的创建和管理,允许开发者在后台执行任务而不阻塞主线程。这样可以保持应用界面的响应性,同时执行如下载文件、数据处理等耗时操作。使用NSThread的常用方法和属性:detachNewThreadSelector:toTarget:withObject......
  • Bmwgroupdesignworks爬虫,网站作品信息多线程采集爬虫源码!
    一个比较简单国外设计站点,作品信息采集爬虫源码,比较简单,采集的内容包括标题、内容及图片信息,适合新人学习参考使用。网站作品信息采集中,关于图片的采集下载使用了重试以及多线程的方式爬取采集下载,适合Python爬虫新人练手使用和学习,如果你正在找练手网站,不妨尝试爬取下载数据。三......
  • Linux 静态链接和动态链接相关知识点总结
    staticlibrary和sharedlibrary的区别静态库(StaticLibrary)和共享库(SharedLibrary)是两种不同的库的形式,它们在链接和加载的方式上有一些关键的区别。静态库(StaticLibrary):文件格式:静态库的代码和数据在编译时被复制到程序的可执行文件中。文件扩展名:在大多数系统中,静态......
  • java基础知识点API之String详解--String基础看它就够了
    一:概述java中的String在java.lang包下,使用时可以直接使用不需要进行导包。字符串在日常使用中非常多,例如之前的变量定义。二:详细说明<1>JDK-帮助文档中对Strng类的介绍<2>字符串常量的创建,字符串常量在创建之后,它们的值不能被更改,但是可以被共享。publicstaticvoidmain(String[......
  • 多线程循环打印123
    1、多线程循环打印123importjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassPrintThread{privateLocklock=newReentrantLock();privatevolatileintflag......
  • 关于python3多线程和协程
    以下内容部分由chatgpt生成,本文仅作为备忘和记录。asyncio.sleep和time.sleep都是用于在Python中进行延迟操作的函数,但它们的工作方式和使用场景有一些不同。asyncio.sleep:asyncio.sleep是用于在异步代码中进行暂停的函数,它是asyncio模块中的一部分。在异步程序中......
  • nodejs多线程-共享内容
    前言:昨天遇到基于Nodejs启动多线程,以便不同服务之间可以调用(共享内存) worker_threadsnode官方文档注明了:worker_threads模块允许使用并行地执行JavaScript的线程。与child_process或cluster不同,worker_threads可以共享内存。它们通过传输ArrayBuffer实例或共享Sh......