如何以自然的语法实现线程安全容器?

2023-12-25

Preface

如果按原样使用,下面的代码会导致未定义的行为:

vector<int> vi;
...
vi.push_back(1);  // thread-1
...
vi.pop(); // thread-2

传统的方法是修复它std::mutex:

std::lock_guard<std::mutex> lock(some_mutex_specifically_for_vi);
vi.push_back(1);

然而,随着代码的增长,这样的事情开始看起来很麻烦,因为每次方法之前都会有一个锁。此外,对于每个对象,我们可能必须维护一个互斥体。

客观的

在不影响访问对象和声明显式互斥体的语法的情况下,我想创建一个模板,以便它完成所有样板工作。例如

Concurrent<vector<int>> vi;  // specific `vi` mutex is auto declared in this wrapper
...
vi.push_back(1); // thread-1: locks `vi` only until `push_back()` is performed
...
vi.pop ()  // thread-2: locks `vi` only until `pop()` is performed

在当前的C++中,这是不可能实现的。但是,我尝试了一个代码,如果只是改变vi. to vi->,那么事情就会按上面代码注释中的预期工作。

Code

// The `Class` member is accessed via `->` instead of `.` operator
// For `const` object, it's assumed only for read purpose; hence no mutex lock
template<class Class,
         class Mutex = std::mutex>
class Concurrent : private Class
{
  public: using Class::Class;

  private: class Safe
           {
             public: Safe (Concurrent* const this_,
                           Mutex& rMutex) :
                     m_This(this_),
                     m_rMutex(rMutex)
                     { m_rMutex.lock(); }
             public: ~Safe () { m_rMutex.unlock(); }

             public: Class* operator-> () { return m_This; }
             public: const Class* operator-> () const { return m_This; }
             public: Class& operator* () { return *m_This; }
             public: const Class& operator* () const { return *m_This; }

             private: Concurrent* const m_This;
             private: Mutex& m_rMutex;
           };

  public: Safe ScopeLocked () { return Safe(this, m_Mutex); }
  public: const Class* Unsafe () const { return this; }

  public: Safe operator-> () { return ScopeLocked(); }
  public: const Class* operator-> () const { return this; }
  public: const Class& operator* () const { return *this; }

  private: Mutex m_Mutex;
};

Demo https://ideone.com/UEhusW

问题

  • 正在使用临时对象来调用重载函数operator->()导致 C++ 中未定义的行为?
  • 这个小实用程序类是否为封装对象提供线程安全的目的在所有情况下?

澄清

对于相互依赖的语句,需要更长的锁定。于是,就有了一个方法介绍:ScopeLocked()。这相当于std::lock_guard()。然而,给定对象的互斥锁是在内部维护的,因此在语法上它仍然更好。
例如而不是以下有缺陷的设计(如答案中所建议的):

if(vi->size() > 0)
  i = vi->front(); // Bad: `vi` can change after `size()` & before `front()`

应该依赖以下设计:

auto viLocked = vi.ScopeLocked();
if(viLocked->size() > 0)
  i = viLocked->front();  // OK; `vi` is locked till the scope of `viLocked`

换句话说,对于相互依赖的陈述,应该使用ScopeLocked().


不要这样做。

创建一个每个方法都带有锁的线程安全集合类几乎是不可能的。

考虑您建议的 Concurrent 类的以下实例。

Concurrent<vector<int>> vi;

开发人员可能会这样做:

 int result = 0;
 if (vi.size() > 0)
 {
     result = vi.at(0);
 }

另一个线程可能会在第一个线程调用之间进行此更改size() and at(0).

vi.clear();

所以现在,同步操作顺序是:

vi.size()  // returns 1
vi.clear() // sets the vector's size back to zero
vi.at(0)   // throws exception since size is zero

因此,即使您有一个线程安全的向量类,两个竞争线程也可能导致在意外的地方抛出异常。

这只是最简单的例子。还有其他一些方式,多个线程尝试同时读/写/迭代可能会无意中破坏线程安全的保证。

您提到整个事情的动机是这种模式很麻烦:

vi_mutex.lock();
vi.push_back(1);
vi_mutex.unlock();

事实上,有一些帮助器类可以使这个更干净,即lock_guard,它将使用互斥锁来锁定其构造函数并在析构函数上解锁

{
    lock_guard<mutex> lck(vi_mutex);
    vi.push_back(1);
}

那么实践中的其他代码就变得线程安全了:

{
     lock_guard<mutex> lck(vi_mutex);
     result = 0;
     if (vi.size() > 0)
     {
         result = vi.at(0);
     }
}

Update:

我编写了一个示例程序,使用您的 Concurrent 类来演示导致问题的竞争条件。这是代码:

Concurrent<list<int>> g_list;

void thread1()
{
    while (true)
    {
        if (g_list->size() > 0)
        {
            int value = g_list->front();
            cout << value << endl;
        }
    }

}

void thread2()
{
    int i = 0;
    while (true)
    {
        if (i % 2)
        {
            g_list->push_back(i);
        }
        else
        {
            g_list->clear();
        }
        i++;
    }
}

int main()
{

    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join(); // run forever

    return 0;
}

在非优化的构建中,上面的程序会在几秒钟内崩溃。 (零售有点困难,但错误仍然存​​在)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何以自然的语法实现线程安全容器? 的相关文章

  • C# 中的 Culture 相当于 Java 中的 Locale 吗?

    C 使用文化的概念 这在操作上与 Java 中的 Locale 类似吗 或者底层概念是否存在显着差异 从文化而不是语言环境的角度进行工作是一种寻找正确抽象层次的尝试 从以类似方式做事的人群的角度来考虑事物 而不是谈论地理区域和语言 并有点疯
  • 如何在C编程中获取当前时间(以毫秒为单位)[重复]

    这个问题在这里已经有答案了 可能的重复 如何使用 ANSI C 测量以毫秒为单位的时间 https stackoverflow com questions 361363 how to measure time in milliseconds
  • C 中的分段错误

    我需要用 0 填充二维数组 但编译后的程序会出现此错误 怎么了 int main int vert 1001 1001 int hor 1001 1001 int dudiag 1416 1416 int uddiag 1416 1416
  • 使用 QTextCursor 选择一段文本

    使用 Qt 框架选择文本片段时遇到问题 例如 如果我有这个文件 没有时间休息 我想选择 ime for r 并从文档中删除这段文本 我应该如何使用 QTextCursor 来做到这一点 这是我的代码 QTextCursor cursor n
  • 在 WPF 中的不同控件上显示验证错误模板

    我有一个UserControl包含其他控件和TextBox 它有一个Value绑定到的属性TextBox文本并有ValidatesOnDataErrors设置为 True 当验证错误发生时Value属性绑定 错误模板 标准红色边框 显示在整
  • C++17 中带有 noexcept 的 std::function

    在 C 17 中noexcept 已添加到类型系统中 http www open std org jtc1 sc22 wg21 docs papers 2015 p0012r1 html void r1 void f noexcept f
  • F# 内联如何工作?

    对于 F 我的理解是您可以使用 inline 关键字在调用站点执行类型专门化 那是 val inline a gt b gt c when a or b static member a b gt c 约束条件是 a or b必须有一个静态成
  • 按位非运算符

    为什么要按位运算 0 打印 1 在二进制中 不是0应该是1 为什么 你实际上很接近 在二进制中 不是0应该是1 是的 当我们谈论一位时 这是绝对正确的 然而 一个int其值为0的实际上是32位全零 将所有 32 个 0 反转为 32 个 1
  • 了解 MVC-5 身份

    我创建了一个新的ASP NET MVC 5申请与Individual User Accounts然后更新了所有的Nuget packages在解决方案中 现在我尝试遵循一些教程中显示的一些指南 但遇到了一些问题 第一个是一个名为Applic
  • 让 GCC/Clang 使用 CMOV

    我有一个简单的标记值联合 这些值可以是int64 ts or doubles 我正在对这些联合进行加法 但需要注意的是 如果两个参数都代表int64 t值 那么结果也应该有一个int64 t value 这是代码 include
  • 使用 MapViewOfFile 有什么限制吗?

    我正在尝试将内存映射文件用作 hFile CreateFile State Path GENERIC READ FILE SHARE READ FILE SHARE WRITE 0 OPEN EXISTING FILE FLAG SEQUE
  • 在 Asp.Net 模板中转义内联代码块

    我有一个页面 我希望在其中呈现以下 html 一个小的 JS 模板 然而 Asp NET 预处理器正在拾取 我已经设法通过文字控件来做到这一点 并在后面的代码中设置它的文本 我理想地希望将其保留在 aspx 页面中 这是我能找到的最好的解决
  • DLL 中的 XP 风格组合框

    我需要使用 C 和 WIN32 API 无 MFC 在 DLL 中创建 XP 风格的组合框 我设法在 DLL 中创建控件 不是以 XP 风格 我设法在带有清单的 exe 中创建 XP 样式组合框 但它在 DLL 中不起作用 为了让您的 DL
  • C语言中如何将整数转换为字符? [复制]

    这个问题在这里已经有答案了 C语言中如何将整数转换为字符 C 中的字符已经是数字 字符的 ASCII 代码 无需转换 如果要将数字转换为相应的字符 只需添加 0 即可 c i 0 0 是 ASCii 表中的一个字符
  • 如何最好地为 Visual Studio 2017 构建的 CMake C++ 项目设置输出目录?

    我使用 Visual Studio 2017 使用 vcxproj 文件构建 C 桌面项目 我喜欢默认行为 其中输出目录是项目下面的子目录 例如 myproj sln myproj vcxproj x64 myproj release my
  • C# 编译器编译 .txt .obj .java 文件

    using System class Program public static void Main Console WriteLine Hello World Console ReadLine 我将文件另存为1 java 2 obj an
  • 使用std::begin()、std::end()将ArrayXd转换为stl向量,

    在我看来我应该能够使用std begin and std end 转换ArrayXd to std vector
  • 序列化时如何跳过 xml 声明?

    我正在尝试输出一个没有 xml 头的 xml 文件 例如 我试过 Type t obj GetType XmlSerializer xs new XmlSerializer t XmlWriter xw XmlWriter Create c
  • 散列 hash_hmac 时,Convert.ToChar(0) 散列结果与 PHP 中的 chr(0) 不同的字符串

    我在 PHP 中有一个字符串 它被转换为字节数组并进行哈希处理 转换为字节数组的字符串如下所示 G 字符 0 便便 我需要 C 中的等效字节数组 这样我才能得到相同的哈希值 编辑 这是完整的问题 生成的哈希值不同 PHP api secre
  • “保留供任何使用”是什么意思?

    注意 这是一个c questions tagged c问题 虽然我补充说c questions tagged c 2b 2b如果某些 C 专家可以提供 C 使用与 C 不同的措辞的基本原理或历史原因 在 C 标准库规范中 我们有这个规范文本

随机推荐