栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

2022-08-04 C++并发编程(七)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

2022-08-04 C++并发编程(七)

C++并发编程(七)
  • 前言
  • 一、只需在初始化中保护的共享数据
  • 二、对于读多,写极少的共享数据
  • 三、递归加锁
  • 总结


前言

除了锁,C++还有其他方式保护共享数据,比如一些生成后就只读的数据,生成后极少更改的数据。

此外,还有一种可递归的锁,虽然极少使用,甚至不推荐使用。


一、只需在初始化中保护的共享数据

我们设计程序时,可能用到延迟初始化,如下代码:

#include 
#include 
#include 
#include 

struct someResource
{
    void doSomething() const
    {
        std::cout << a << std::endl;
    }

    int a = 0;
};

std::shared_ptr resourcePtr;

void foo()
{
    if (!resourcePtr)
    {
        resourcePtr = std::make_shared();
    }
    resourcePtr->doSomething();
}

auto main() -> int
{}

为了将以上代码用于多线程,一般需要在判断语句前加锁:

std::shared_ptr resourcePtr;

void foo()
{
    std::unique_lock lk(resourceMutex);

    if (!resourcePtr)
    {
        resourcePtr = std::make_shared();
    }

    lk.unlock();

    resourcePtr->doSomething();
}

然而,其后果是由于所有线程都需要竞争锁,导致了多线程逻辑又回到单线程逻辑,除了徒添烦恼,没有什么卵用。

于是为了改进以上代码,实施了会导致 UB 的双检验:

std::shared_ptr resourcePtr;

void undefinedBehaviourWithDoubleCheckedLocking()
{
    if (!resourcePtr)
    {
        std::lock_guard lk(resourceMutex);
        if (!resourcePtr)
        {
            resourcePtr = std::make_shared();
        }
    }
    resourcePtr->doSomething();
}

如果上述代码中,智能指针指向对象的初始化是原子性的,问题不大,但如果分成两步,初始化指针指向的对象,操作指针指向对象,以达到最终可用状态,那么无论过程是否上锁,都会出现指针非空,但对象并未可用状态,然而其他线程已经开始使用此指针指向的对象数据了,这时一种隐蔽很深的逻辑错误。

为了改良设计,我们可以用 std::call_once() 函数进行初始化:

std::once_flag resourceFlag;
std::shared_ptr resourcePtr;

void initResource()
{
    resourcePtr = std::make_shared();
}

void fooInitOnce()
{
    std::call_once(resourceFlag, initResource);
    resourcePtr->doSomething();
}

auto main() -> int
{
    fooInitOnce();
    return 0;
}

同样,可以利用 std::call_once() 做线程安全的类成员数据延迟初始化。

例如取得连接很耗时,也只需一次,则可以在操作类函数,进行收或发数据时进行。

struct X
{
    explicit X(const connectionInfo &connectionDetails_)
        : connectionDetails(connectionDetails_)
    {}

    void sendData(const dataPacket &data)
    {
        std::call_once(connectionInitFlag, &X::openConnection, this);
        connection.sendData(data);
    }

    auto receiveData() -> dataPacket
    {
        std::call_once(connectionInitFlag, &X::openConnection, this);
        return connection.receiveData();
    }

  private:
    void openConnection()
    {
        connection = connectionManager.open(connectionDetails);
    }

    connectionInfo connectionDetails;
    connectionHandle connection;
    std::once_flag connectionInitFlag;
};

对于全局只需唯一单例的设计,C++11后一般用局部静态返回函数完成,不必用 std::call_once()

struct myClass
{
    int a = 0;
};

auto getMyClassInstance() -> myClass &
{
    static myClass instance;
    return instance;
}

局部静态变量,只初始化一次,且一直存在直至程序结束。

二、对于读多,写极少的共享数据

有些数据,一旦写入则极少更改,而且基本是只需要读,C++是否有能够锁写不锁读的锁呢?

有但只在至少C++14以上支持,读写锁或称共享锁 std::shared_mutex,通过 std::shared_lock 模板加锁,其他线程可共享持有 std::shared_lock,但一旦有线程要持有普通锁 std::lock_guard 则会阻塞,直到所有持有共享锁的线程释放。

反之只要一个线程持有普通锁,所有线程不可持有共享锁,直到普通锁释放。

注意,一般互斥变量要用mutable修饰,因为用锁的函数很可能是 const 成员函数。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

struct dnsEntry
{
    int a = 0;
};

struct dnsCache
{
    auto findEntry(const std::string &domain) const -> dnsEntry
    {
        std::shared_lock lk(entryMutex);
        const auto it = entries.find(domain);
        return (it == entries.end()) ? dnsEntry() : it->second;
    }

    void updateOrAddEntry(const std::string &domain, const dnsEntry &dnsDetails)
    {
        std::lock_guard lk(entryMutex);
        entries[domain] = dnsDetails;
    }

  private:
    std::map entries;
    mutable std::shared_mutex entryMutex;
};

auto main() -> int
{
    dnsEntry a;
    dnsCache b;
    b.updateOrAddEntry("test: ", a);
    b.updateOrAddEntry("test2: ", a);
    auto c = b.findEntry("test: ");

    return 0;
}

三、递归加锁

首先,一般互斥变量 std::mutex 不可以重复加锁,但递归互斥变量 std::recursive_mutex 除外。

#include 

std::recursive_mutex m;

auto fn(int b) -> int
{
    std::lock_guard lk(m);
    if (b == 0)
    {
        return 0;
    }
    return b + fn(b - 1);
}

auto main() -> int
{
    int c = fn(5);
    return 0;
}

其次,一般来讲,用到递归锁,说明程序设计可能出了问题,比如一个类中的两个公有函数,两个函数都上锁,其中一个函数调用另一个函数,那就必然重复上锁,一般互斥锁会报错,而暴力的使用递归互斥就可以解决,但这明显不是递归锁的使用场景。

以上情况需要尽量避免。


总结

对于只需初始化一次,后续不变的公有变量,我们 可以通过一次初始化而非一直加锁解决,对于读多写少的数据,可以使用读写锁解决。

对于可能出现的递归加锁,我们有相应的工具,但通常是设计出了问题,需要理清逻辑更改设计,而非简单粗暴的递归加锁。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1033689.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号