c++互斥锁

发布时间:2023年12月17日

C++线程同步之互斥锁

进行多线程编程,如果多个线程需要对同一块内存进行操作,比如:同时读、同时写、同时读写对于后两种情况来说,如果不做任何的人为干涉就会出现各种各样的错误数据。这是因为线程在运行的时候需要先得到CPU时间片,时间片用完之后需要放弃已获得的CPU资源,就这样线程频繁地在就绪态和运行态之间切换,更复杂一点还可以在就绪态、运行态、挂起态之间切换,这样就会导致线程的执行顺序并不是有序的,而是随机的混乱的,就如同下图中的这个例子一样,理想很丰满现实却很残酷。

在这里插入图片描述

解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,在C++11中一共提供了四种互斥锁:

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

互斥锁在有些资料中也被称之为互斥量,二者是一个东西。

1.std::mutex

不论是在C还是C++中,进行线程同步的处理流程基本上是一致的,C++的mutex类提供了相关的API函数:

1.1成员函数

lock()函数**用于给临界区加锁,并且只能有一个线程获得锁的所有权font>,它有阻塞线程的作用**,函数原型如下:

void lock();

独占互斥锁对象有两种状态:锁定和未锁定。如果互斥锁是打开的,调用lock()函数的线程会得到互斥锁的所有权,并将其上锁,其它线程再调用该函数的时候由于得不到互斥锁的所有权,就会被lock()函数阻塞当拥有互斥锁所有权的线程将互斥锁解锁,此时被lock()阻塞的线程解除阻塞,抢到互斥锁所有权的线程加锁并继续运行,没抢到互斥锁所有权的线程继续阻塞

除了使用lock()还可以使用try_lock()获取互斥锁的所有权并对互斥锁加锁,函数原型如下:

bool try_lock();

二者的区别在于try_lock()不会阻塞线程,lock()会阻塞线程

  • 如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回true
  • 如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回false

try_lock 尝试锁定互斥锁,如果互斥锁已经被其他线程锁定,则返回 false,否则返回 true。这可以用于避免线程阻塞,可以在锁定前进行检查。

#include <mutex>

std::mutex myMutex;

void myFunction() {
    if (myMutex.try_lock()) {
        // 访问共享资源的代码
        myMutex.unlock();
    } else {
        // 互斥锁被其他线程锁定,执行备选方案
    }
}

当互斥锁被锁定之后可以通过unlock()进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。该函数的函数原型如下:

void unlock();

通过介绍以上三个函数,使用互斥锁进行线程同步的大致思路差不多就能搞清楚了,主要分为以下几步:

  1. 找到多个线程操作的共享资源(全局变量、堆内存、类成员变量等),也可以称之为临界资源
  2. 找到和共享资源有关的上下文代码,也就是临界区(下图中的黄色代码部分)
  3. 在临界区的上边调用互斥锁类的lock()方法
  4. 在临界区的下边调用互斥锁的unlock()方法

线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了线性访问,访问效率降低了,但是保证了数据的正确性。

在这里插入图片描述

当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。

1.2线程同步

举个栗子,我们让两个线程共同操作同一个全局变量,二者交替数数,将数值存储到这个全局变量里边并打印出来。

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

// 创建全局锁对象
mutex mt1;
int num = 0;

void test01() {
	while (true) {
		mt1.lock();
		if (num == 100) {
			mt1.unlock(); // 没有这行代码 程序是无法结束的
			return;
		}
		cout << this_thread::get_id() << ": num = " << ++num << endl;
		mt1.unlock();
		this_thread::sleep_for(chrono::milliseconds(10));
	}
}

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

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

	return 0;
}

在上面的示例程序中,两个子线程执行的任务的一样的(其实也可以不一样,不同的任务中也可以对共享资源进行读写操作),在任务函数中把与全局变量相关的代码加了锁,两个线程只能顺序访问这部分代码(如果不进行线程同步打印出的数据是混乱且无序的)。另外需要强调一点:

  1. 在所有线程的任务函数执行完毕之前,互斥锁对象是不能被析构的,一定要在程序中保证这个对象的可用性。
  2. 互斥锁的个数和共享资源的个数相等,也就是说每一个共享资源都应该对应一个互斥锁对象。互斥锁对象的个数和线程的个数没有关系。

2.lock_guard

std::lock_guard 是 C++ 标准库提供的一个 RAII(Resource Acquisition Is Initialization)风格的锁管理类。它用于在特定范围内自动锁定和解锁互斥锁,确保在离开范围时释放锁。

就是说我们不用手动解锁了, lock_guard会帮助我们自己解锁

#include <iostream>
#include <mutex>
#include <thread>

std::mutex myMutex;
int sharedData = 0;

void myFunction() {
    std::lock_guard<std::mutex> lock(myMutex);

    // 在这个作用域内,myMutex 已经被锁定

    sharedData++; // 对共享数据的互斥访问

} // 在这个作用域结束时,lock_guard 析构,myMutex 自动解锁

上面的示例就可以这样写:

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

// 创建全局锁对象
mutex mt1;
int num = 0;

void test01() {
	while (true) {
		lock_guard<mutex> lock(mt1);
		if (num == 100) {
			return;
		}
		cout << this_thread::get_id() << ": num = " << ++num << endl;
		this_thread::sleep_for(chrono::milliseconds(10));
	}
}

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

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

	return 0;
}

? 通过修改发现代码被精简了,而且不用担心因为忘记解锁而造成程序的死锁,但是**这种方式也有弊端,在上面的示例程序中整个循环的体都被当做了临界区,多个线程是线性的执行临界区代码的,因此临界区越大程序效率越低,**还是需要根据实际情况选择最优的解决方案。

3.recursive_mutex

? std::recursive_mutex 是 C++ 标准库中提供的一种互斥量(mutex)的实现,它允许同一个线程多次对互斥量进行加锁。这意味着,如果一个线程已经拥有了 std::recursive_mutex 的锁,那么它可以再次加锁,而不会发生死锁。

? 在标准库中,互斥量是用于保护共享资源,防止多个线程同时访问这些资源,从而避免数据竞争。std::recursive_mutex 在互斥量的基础上加入了递归特性,使得同一个线程可以多次加锁,而不会引发死锁。

? 以下是 std::recursive_mutex 的主要特点和用法:

  1. 递归性: 允许同一线程多次加锁 std::recursive_mutex,而不会产生死锁。
  2. 锁定和解锁: 通过 lock()unlock() 方法进行锁定和解锁。
  3. 嵌套锁: 可以在同一线程中嵌套地对 std::recursive_mutex 进行多次加锁和解锁。
  4. 死锁避免: 适用于需要在递归调用中使用同步手段的情况,避免死锁。
  5. 性能开销: 与一般的互斥量相比,std::recursive_mutex 可能有一些性能开销,因为需要维护额外的信息来跟踪递归调用。

? 以下是一个简单的示例,演示了 std::recursive_mutex 的基本用法:

#include <iostream>
#include <mutex>
#include <thread>

std::recursive_mutex myMutex;

void foo(int threadID, int depth) {
    std::unique_lock<std::recursive_mutex> lock(myMutex);

    // 执行一些操作

    std::cout << "Thread " << threadID << " locked mutex at depth " << depth << std::endl;

    if (depth > 0) {
        foo(threadID, depth - 1); // 递归调用
    }

    // 执行一些其他操作

    std::cout << "Thread " << threadID << " unlocked mutex at depth " << depth << std::endl;
}

int main() {
    std::thread t1(foo, 1, 3);
    std::thread t2(foo, 2, 2);

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

    return 0;
}

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

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

4.time_mutex

std::timed_mutex 是 C++ 标准库中提供的一种带有超时功能的互斥量(mutex)实现。它允许线程在尝试获取锁的过程中设置超时时间,如果在规定的时间内无法获得锁,线程可以选择放弃或者执行其他逻辑。这个超时机制对于避免死锁和处理锁等待时间较长的情况很有用。

以下是 std::timed_mutex 的主要特点和使用方法:

  1. 锁定和解锁: 通过 lock()unlock() 方法进行锁定和解锁,与常规互斥量类似。

  2. 超时机制:

    通过 try_lock_for() 和 try_lock_until()方法,线程可以尝试在一段时间内获取锁,如果超过指定的时间仍未获取到锁,就会返回失败。

    • try_lock_for(duration):在指定的时间段内尝试获取锁。
    • try_lock_until(time_point):在指定的时间点之前尝试获取锁。
  3. 等待超时: 如果超时机制启用,线程可以通过返回值来判断是否成功获取锁。如果返回 true,表示成功获取锁;如果返回 false,表示在规定时间内未能获取锁。

  4. 可与条件变量一起使用: std::timed_mutex 可以与 std::condition_variable 一起使用,实现更为复杂的同步机制。

以下是一个简单的示例,演示了 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 << "当前线程ID: " << this_thread::get_id()
                << ", 得到互斥锁所有权..." << endl;
            // 模拟处理任务用了一定的时长
            this_thread::sleep_for(chrono::seconds(10));
            // 互斥锁解锁
            g_mutex.unlock();
            break;
        }
        else
        {
            cout << "当前线程ID: " << this_thread::get_id()
                << ", 没有得到互斥锁所有权..." << endl;
            // 模拟处理其他任务用了一定的时长
            this_thread::sleep_for(chrono::milliseconds(50));
        }
    }
}

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

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

    return 0;
}

程序运行结果为:

当前线程ID: 18912, 得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 得到互斥锁所有权...

? 在上面的例子中,通过一个while循环不停的去获取超时互斥锁的所有权,如果得不到就阻塞1秒钟,1秒之后如果还是得不到阻塞50毫秒,然后再次继续尝试,直到获得互斥锁的所有权,跳出循环体。

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

5.unique_lock 和 shared_mutex

unique_mutex

std::unique_lock 是一个灵活的锁管理类,与 std::lock_guard 类似,但提供了更多的功能,如可延迟锁定、手动解锁等。它可以与各种互斥锁一起使用,包括 std::mutexstd::timed_mutexstd::recursive_mutex 等。

#include <mutex>

std::mutex myMutex;
std::unique_lock<std::mutex> myUniqueLock(myMutex);
shared_mutex(C++14引入)

std::shared_mutex 是一种读写锁,允许多个线程同时获取读锁,但只有一个线程能够获取写锁。这对于共享和独占访问共享资源非常有用。

#include <shared_mutex>

std::shared_mutex mySharedMutex;
文章来源:https://blog.csdn.net/f593256/article/details/134952202
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。