并发问题:如何只有一个线程通过关键部分

Concurrency Problem: how to have only one thread go through critical section

本文关键字:键部 线程 有一个 问题 并发      更新时间:2023-10-16

我想要一个多线程函数,为对象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;
}

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

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

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

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

如何只有一个线程通过关键部分

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

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

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

但当许多线程试图获得锁定时运行缓慢

这个问题是串行访问同一数据结构所固有的。提高性能的一种方法是从一开始就避免这样做。

在这个特定的例子中,您可以在进程仍然是单线程的时候简单地初始化资源,并且只有在初始化完成后才启动并行线程。这样,访问指针就不需要锁定。

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

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


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

由于您无法轻松修复它,因为它是并发的固有特性,因此我有一个想法,可以通过来大幅提高或降低性能。

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

从@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;
}