【问题标题】:Concurrency Problem: how to have only one thread go through critical section并发问题:如何只有一个线程通过临界区
【发布时间】:2019-10-28 19:50:20
【问题描述】:

我想要一个多线程函数,它为对象obj 分配一些内存并返回分配的内存。我目前的单线程和多线程版本代码如下。

多线程版本没有竞争条件,但是当很多线程试图获取锁时运行缓慢。在malloc 和指针更新之后,每个线程仍然需要获取和释放相同的锁。这会导致一些多线程性能下降。我想知道是否还有其他方法可以提高性能。

struct multi_level_tree{
    multi_level_tree* ptr[256];
    mutex mtx;
};

multi_level_tree tree; // A global object that every thread need to access and update

/* Single Threaded */
multi_level_tree* get_ptr(multi_level_tree* cur, int idx) {
    if (!cur[idx].ptr) 
        cur[idx].ptr = malloc(sizeof(T));
    return cur[idx].ptr;
}

/* Multi Threaded with mutex */
void get_ptr(multi_level_tree* cur, int idx) {
    if (!cur[idx].ptr) {
        cur[idx].mtx.lock(); // other threads wait here, and go one by one
        /* Critical Section Start */
        if (!cur[idx].ptr)
            cur[idx].ptr = malloc(sizeof(multi_level_tree)); // malloc takes a while
        /* Critical Section End */
        cur[idx].mtx.unlock();
    }
    return cur[idx].ptr;
}

我正在寻找的代码应该具有以下属性。

  • 当第一个线程分配内存时,它应该提醒所有其他等待它的线程。
  • 应同时解除所有其他线程的阻塞。
  • 没有竞争条件。

问题中的挑战 * 树是稀疏的,有多个级别,考虑到我们拥有的内存,初始化所有这些是不可能的 * 类似于Double-Checked Locking 的问题,但试图避免std::atomic

这段代码的重点是将多级数组实现为全局变量。除了最低级别之外,每个数组都是指向下一级数组的指针列表。由于这个数据结构需要动态增长,所以我遇到了这个问题。

【问题讨论】:

  • 为什么所有线程都必须使用相同的资源来分配内存?他们不能各自独立工作吗?
  • 这就是我要问的重点。资源obj 实际上是一个全局多级数组。所有线程都想访问和更新数据结构。 @原名_463035818
  • 不确定,但我认为您的多线程并不是真正的线程安全,因为在锁定互斥锁之前访问了!o.ptr
  • 我没有看到大局,所以我只能问问题......你知道malloc只是分配内存,而不是创建对象吗?

标签: c++ multithreading


【解决方案1】:

如何让一个线程通过临界区

您可以使用互斥锁。您的问题中有一个示例。

这不是同步初始化的最佳解决方案。一个简单的改进是使用本地静态,在这种情况下编译器负责实现同步:

T& get_T() {
    static T instance;
    return instance;
}

但是当很多线程试图获取锁时运行缓慢

此问题是序列化对同一数据结构的访问所固有的。提高性能的一种方法是首先避免这样做。

在这个特定示例中,您似乎可以在进程仍为单线程时简单地初始化资源,并仅在初始化完成后启动并行线程。这样就不需要锁定来访问指针。

如果这不是一个选项,另一种方法是在每个线程中简单地调用一次get_ptr,并在本地存储一个副本。这样,锁定开销仍然很小。

更好的是在每个线程中都有单独的数据结构。这在线程只产生数据而不需要访问其他线程的结果时很有用。


关于已编辑的示例:您可能会从无锁树实现中受益。然而,它可能难以实施。

【讨论】:

  • 我发现单例模型确实如此。但是,它不能轻易适应我的问题,因为我是一个树结构,每个级别都包含更多节点到下一个级别。我需要为每个树节点创建一个动态数组。
  • 是的。但我正在实现一个 8 级树。构造函数应该只用 nullptr 初始化根节点。
  • @BruceShen 为什么不立即初始化整个树?
  • 每个节点有 256 个子节点的 8 级树将采用底层的所有 64 位地址。尝试创建提供每个地址元数据的影子内存。
【解决方案2】:

由于您无法轻松修复它,因为它是并发性的固有特性,我有一个想法可以通过。

如果这个资源真的经常被使用并且是有害的,你可以尝试使用 Active Object (https://en.wikipedia.org/wiki/Active_object) 和 Boost Lockfree Queue (https://www.boost.org/doc/libs/1_66_0/doc/html/lockfree/reference.html#header.boost.lockfree.queue_hpp)。在 Future 对象上使用原子存储/加载,您将使此过程完全无锁。但另一方面,它将需要一个线程来维护。这种解决方案的性能在很大程度上取决于该资源的使用频率。

【讨论】:

  • 真的很感激。我想我想到了使用原子加载和存储以及atomic::compare_exchange。我将在另一个答案中发布以进行校对。
【解决方案3】:

从@WilliamClements 的评论表单中,我看到这本身就是一个双重检查锁定问题。我问题中的原始多线程代码可能会损坏。为了正确编程,我切换到原子指针以防止加载/存储指令出现排序问题。

但是,该示例仍然使用了我想摆脱的锁。因此,我选择使用std::atomic::compare_exchange_weak 仅在指针值为nullptr 时更新指针。这样只有一个线程会成功更新指针值,其他线程失败会释放申请的内存std::atomic::compare_exchange_weak

到目前为止,这段代码对我来说做得很好。

struct multi_level_tree{
    std::atomic<multi_level_tree*> ptr;
};

multi_level_tree tree; 

void get_ptr(multi_level_tree* cur, int idx) {
    if (!cur[idx].ptr.load()) { 
        /* Critical Section Start */
        if (!cur[idx].ptr.load()) {
            node* tmp = malloc(sizeof(multi_level_tree)*256);
            if (cur[idx].ptr.compare_exchange_weak(nullptr, tmp)) {
                /* successfully updated, do nothing */
            }
            else {
                /* Already updated by other threads, release */
                free(tmp);
            }
        }
        /* Critical Section End */
    }
    return cur[idx].ptr;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-09-09
    • 2020-01-29
    • 2010-10-17
    • 2022-11-17
    • 2016-10-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多