您的位置:

unique_lock和lock_guard详解

一、unique_lock的头文件

unique_lock是C++11中新增的一个互斥量锁的类型,定义在头文件 中。unique_lock最主要的一个作用是实现线程安全的资源管理。unique_lock可以被移动但不能被复制,因为一个unique_lock保存了与某个mutex相关的关键信息。

#include <mutex>
std::unique_lock<std::mutex> lock(mutex);

unique_lock的构造函数接受一个mutex的引用,并尝试使用这个mutex进行加锁。如果mutex已经被其他线程锁定,那么这个线程会被阻塞直到这个锁被释放为止。下面是一个使用unique_lock的简单示例:

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

std::mutex mtx;
std::vector<int> vec;

void pushVec(int n)
{
    std::unique_lock<std::mutex> lock(mtx); // unique_lock加锁
    vec.push_back(n); // 对共享资源进行操作
} 

int main()
{
    std::thread t1(pushVec, 1);
    std::thread t2(pushVec, 2);
    t1.join();
    t2.join();
    for(auto i : vec)
    {
        std::cout << i << " ";
    }
    return 0;
}

上面的示例将两个数1和2分别加入到了std::vector中,并在主线程中通过迭代器遍历输出。

二、unique_lock详解

1、unique_lock的基本使用

除了构造函数之外,unique_lock还提供了一些其他的使用方式。

  1. 可以使用unique_lock的lock()函数手动锁定(或者解锁)mutex。
  2. 当unique_lock的作用域结束时会自动释放mutex。
  3. unique_lock还提供了与条件变量结合使用的功能,可以在wait()函数中自动解锁mutex并等待条件变量的通知。

unique_lock在实现资源管理的方面非常有用。下面是一个使用unique_lock实现同步输出的示例。这个示例中共有10个线程,每个线程循环执行10次,每次输出自己的ID和一个随机数字。我们要求它们一定按照ID递增的顺序依次输出。

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool flag = false;
const int threadCount = 10;

void printID(int id)
{
    for(int i = 0; i < 10; ++i)
    {
        std::unique_lock<std::mutex> lock(mtx);
        while(flag == false || (id == 0 && i == 0))
        {
            cv.wait(lock);
        }
        std::cout << id << " " << rand()%100 << std::endl;
        flag = false;
        cv.notify_all();
    }
} 

int main()
{
    std::vector<std::thread> vec;
    for(int i = 0; i < threadCount; ++i)
    {
        vec.emplace_back(printID, i);
    }
    flag = true;
    cv.notify_all();
    for(auto &t : vec)
    {
        t.join();
    }
    return 0;
}

这个示例中有10个线程,任意一个线程中的操作都不能影响到另一个线程。程序的实现方式是为每个线程定义一个id,然后每个线程循环执行10次,每次输出自己的id和一个随机数。要求每个线程都输出完自己的内容后,主线程输出全部内容。

为了保证线程按照id递增的顺序输出,我们使用了一个flag和一个条件变量cv。flag=true表示线程可以输出,false表示线程要等待其他线程的输出完成后才能输出。当线程输出完自己的内容后,需要唤醒其他等待线程的输出。这里使用了cv.wait(lock)等待条件变量cv的通知,同时unique_lock的构造函数会锁住mutex,确保线程的安全性。

2、unique_lock的高级使用

unique_lock除了上面提到的一些基本使用方法,还有许多高级的操作。例如:

  1. 可以将unique_lock的lock()函数作为可调用对象传递给线程,线程会在运行时执行lock()函数并加锁。
  2. 可以将unique_lock的拷贝/移动构造函数直接传递给线程作为参数,线程会在运行时创建unique_lock实例并加锁。
  3. 可以使用notfy_one()唤醒等待中的线程。
  4. 可以使用try_lock()函数尝试加锁,返回真表示加锁成功,返回假表示加锁失败。

唤醒等待中的线程非常有用,省去了等待时间,可以节省CPU时间。下面是一个使用notfy_one()的示例:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
std::vector<std::thread::id> vec;
bool flag = false;

void addID()
{
    while(true)
    {
        std::unique_lock<std::mutex> lock(mtx);
        if(flag == true)
        {
            std::cout << "Inserting ID: " << std::this_thread::get_id() << std::endl;
            vec.push_back(std::this_thread::get_id());
            flag = false;
            cv.notify_one(); // 唤醒等待中的线程
        }
        else
        {
            cv.wait(lock);
        }
    }
} 

int main()
{
    std::vector<std::thread> tVec;
    for(int i = 0; i < 3; ++i)
    {
        tVec.emplace_back(addID);
    }
    std::this_thread::sleep_for(std::chrono::seconds(1));
    flag = true;
    cv.notify_all();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    flag = true;
    cv.notify_all();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    flag = true;
    cv.notify_all();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    flag = true;
    cv.notify_all();
    for(auto &t : tVec)
    {
        t.join();
    }
    for(auto i : vec)
    {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}

三、lock_guard

1、lock_guard的使用

lock_guard是std::mutex的一个RAII封装。在构造函数中尝试对mutex加锁,在析构函数中自动解锁。由于lock_guard只有一个有参构造函数并且没有拷贝/移动构造函数,因此不能被拷贝和移动。lock_guard的使用方式非常简单:

void func()
{
    std::lock_guard<std::mutex> lock(mutex); // lock_guard加锁
    // 对共享资源进行操作
}

lock_guard在使用中非常容易理解且安全,让我们可以轻松地使用互斥量而不用担心忘记解锁互斥量。

2、lock_guard和unique_lock的区别

lock_guard与unique_lock最大的区别就是在使用方式上。lock_guard的构造函数中只接受一个mutex的引用,并对mutex进行加锁;在lock_guard的析构函数中会对mutex进行解锁。unique_lock的构造函数同样接受mutex的引用,但unique_lock的构造函数可以接受多个参数,比如try_to_lock()表示尝试对mutex进行加锁,如果失败就直接返回;还有一个defer_lock()表示unique_lock不会立即对mutex进行加锁,而是后续再加锁。unique_lock的析构函数会解锁mutex。

总的来说,lock_guard更加简单直接,而unique_lock相对来说更加灵活

3、lock_guard的简单示例

下面是一个使用lock_guard实现同步累加器的示例。这个示例中有10个线程,每个线程循环执行10次,每次加1。最后输出累加器的值。

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

std::mutex mtx;
int sum = 0;

void add()
{
    for(int i = 0; i < 10; ++i)
    {
        std::lock_guard<std::mutex> lock(mtx); // lock_guard加锁
        sum++;
    }
} 

int main()
{
    std::vector<std::thread> tVec;
    for(int i = 0; i < 10; ++i)
    {
        tVec.emplace_back(add);
    }
    for(auto &t : tVec)
    {
        t.join();
    }
    std::cout << sum << std::endl;
    return 0;
}