无法理解或修复我的程序中的竞争条件

Posted

技术标签:

【中文标题】无法理解或修复我的程序中的竞争条件【英文标题】:Unable to understand or fix race condition in my program 【发布时间】:2019-02-06 18:00:57 【问题描述】:

我正在实现我自己的 malloc 版本,它与 glibc malloc 非常相似,因为它通过创建 arenas 来支持多线程,这是一个线程可以在没有与另一个线程竞争的风险的情况下工作的内存区域.

我的数据结构如下:

typedef struct          s_arena 
    pthread_mutex_t     mutex;
    t_pool              *main_pool;
                       t_arena;

typedef struct          s_arena_data 
    _Atomic int         arena_count;
    t_arena             arenas[M_ARENA_MAX];
                       t_arena_data;

t_arena_data 是一个全局变量,其中包含已创建的竞技场数量,第一次调用从 0 开始,上限为 M_ARENA_MAX(我目前定义为 8),以及一个包含我所有竞技场的数组。

一个 arena 只包含一个互斥锁,它使用 pthread_mutex_init() 初始化,以及一个指向内存池的指针。内存池对于这个主题并不重要,因为竞争条件在到达它之前就发生了。

我的程序是如何工作的:当每个线程进入 malloc 时,它会尝试 pthread_try_lock 第一个 arena 的互斥锁。如果是这样,一切都很好,它会继续进行我在这里没有描述的分配。如果没有,可能会发生几件事。

如果数组中的下一个条目为空且未达到 M_ARENA_MAX,则将锁定一个新的互斥体以创建新的 arena 并将其添加到数组中。互斥锁是全局的,这意味着没有两个线程可以同时创建一个 arena。

如果该互斥体被锁定,则线程将循环回到 arena[0] 并继续搜索打开的互斥体。

现在,我很确定由于变量 arena_count 会发生竞态条件。多亏了调试 printf 语句,我观察到无论何时函数段错误,都没有达到 M_ARENA_MAX。如果有,程序将不会崩溃。所以我怀疑一个线程可能在另一个线程增加它之前读取 arena_count 的值,当它完成读取它时,增加它的线程释放 new_arena_mutex 并且第一个线程开始创建一个使用索引错误。

这是我的第一个多线程程序,所以如果我的解释或代码不清楚,我深表歉意,但我在过去 4 个小时里一直在解决这个问题,虽然我认为我缩小了问题的范围,但我真的没有知道怎么解决。

这里是错误的代码部分:

    current_arena = &arena_data.arenas[0];
    int arena_index = 0;
    while (pthread_mutex_trylock(&current_arena->mutex) != 0) 

        printf("THREAD %p READS[0] ARENA COUNT AT %d\n", (void *)pthread_self(), arena_data.arena_count);
        if (arena_index == arena_data.arena_count - 1) 
            printf("THREAD %p READS[1] ARENA COUNT AT %d\n", (void *)pthread_self(), arena_data.arena_count);

            if (pthread_mutex_trylock(&new_arena_mutex) != 0 || arena_data.arena_count == M_ARENA_MAX) 
                current_arena = &arena_data.arenas[(arena_index = 0)];
                continue;
            

            creator = true;
            break;
        

        current_arena = &arena_data.arenas[arena_index++];
    

    /* All arenas are occupied by other threads but M_ARENA_MAX isn't reached. Let's just create a new one. */
    if (creator == true) 

        printf("THREAD %p READS[2] ARENA COUNT AT %d\n", (void *)pthread_self(), arena_data.arena_count);

        current_pool = create_new_pool(MAIN_POOL, chunk_type, size, pagesize, &new_arena_mutex);
        if (current_pool == MAP_FAILED) return NULL;

        ++arena_data.arena_count;
        arena_data.arenas[arena_index + 1] = (t_arena) .main_pool = current_pool ;
        pthread_mutex_init(&arena_data.arenas[arena_index + 1].mutex, NULL);
        pthread_mutex_lock(&arena_data.arenas[arena_index + 1].mutex);
        pthread_mutex_unlock(&new_arena_mutex);

        return user_area((t_alloc_chunk *)current_pool->chunk, size, &arena_data.arenas[arena_index + 1].mutex);
    

这是 printf 语句之一,它安慰了我的理论,即存在竞争条件:

THREAD 0x7f9c3b216700 READS[1] ARENA COUNT AT 4
THREAD 0x7f9c3b216700 READS[2] ARENA COUNT AT 5

值应该相等,但事实并非如此。

【问题讨论】:

好吧,一个最小的示例可能会有所帮助,因为这对我来说是一个相当模糊的问题;尽管如此,良好的锁处理通常是通过让每个锁清楚地识别它保护的数据来完成的,在你的情况下,我希望锁能够保护 t_arena ,仅此而已,但它似乎以某种方式保护计数,即对我来说很可疑...... 你建议如何做一个例子?代码比这多得多,所以我尽量减少它。 【参考方案1】:

我可以在您的代码中发现三个问题。

1。两个线程创建 arenas 时的竞争条件

这是您在问题中描述的竞争条件:

所以我怀疑一个线程可能在另一个线程递增它之前读取 arena_count 的值,当它完成读取它时,递增它的线程释放 new_arena_mutex 并且第一个线程开始创建一个索引错误的竞技场。

是的,这可能会发生。 arena_data.arena_countload 以原子方式发生,但线程通常可能不会假设该值(仍然)正确。修改后的版本in your answer 确实没有解决问题。

为了解决这个问题,以下保证可能会有所帮助:arena_data.arena_count 的任何 存储 都会在持有 new_arena_mutex 时发生。结果,持有互斥锁的线程可以安全地加载arena_data.arena_count(当然,在持有互斥锁的同时),并且可以确保在解锁互斥锁之前它的值不会改变。让我尝试通过更改和评论您更新的代码来解释:

  while (pthread_mutex_trylock(&current_arena->mutex) != 0) 

    if (arena_index == arena_data.arena_count - 1) 
// This thread checks the condition above _without_ holding the
// `new_arena_mutex`. Another thread may hold the mutex (and hence it
// may increment `arena_count`).

      if (pthread_mutex_trylock(&new_arena_mutex) == 0) 
// Now, this thread can assume that no other thread writes to
// `arena_data.arena_count`. However, the condition
//
//     arena_index == arena_data.arena_count - 1
//
// may no longer be true (because it had been checked before locking).

    if (arena_data.arena_count < M_ARENA_MAX) 
// This thread may create a new arena at index
// `arena_data.arena_count`. That is safe because this thread holds
// the `new_arena_mutex` (preventing other threads from modifying
// `arena_count`.
//
// However, it is possible that `arena_index` is not at the position
// of the most recently created arena (checked _before_ locking). Let
// us just assume that all the recently created arenas are still
// locked. Hence we just skip the check and directly jump to the most
// recently created arena (as if we failed locking).
      arena_index = arena_data.arena_count - 1;
      current_arena = &arena_data.arenas[arena_index];
      ++arena_data.arena_count;
      assert(
        arena_index + 1 == arena_data.arena_count &&
        "... and this thread is holding the mutex, so it stays true."
      );
      creator = true;
      break;
     else 
      pthread_mutex_unlock(&new_arena_mutex);
    

在我看来,如果您将这些操作提取到诸如

之类的函数中,代码会变得更具可读性
// both functions return `arena_index` or `-1`
int try_find_and_lock_arena();
int try_create_and_lock_arena();

2。可疑(错误?)后增量运算符

以下行中的后增量运算符在我看来是错误的:

current_arena = &arena_data.arenas[arena_index++];// post-increment
// now, `&arena_data.arenas[arena_index]` is one beyond `current_arena`.

写成两行,可能更容易推理行为:

assert(
  current_arena == &arena_data.arenas[arena_index] &&
  "this is an invariant I expect to hold"
);

current_arena = &arena_data.arenas[arena_index];// this is a no-op...
arena_index++;// ... and now, they are out of sync

assert(
  current_arena == &arena_data.arenas[arena_index] &&
  "now, the invariant is broken (and this assert should fire)"
);

3。互斥锁/解锁对的可读性

我发现很难为所有可能的路径匹配互斥锁的锁定/解锁操作,因为它们发生在不同的范围内。

    // [new_arena_mutex is locked]

    current_pool = create_new_pool(/* ... */, &new_arena_mutex);

    if (current_pool == MAP_FAILED) return NULL;// error-path return
    // `create_new_pool` unlocks iff it returns `MAP_FAILED`...

    /* ... */

    pthread_mutex_unlock(&new_arena_mutex);
    // ... otherwise, the mutex is unlocked here

    return user_area(/* ... */);

【讨论】:

是的,create_new_pool 调用 mmap。在从 mmap 返回 MAP_FAILED 的情况下,将在互斥锁上调用 pthread_mutex_unlock,就在将 MAP_FAILED 返回到 malloc 之前。 您的版本确实更有意义。关于 mutex 版本,您是在谈论可读性吗? @JonNimrod:阅读您对create_new_pool 行为的评论后:是的,互斥锁讨论只是关于可读性。【参考方案2】:

(编辑):它没有。

这似乎解决了问题:

    /* Look for an open arena. */
    current_arena = &arena_data.arenas[0];
    int arena_index = 0;
    while (pthread_mutex_trylock(&current_arena->mutex) != 0) 

        if (arena_index == arena_data.arena_count - 1) 

            if (pthread_mutex_trylock(&new_arena_mutex) == 0) 

                if (arena_data.arena_count < M_ARENA_MAX) 
                    ++arena_data.arena_count;
                    creator = true;
                    break;
                 else 
                    pthread_mutex_unlock(&new_arena_mutex);
                
            

            current_arena = &arena_data.arenas[(arena_index = 0)];
            continue;
        

        current_arena = &arena_data.arenas[arena_index++];
    

    /* All arenas are occupied by other threads but M_ARENA_MAX isn't reached. Let's just create a new one. */
    if (creator == true) 

        current_pool = create_new_pool(MAIN_POOL, chunk_type, size, pagesize, &new_arena_mutex);
        if (current_pool == MAP_FAILED) return NULL;

        arena_data.arenas[arena_index + 1] = (t_arena) .main_pool = current_pool ;
        pthread_mutex_init(&arena_data.arenas[arena_index + 1].mutex, NULL);
        pthread_mutex_lock(&arena_data.arenas[arena_index + 1].mutex);
        pthread_mutex_unlock(&new_arena_mutex);

        return user_area((t_alloc_chunk *)current_pool->chunk, size, &arena_data.arenas[arena_index + 1].mutex);
    

【讨论】:

代码对我来说仍然是错误的。我更新了我的答案来解释。

以上是关于无法理解或修复我的程序中的竞争条件的主要内容,如果未能解决你的问题,请参考以下文章

资源竞争

JVM 如何在内部处理竞争条件?

MySQL事务没有停止for循环的竞争条件

如何修复条件变量等待/通知的竞争条件

我的电脑开机时候显示windows无法正常启动 我点击了修复

NHibernate 事务和竞争条件