Невозможно понять или исправить состояние гонки в моей программе - PullRequest
0 голосов
/ 06 февраля 2019

Я реализую свою собственную версию malloc, которая очень похожа на glibc malloc, так как в нем поддерживается многопоточность путем создания арен, которые являются областью памяти, с которой поток может работать, не рискуя конкурировать с другим потоком..

Моя структура данных выглядит следующим образом:

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) и массив, содержащий все мои арены.

Арена содержит только мьютекс, который инициализируется с помощью pthread_mutex_init (), и указатель на пул памяти.Пул памяти не важен для этой темы, так как условие гонки возникает до его достижения.

Как работает моя программа: когда каждый поток входит в malloc, он пытается pthread_try_lock мьютекс первой арены.Если это так, все хорошо, и он переходит к распределению, которое я здесь не описал.Если этого не произойдет, может произойти несколько вещей.

Если следующая запись в массиве пуста и M_ARENA_MAX не достигнут, новый мьютекс будет заблокирован, чтобы создать новую арену и добавить ее вмассив.Мьютекс является глобальным, что означает, что никакие два потока не могут создавать арену одновременно.

Если этот мьютекс заблокирован, то поток вернется к арене [0] и продолжит поиск открытого мьютекса.

Теперь я почти уверен, что условие гонки происходит из-за переменной arena_count.Благодаря инструкциям отладки printf я заметил, что в любое время функция segfaults, 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

Значение должно быть равным, но это не так.

Ответы [ 2 ]

0 голосов
/ 06 февраля 2019

(РЕДАКТИРОВАТЬ): Это не.

Кажется, это решило проблему:

    /* 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);
    }
0 голосов
/ 06 февраля 2019

Я могу заметить три проблемы в вашем коде.

1.Состояние гонки, когда два потока создают арены

Это условие гонки, которое вы описываете в своем вопросе:

Так что я подозреваю, что один поток может читать значение arena_count непосредственно переддругой поток увеличивает его, и к тому времени, когда он заканчивает чтение, поток, который увеличивает его, освобождает new_arena_mutex, и первый поток начинает создавать арену с неправильным индексом.

Да, это можетбывает. load from arena_data.arena_count происходит атомарно, но потоки могут вообще не предполагать, что значение (все еще) корректно.Измененная версия в вашем ответе не не решает проблему.

Чтобы это исправить, может быть полезна следующая гарантия: Любая store до 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(/* ... */);
...