线程安全的 C++ 堆栈

Posted

技术标签:

【中文标题】线程安全的 C++ 堆栈【英文标题】:Thread-safe C++ stack 【发布时间】:2010-10-21 21:23:48 【问题描述】:

我是 C++ 新手,正在编写一个多线程应用程序,通过该应用程序不同的编写者将对象推送到堆栈上,而读者将它们从堆栈中拉出(或至少将指针推送到对象)..

是否有任何内置于 C++ 中的结构可以在不添加锁定代码等的情况下处理此问题?如果没有,那么 Boost 库呢?

编辑:

嗨。感谢您最初的精彩回答。我想我认为这可能是内置的一个原因是我纯粹在 x86 空间中思考,并认为指针的 PUSH/POP 应该是指令级别的原子操作。

我不确定我最初的预感是否正确,但我想这不一定适用于所有平台。虽然如果在 x86 上运行,您是否将原子 PUSH 和 POP 放入堆栈,如果是,这是否实质上使其无锁?

【问题讨论】:

如果您对 x86 PUSH/POP 指令的原子性感兴趣,请提出一个单独的问题 - 它与 C++ 无关,C++ 不会使用此类指令访问堆栈数据结构。 委员会更忙于编写关于 DDJ 的并行讲道课程,而不是在 TR1 中强制编译器进行原子和更好的内存模型抽象(甚至可能在 TR2 中都没有)。回答:您并没有真正推送和弹出,因此隐式修改跨线程的寄存器说当前在不同的内核上运行是吗? :-) 不错的镜头,但不起作用.. 你不能无锁或至少没有 CAS 锤子。对于 C++ ***者:他们应该坐下来定义并就现有的一致性协议达成一致,+ 为新的开发留出一些空间.. 对于那些感兴趣的人,我研究了原子操作,英特尔通过 cmpxchg16b 提供了 DCAS 支持。不幸的是,AMD 只有 cmpxchg8b。对我来说没关系,因为我正在为英特尔机器写作 :) 这不是 DCAS 它是一个广泛的 CAS - 有很大的不同 如果您使用的是 C++11 或更新版本,below 是来自 @Reunanen 的优秀 example 的现代化示例。 【参考方案1】:

是的:Boost.Thread 很棒,应该非常适合您的需求。 (现在,很多人说您几乎可以将 Boost 视为内置功能。)

仍然没有可以开箱即用的类,但是一旦掌握了同步原语,就可以非常简单地实现自己的线程安全包装器,例如@987654324 @。它可能看起来像这样(不是实现每个方法...):

template <typename T> class MyThreadSafeStack 
  public:
    void push(const T& item) 
      boost::mutex::scoped_lock lock(m_mutex);
      m_stack.push(item);
    
    void pop() 
      boost::mutex::scoped_lock lock(m_mutex);
      m_stack.pop();
    
    T top() const  // note that we shouldn't return a reference,
                    // because another thread might pop() this
                    // object in the meanwhile
      boost::mutex::scoped_lock lock(m_mutex);
      return m_stack.top();
    

  private:
    mutable boost::mutex m_mutex;
    std::stack<T> m_stack;
    

如果您是 C++ 新手,请了解 RAII。与这种情况相关的是,Boost.Thread 具有“作用域锁”类,以防止忘记释放锁而使自己陷入困境。

如果你发现自己在编写这样的代码:

void doStuff() 
  myLock.lock();
  if (!condition) 
    reportError();
    myLock.unlock();
    return;
  
  try 
    doStuffThatMayThrow();
  
  catch (std::exception& e) 
    myLock.unlock();
    throw e;
  
  doMoreStuff();
  myLock.unlock();

,那么你应该直接说不,转而使用 RAII(语法不是直接来自 Boost):

void doStuff() 
  scoped_lock lock;
  if (!condition) 
    reportError();
    return;
  
  doStuffThatMayThrow();
  doMoreStuff();

关键是当scoped_lock 对象超出范围时,它的析构函数会释放资源——在本例中是锁。无论您是通过抛出异常退出范围,还是通过执行您的同事在您的函数中间偷偷添加的奇怪的return 语句,或者只是通过到达函数的末尾,这总是会发生。

【讨论】:

您应该注意,即使被互斥锁锁定,std 容器也不是线程安全的。原因是它们的修改使现有的迭代器无效。 @ASk - 如果您正在迭代共享 STL 容器(或任何共享容器),那么您可能也应该锁定。例如,如果您要迭代地对容器的内容执行只读操作,您可以获得一个读锁,以便在该操作期间没有人可以通过写操作使您的迭代器无效。此外,您需要获得写锁才能对该结构进行更改,以强制等待当前读取(未完成的迭代器)完成。 @maciekm:当然你可以,但问题是你需要确定你总是调用 LeaveCriticalSection - 即使抛出异常。所以在这方面,它并不比在示例中调用 myLock.lock() 和 myLock.unlock() 更好。正如我所写:请了解 RAII。 @David 是的,你是绝对正确的。这只是一个示例(将标准容器包装在线程安全类中),而不是一个可用于生产的功能齐全的实现。 迟到总比没有好:我添加了一个示例,将这个出色的答案重写为 C++11 或更新的 here。【参考方案2】:

当前的 C++ 标准根本不涉及线程,因此您的第一个问题的答案是否定的。通常,将锁定构建到基本数据结构中是一个坏主意,因为它们没有足够的信息来正确和/或有效地执行它。相反,锁定应该在使用数据结构的类中执行 - 换句话说,在您自己的应用程序类中。

【讨论】:

我不得不有点不同意(尽管不是反对票)。我认为编写一个包装私有 std::stack 实例的类 ThreadSafeStack 是完全可以的。然后在每个方法(push、pop、...)的开头输入一个关键部分或类似的部分。确实 top() 不应该再返回一个引用,而是一个副本。这会导致效率下降,但在我看来,编写应用程序类的额外便利通常是值得的。在不使用的情况下(例如,复制巨大的对象),那么你就不要使用包装类。 是的,这就是我的意思,但可能表达得很糟糕 - ThreadSafeStack 是您应用程序的类之一。但是通用库不应该(恕我直言)提供这样的类。 啊,好的。我认为应用程序类主要包含业务逻辑。【参考方案3】:

AFAIK,在 C++ 中没有内置支持。您必须使用简单的同步工具来同步堆栈操作。如果线程属于同一个进程,CriticalSection 会这样做,否则就使用 Mutex。

【讨论】:

【参考方案4】:

在 C++ 和 Boost 库中都没有内置机制来支持这一点(注意:有些人写过 thread-safe stacks/etc. in the Boost style)。您将不得不借用一些代码或在自己的同步中做饭。

请注意,您的情况可能需要一个单一写入器多读取器保护 (SWMRG),其中多个写入器线程可以访问堆栈(但在给定时间点只有一个)并且多个读取器可以访问堆栈(许多在给定的时间点)。里希特有reference implementation。

【讨论】:

【参考方案5】:

如果不想使用锁定,则需要使用无锁堆栈。这实际上并不难(无锁队列更难)。您确实需要一个特定于平台的比较交换原语,例如 Windows 上的 InterlockedCompareExchange,但这并不难抽象。

请参阅此处以获取 C# 中的示例:

http://www.boyet.com/Articles/LockFreeRedux.html

【讨论】:

【参考方案6】:

C++ 11 标准引入了内存模型以及线程安全编程的标准工具。

Reuanen 的 excellent example 可以用这些工具重写,它们的使用与 boost 示例中的非常相似。

您需要这些标题:

#include <mutex> // defines mutexes and locks
#include <stack>

然后你可以创建你的线程安全堆栈:

template <typename T> class MyThreadSafeStack 
  public:
    void push(const T& item) 
      std::lock_guard<std::mutex> lock(m_mutex);
      m_stack.push(item);
    
    void pop() 
      std::lock_guard<std::mutex> lock(m_mutex);
      m_stack.pop();
    
    T top() const  // note that we shouldn't return a reference,
                    // because another thread might pop() this
                    // object in the meanwhile
      std::lock_guard<std::mutex> lock(m_mutex);
      return m_stack.top();
    

  private:
    mutable std::mutex m_mutex;
    std::stack<T> m_stack;
;    

当然,您也可以使用具有原子操作的非锁定结构,但是,这更复杂。当人们感兴趣时,我可以添加一个示例。

【讨论】:

【参考方案7】:

如果您在 Windows 上运行,SLIST 会实现无锁堆栈(结构为 SLIST_HEADER &amp; SLIST_ENTRY)。

该算法是通过使用互锁函数的相当简单的 push/pop 单链表堆栈实现的。唯一不明显的项目是计数器增量以避免 ABA 问题。

【讨论】:

谢谢。我希望这对其他人有用。我在 Linux 上运行并使用了上面的作用域锁解决方案。最后,我把锁定代码放在代码而不是结构中。

以上是关于线程安全的 C++ 堆栈的主要内容,如果未能解决你的问题,请参考以下文章

java 此类表示线程安全堆栈并使用CAS指令来确保线程安全。

为啥局部变量在 Java 中是线程安全的

c++ string线程安全吗

可变关键字线程安全[重复]

C++ 是不是有任何线程安全可以写入(比没有锁的线程安全类似物更快)组件?

如何安全地终止线程? (使用指针)C++