03_c++多线程_锁

2023-12-18 23:13:18

学习内容:
学习c++中的锁

  1. Mutex
  2. lock_guard
  3. unique_lock

简介:

在C++中,锁是一种同步机制,用于保护共享资源,以防止多个线程同时访问或修改该资源,从而避免数据竞争和不一致性。C++标准库提供了多种锁的实现,包括互斥锁、读写锁和条件变量。

  1. 互斥锁(Mutex):互斥锁是最常用的锁类型,它提供了两个基本操作:上锁(lock)和解锁(unlock)。当一个线程获得了互斥锁的所有权,其他线程必须等待该线程释放锁后才能获得锁并继续执行。互斥锁的实现通常基于操作系统提供的原子操作。
  2. 递归锁(Recursive Mutex):递归锁是一种特殊的互斥锁,允许同一个线程多次获得锁。当一个线程多次请求同一个递归锁时,必须释放对应数量的锁才能完全释放锁资源。递归锁用于解决一个线程需要多次获得同一个锁的场景。
  3. 读写锁(Read-Write Lock):读写锁允许多个线程同时读取共享数据,但只允许一个线程写入共享数据。读写锁的实现通常基于互斥锁和条件变量,读操作使用共享锁(读锁)进行同步,写操作使用互斥锁(写锁)进行同步。
  4. 条件变量(Condition Variable):条件变量用于实现线程间的条件同步。它可以让一个线程等待某个条件满足后再继续执行,或者通知其他线程某个条件已经满足。条件变量的实现通常基于互斥锁和条件变量。
概念

死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

注意:

在使用锁的时候,需要注意以下几点:

  1. 加锁的粒度:锁的粒度要尽量小,只锁定共享资源的部分,以减少锁的竞争和阻塞的线程数量。
  2. 加锁的顺序:如果在多个地方使用了多个锁,要保证在任何情况下都按照相同的顺序获得和释放锁,以避免死锁。
  3. 锁的使用范围:锁的使用范围要尽量小,尽快释放锁资源,以便其他线程能够及时获取锁。可以使用RAII(Resource Acquisition Is Initialization)技术,即在对象的构造函数中获得锁,在析构函数中释放锁。

总之,锁是一种重要的并发编程工具,在多线程程序中起到了保护共享资源的作用。正确使用锁能够有效地避免数据竞争和不一致性的问题,提高程序的并发性能和稳定性。

详细介绍

lock与unlock

C++中的互斥锁(mutex)是用于保护共享资源的重要工具。下面是互斥锁常见的使用方式:

  1. 创建互斥锁对象:可以使用std::mutex类创建互斥锁对象。
std::mutex mtx;
  1. 锁定互斥锁:使用std::mutex类的lock成员函数可以锁定互斥锁。如果互斥锁已被其他线程锁定,则当前线程会被阻塞直到互斥锁可用。
mtx.lock();
  1. 尝试锁定互斥锁:使用std::mutex类的try_lock成员函数可以尝试锁定互斥锁。如果互斥锁已被其他线程锁定,则try_lock函数会立即返回false,不会阻塞当前线程。
if (mtx.try_lock()) {
    // 互斥锁已成功被当前线程锁定
} else {
    // 互斥锁已被其他线程锁定
}
/*
	trylock():查看是否上锁,它有下列3种类情况:
	(1)未上锁返回false,并锁住;
	(2)其他线程已经上锁,返回true;
	(3)同一个线程已经对它上锁,将会产生死锁。
*/
  1. 解锁互斥锁:使用std::mutex类的unlock成员函数可以解锁互斥锁。注意,在解锁之前,当前线程必须已锁定互斥锁。
mtx.unlock();
  1. 自动加锁和解锁:使用std::unique_lock类可以实现自动加锁和解锁。std::unique_lock对象在创建时自动加锁,析构时自动解锁。
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx); // 加锁
// 锁定期间对共享资源的操作
// 解锁
lock_guard

lock_guard是C++11引入的一种用于简化线程锁的RAII(资源获取即初始化)封装类。它是一个模板类,用于管理一个std::mutex或std::recursive_mutex的锁。

通过使用lock_guard,可以在一个代码块内自动获取锁,并在代码块结束时自动释放锁,从而确保线程安全。

lock_guard类的定义如下:

template <class Mutex>
class lock_guard {
public:
    explicit lock_guard(Mutex& mtx);
    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;
    ~lock_guard();
};

构造函数lock_guard接受一个互斥量(Mutex)的引用作为参数,并在构造时自动获取锁。析构函数在对象销毁时自动释放锁。

使用lock_guard的一般步骤为:

  1. 创建一个std::mutex或std::recursive_mutex的实例。
  2. 在需要进行线程同步的代码块之前创建一个lock_guard对象,将互斥量作为参数传入。
  3. lock_guard对象会在构造时获取锁,保证代码块的线程安全性。
  4. lock_guard对象会在代码块结束时,即在其作用域结束时,自动释放锁。

例如,下面的代码片段展示了如何使用lock_guard来保证一个共享整数的线程安全访问:

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

std::mutex mtx;
int sharedData = 0;

void increment() {
    std::lock_guard<std::mutex> lock(mtx);
    sharedData++;
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

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

    std::cout << "Final value of sharedData: " << sharedData << std::endl;

    return 0;
}

在上述代码中,两个线程t1和t2同时执行increment函数来增加sharedData的值。由于lock_guard的存在,每个线程在执行increment函数时都会获取互斥量mtx的锁,从而确保了对sharedData的线程安全访问。

需要注意的是,由于lock_guard的析构函数会自动释放锁,因此不建议手动调用unlock函数来释放锁。如果需要手动释放锁,可以使用std::unique_lock来替代lock_guard。

unique_lock

  • unique_lock是C++标准库提供的一种互斥锁管理器。它允许对互斥量进行自动加锁和解锁操作,从而减少了手动管理锁的复杂性和错误。

  • unique_lock提供了更灵活的锁管理功能,与lock_guard相比,它具有更多的特性和控制权。unique_lock可以在创建时选择是否锁定互斥量,也可以在任何时候手动锁定或解锁互斥量。

  • unique_lock的使用方式类似于std::lock_guard,可以通过构造函数或成员函数来锁定互斥量。它还提供了额外的功能,如延迟锁定、尝试锁定和条件变量支持。

  • unique_lock还支持移动语义,即可以将已经锁定的unique_lock对象从一个线程传递给另一个线程。这样可以方便地在函数间传递锁的所有权,避免了手动解锁和重新锁定的麻烦。

  • unique_lock还可以与条件变量一起使用,用于实现复杂的线程同步操作。当条件不满足时,可以释放锁并等待条件满足,一旦条件满足,就可以重新获取锁并继续执行。

总之,unique_lock提供了更灵活、更安全的互斥锁管理功能,可以帮助开发者编写出更可靠、高效的多线程程序。它是C++多线程编程中的重要工具之一。

unique_lock提供了以下几个重要的成员函数:
lock():锁定互斥量。
unlock():解锁互斥量。
try_lock():尝试锁定互斥量,如果互斥量已经被其他线程锁定,则返回falserelease():将unique_lock对象和互斥量的关联解除,返回持有的互斥量,并且unique_lock对象不再管理该互斥量。
reset():将unique_lock对象重新与互斥量关联起来。

unique_lock还提供了以下两个成员函数用于等待条件变量:
wait():等待条件变量满足,并释放互斥量。当满足条件时,wait()重新获取互斥量并返回。
wait_for():等待一段时间内条件变量满足,并释放互斥量。当满足条件或超时时,wait_for()重新获取互斥量并返回。

例如:

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

std::mutex mtx;
int count = 0;

void increment()
{
	// 创建unique_lock对象,并锁定互斥量
    std::unique_lock<std::mutex> lock(mtx); 
    for (int i = 0; i < 100000; ++i)
    {
        count++;
    }
    // lock析构时自动解锁互斥量
} 

int main()
{
    std::thread t1(increment);
    std::thread t2(increment);

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

    std::cout << "count: " << count << std::endl;

    return 0;
}

在上面的例子中,创建了两个线程t1和t2,它们都会调用increment函数来递增变量count的值。increment函数中创建了一个unique_lock对象,并锁定了互斥量mtx。这样就能保证在任意时刻只能有一个线程访问count变量,避免了数据竞争的问题。
在主函数中,我们等待两个线程的完成,并输出count的值。由于互斥量的保护,我们可以确保count的值最终是正确的。

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