C++ 中的并发:互斥和锁 — 第2部分

作者:微信公众号:【架构师老卢】
4-29 9:34
24

概述:锁护罩在前面的示例中,我们直接调用了互斥锁的 and 函数。“在锁下工作”的想法是阻止其他线程对同一资源的不需要的访问。只有获得锁的线程才能解锁互斥锁,并让所有剩余的线程有机会获得锁。然而,在实践中,应不惜一切代价避免直接呼叫!想象一下,在锁下工作时,线程会抛出异常并退出关键部分,而无需调用互斥锁上的解锁函数。在这种情况下,程序很可能会冻结,因为没有其他线程可以再获取互斥锁。这正是我们在上一个示例的函数中看到的。lock()unlock()lock()divideByNumber我们可以通过创建一个对象来避免这个问题,该对象在整个对象生命周期内保持锁定关联的互斥锁。锁在施工时获得,并在销毁时自

锁护罩

在前面的示例中,我们直接调用了互斥锁的 and 函数。“在锁下工作”的想法是阻止其他线程对同一资源的不需要的访问。只有获得锁的线程才能解锁互斥锁,并让所有剩余的线程有机会获得锁。然而,在实践中,应不惜一切代价避免直接呼叫!想象一下,在锁下工作时,线程会抛出异常并退出关键部分,而无需调用互斥锁上的解锁函数。在这种情况下,程序很可能会冻结,因为没有其他线程可以再获取互斥锁。这正是我们在上一个示例的函数中看到的。lock()unlock()lock()divideByNumber

我们可以通过创建一个对象来避免这个问题,该对象在整个对象生命周期内保持锁定关联的互斥锁。锁在施工时获得,并在销毁时自动释放。这使得不可能忘记解锁关键部分。此外,还保证了异常的安全性,因为当抛出异常时,任何关键部分都会自动解锁。在前面的示例中,我们可以简单地将 and 替换为以下代码:std::lock_guardstd::lock_guard_mutex.lock()_mutex.unlock()

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

std::mutex mtx;
double result;

void printResult(int denom)
{
    std::cout << "for denom = " << denom << ", the result is " << result << std::endl;
}

void divideByNumber(double num, double denom)
{
    try
    {
        // divide num by denom but throw an exception if division by zero is attempted
        if (denom != 0) 
        {
            std::lock_guard<std::mutex> lck(mtx);
            
            result = num / denom;
            std::this_thread::sleep_for(std::chrono::milliseconds(1)); 
            printResult(denom);
        }
        else
        {
            throw std::invalid_argument("Exception from thread: Division by zero!");
        }
    }
    catch (const std::invalid_argument &e)
    {
        // notify the user about the exception and return
        std::cout << e.what() << std::endl;
        return; 
    }
}

int main()
{
    // create a number of threads which execute the function "divideByNumber" with varying parameters
    std::vector<std::future<void>> futures;
    for (double i = -5; i <= +5; ++i)
    {
        futures.emplace_back(std::async(std::launch::async, divideByNumber, 50.0, i));
    }

    // wait for the results
    std::for_each(futures.begin(), futures.end(), [](std::future<void> &ftr) {
        ftr.wait();
    });

    return 0;
}

请注意,不再直接调用锁定或解锁互斥锁。我们现在有一个 std::lock_guard 对象,它将互斥锁作为参数并在创建时锁定它。当 divideByNumber 方法退出时,std::lock_guard 对象会在对象被销毁后立即自动解锁互斥锁 - 当局部变量超出范围时,就会发生这种情况。

唯一锁

前面示例的问题在于,我们只能锁定互斥锁一次,控制锁定和解锁的唯一方法是使 std::lock_guard 对象的范围失效。但是,如果我们想要(或需要)对锁定机构进行更精细的控制呢?

unique_lock 是 std::lock_guard 的更灵活的替代方案,它还支持更高级的机制,例如延迟锁定、时间锁定、递归锁定、锁所有权转移和条件变量的使用。它的行为与lock_guard类似,但提供了更大的灵活性,特别是在锁定机构的定时行为方面。

让我们看一下上一节代码的改编版本:

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

std::mutex mtx;
double result;

void printResult(int denom)
{
    std::cout << "for denom = " << denom << ", the result is " << result << std::endl;
}

void divideByNumber(double num, double denom)
{
    std::unique_lock<std::mutex> lck(mtx);
    try
    {
        // divide num by denom but throw an exception if division by zero is attempted
        if (denom != 0) 
        {   
            result = num / denom;
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); 
            printResult(denom);
            lck.unlock();

            // do something outside of the lock
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); 

            lck.lock(); 
            // do someting else under the lock
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); 
        }
        else
        {
            throw std::invalid_argument("Exception from thread: Division by zero!");
        }
    }
    catch (const std::invalid_argument &e)
    {
        // notify the user about the exception and return
        std::cout << e.what() << std::endl;
        return; 
    }
}

int main()
{
    // create a number of threads which execute the function "divideByNumber" with varying parameters
    std::vector<std::future<void>> futures;
    for (double i = -5; i <= +5; ++i)
    {
        futures.emplace_back(std::async(std::launch::async, divideByNumber, 50.0, i));
    }

    // wait for the results
    std::for_each(futures.begin(), futures.end(), [](std::future<void> &ftr) {
        ftr.wait();
    });

    return 0;
}

在此版本的代码中,已替换为 .和以前一样,锁定对象将解锁其析构函数中的互斥锁,即当函数返回并超出范围时。除了这种自动解锁功能外,还提供了额外的灵活性,可以通过手动调用方法和 来根据需要接合和断开锁。此功能可以大大提高并发程序的性能,尤其是当许多线程正在等待访问锁定的资源时。在示例中,在执行某些非关键工作(模拟)之前释放锁,并在关键部分执行其他工作之前重新接合,因此在功能结束时再次处于锁下。这对于在两次访问关键资源之间经过大量时间时优化性能和响应能力特别有用。std::lock_guardstd::unique_locklckdivideByNumberlckstd::unique_locklock()unlock()sleep_for

下面简要总结了使用over的主要优点。使用可以让你...std::unique_lock<>std::lock_guardstd::unique_lock

  1. ...使用默认构造函数构造没有关联互斥锁的实例
  2. ...使用关联的互斥锁构造实例,同时首先使用延迟锁定构造函数使互斥锁保持解锁状态
  3. ...构造一个实例,该实例尝试锁定互斥锁,但如果锁定失败,则使用 try-lock 构造函数将其解锁
  4. ...构造一个实例,该实例尝试在指定时间段内或直到指定时间点获取锁

然而,尽管直接访问互斥锁具有优势,但同时访问两个互斥锁的死锁情况(请参阅本系列的第 2 部分)仍然会发生。

阅读排行