为什么程序只能在调试模式下工作?

时间:2020-04-01 17:43:01

标签: c++ multithreading

我试图了解无锁编程,并编写了无锁堆栈:

template <typename T>
class LockFreeStack
{
    struct Node
    {
        std::shared_ptr<T> data;
        Node* next;

        explicit Node(const T& _data)
            : data(std::make_shared<T>(_data)), next(nullptr)
        {}
    };
    std::atomic<Node*> head;

public:
    void push(const T& data)
    {
        auto n{new Node(data)};
        n->next = head.load();
        while (!head.compare_exchange_weak(n->next, n))
            ;
    }

    std::shared_ptr<T> pop(void)
    {
        auto old_head{head.load()};
        while (old_head && head.compare_exchange_weak(old_head, old_head->next))
            ;
        return old_head ? old_head->data : std::shared_ptr<T>{};
    }
};

和两个用于推/弹出操作的线程:

static LockFreeStack<int> global_stack;

main函数:

int main(void)
{
    std::srand(std::time(nullptr));

    std::thread pushing_thread([](void) {
        for (size_t i{}; i < MAX_LENGTH; ++i)
        {
            const auto v{std::rand() % 10000};
            global_stack.push(v);

            std::cout << "\e[41mPoping: " << v << "\e[m" << std::endl;
        }
    });
    std::thread poping_thread([](void) {
        for (size_t i{}; i < MAX_LENGTH; ++i)
        {
            if (auto v{global_stack.pop()}; v)
            {
                std::cout << "\e[42mPushing: " << *v << "\e[m" << std::endl;
            }
        }
    });

    pushing_thread.join();
    poping_thread.join();
}

此程序仅在调试模式下运行pushing_thread,但是当我使用调试器运行该程序时,它将按预期运行两个线程,或者如果我在两个线程之间稍等片刻:

std::thread pushing_thread(...);
std::this_thread::sleep_for(1s);
std::thread poping_thread(...);

它正常工作。那么,当我们使用调试器运行程序时会发生什么?

  • 编译器:GCC 9.3
  • 编译器标志:-std=c++2a -lpthread -Wall
  • 操作系统:ArchLinux with linux-5.5.13

2 个答案:

答案 0 :(得分:1)

这种逻辑有缺陷:

    while (old_head && head.compare_exchange_weak(old_head, old_head->next))
        ;

如果old_head不为null,并且交换的文件有效,则重试!

答案 1 :(得分:1)

您的实现遇到了所谓的ABA问题。请考虑以下情形:

  • 堆栈看起来像这样:A->B->C(即,头指向A)
  • 线程1加载头(即A的地址)和A的下一个指针(B),但是在执行CAS之前,它被中断了。
  • 线程2先弹出A,然后弹出B,然后再推A,即堆栈现在看起来像这样:A->C
  • 线程1恢复并愉快地将头从A更新为B->您的堆栈已损坏-看起来像这样:B->C-但是B当前在由线程2使用。

有几种可能的方法可以避免ABA问题,例如标记指针或并发内存回收方案。

更新
带标记的指针只是一个用版本计数器扩展的指针,每次更新该指针时,版本标记都会递增。您可以使用DWCAS(Double-Width-CAS)用单独的版本字段更新结构,也可以将版本标签压缩到指针的高位。并非所有的体系结构都提供DWCAS指令(x86可以),并且如果未使用高位,则取决于操作系统(在Windows和Linux上,通常可以使用最高的16位)。

关于内存回收方案的主题,我可以向您介绍我的论文:Effective memory reclamation for lock-free data structures in C++