Как решить проблему обедающих философов, используя только мьютексы?

Я написал эту программу для решения проблема обедающих философов с использованием алгоритма Дейкстры, обратите внимание, что я использую массив логических значений (data->locked) вместо массива двоичных семафоров.

Я не уверен, что это решение действительно (отсюда и вопрос ТАК).

Будет ли доступ к массиву data->locked в функциях test и take_forks вызывать гонки данных? если да, то возможно ли решить эту проблему, используя алгоритм Дейкстры только с мьютексами?

Мне разрешено использовать только мьютексы, никаких семафоров, никаких условных переменных (это присваивание).

Пример использования:

./a.out 4 1000 1000
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>

#define NOT_HUNGRY  1
#define HUNGRY      2
#define EATING      3
#define RIGHT   ((i + 1) % data->n)
#define LEFT    ((i + data->n - 1) % data->n)

typedef struct s_data
{
    int             n;
    int             t_sleep;
    int             t_eat;
    int             *state;
    bool            *locked;
    pthread_mutex_t *state_mutex;
}   t_data;

typedef struct s_arg
{
    t_data  *data;
    int     i;
}   t_arg;

int ft_min(int a, int b)
{
    if (a < b)
        return (a);
    return (b);
}

int ft_max(int a, int b)
{
    if (a > b)
        return (a);
    return (b);
}

// if the LEFT and RIGHT threads are not eating
// and thread number i is hungry, change its state to EATING
// and signal to the while loop in `take_forks` to stop blocking.
// if a thread has a state of HUNGRY then it's guaranteed
// to be out of the critical section of `take_forks`.
void    test(int i, t_data *data)
{
    if (
        data->state[i] == HUNGRY
        && data->state[LEFT] != EATING
        && data->state[RIGHT] != EATING
    )
    {
        data->state[i] = EATING;
        data->locked[i] = false;
    }
}

// set the state of the thread number i to HUNGRY
// and block until the LEFT and RIGHT threads are not EATING
// in which case they will call `test` from `put_forks`
// which will result in breaking the while loop
void    take_forks(int i, t_data *data)
{
    pthread_mutex_lock(data->state_mutex);
    data->locked[i] = true;
    data->state[i] = HUNGRY;
    test(i, data);
    pthread_mutex_unlock(data->state_mutex);
    while (data->locked[i]);
}

// set the state of the thread number i to NOT_HUNGRY
// then signal to the LEFT and RIGHT threads
// so they can start eating when their neighbors are not eating
void    put_forks(int i, t_data *data)
{
    pthread_mutex_lock(data->state_mutex);
    data->state[i] = NOT_HUNGRY;
    test(LEFT, data);
    test(RIGHT, data);
    pthread_mutex_unlock(data->state_mutex);
}

void    *philosopher(void *_arg)
{
    t_arg   *arg = _arg;

    while (true)
    {
        printf("%d is thinking\n", arg->i);
        take_forks(arg->i, arg->data);
        printf("%d is eating\n", arg->i);
        usleep(arg->data->t_eat * 1000);
        put_forks(arg->i, arg->data);
        printf("%d is sleeping\n", arg->i);
        usleep(arg->data->t_sleep * 1000);
    }
    return (NULL);
}

void    data_init(t_data *data, pthread_mutex_t *state_mutex, char **argv)
{
    int i = 0;

    data->n = atoi(argv[1]);
    data->t_eat = atoi(argv[2]);
    data->t_sleep = atoi(argv[3]);
    pthread_mutex_init(state_mutex, NULL);
    data->state_mutex = state_mutex;
    data->state = malloc(data->n * sizeof(int));
    data->locked = malloc(data->n * sizeof(bool));
    while (i < data->n)
    {
        data->state[i] = NOT_HUNGRY;
        data->locked[i] = true;
        i++;
    }
}

int main(int argc, char **argv)
{
    pthread_mutex_t state_mutex;
    t_data          data;
    t_arg           *args;
    pthread_t       *threads;
    int             i;

    if (argc != 4)
    {
        fputs("Error\nInvalid argument count\n", stderr);
        return (1);
    }
    data_init(&data, &state_mutex, argv);
    args = malloc(data.n * sizeof(t_arg));
    i = 0;
    while (i < data.n)
    {
        args[i].data = &data;
        args[i].i = i;
        i++;
    }
    threads = malloc(data.n * sizeof(pthread_t));
    i = 0;
    while (i < data.n)
    {
        pthread_create(threads + i, NULL, philosopher, args + i);
        i++;
    }
    i = 0;
    while (i < data.n)
        pthread_join(threads[i++], NULL);
}

Я думаю, что ваши блокировки хороши и консервативны (хотя я могу ошибаться), но мне очень не нравится while (data->locked[i]);, который взывает к условной переменной

pm100 21.03.2022 04:46

Да, это правда. но мне разрешено использовать только мьютексы, без семафоров, без условных переменных (это присваивание).

soufiane yakoubi 21.03.2022 04:59

Вращение на locked по определению является гонкой данных. Нет никакой гарантии, что что-то не только что приобрело мьютекс и собирается изменить это значение. Если вы используете этот цикл для любого вида синхронизации, я бы сказал, что все ставки сняты, если только за этим не стоит очень специфический вид дизайна, который делает его разумным. Я ничего не вижу, и в вашем коде нет комментариев, которые могли бы объяснить логику и последовательность. Предостережение: Я не проводил полного анализа вашей программы и не знаком с решаемой проблемой.

paddy 21.03.2022 05:01

хотя бы поспать в этой петле - пожалуйста :-)

pm100 21.03.2022 05:01

@paddy вращение выполняется после снятия замков

pm100 21.03.2022 05:02

Да, это то, что я нахожу непростым. С чисто локальной точки зрения нет никакой гарантии, что после завершения while (data->locked[i]); это data->locked[i] снова не будет истинным. Может быть, это логическая гарантия алгоритма, но мне это кажется подозрительным.

paddy 21.03.2022 05:09

@paddy, как вы сказали, это логическая гарантия алгоритма, data->locked[i] может быть установлено значение true только по номеру потока i, в то время как для него может быть установлено значение false по потоку LEFT или RIGHT (LEFT и RIGHT — это макросы для упрощения вычисления потока индексы).

soufiane yakoubi 21.03.2022 05:29

Я добавил несколько комментариев к важным функциям, чтобы пояснить, что я пытаюсь сделать.

soufiane yakoubi 21.03.2022 06:38

Вам разрешено использовать pthread_mutex_trylock() ? Ссылка: man7.org/linux/man-pages/man3/pthread_mutex_trylock.3p.html

Kingsley 21.03.2022 06:57

@Kingsley, к сожалению, нет, это функции, связанные с мьютексами, которые мне разрешено использовать: pthread_mutex_initpthread_mutex_destroypthread_mutex_lockpthread_mutex_unlock

soufiane yakoubi 21.03.2022 07:41

Как сказал Пэдди, while (data->locked[i]); — это гонка данных; вы не удерживаете блокировку во время чтения, поэтому другой поток может взять блокировку и записать, пока вы читаете. На самом деле, вы полагаетесь на то, что происходит. Но это неопределенное поведение. Непосредственные практические последствия заключаются в том, что компилятор может удалить тест (поскольку в отсутствие гонки данных data->locked[i] не может меняться между итерациями) или вообще удалить цикл (поскольку теперь это бесконечный цикл, а нетривиальные бесконечные циклы — это UB).

Nate Eldredge 21.03.2022 16:17

Таким образом, вы должны удерживать мьютекс при проверке флага. Если оно ложно, вы должны затем держать мьютекс, пока вы не установите его в истину и не выполните другую свою работу; в противном случае возникает гонка, в которой другой поток может получить его первым. Если это правда, то отбросьте мьютекс, немного подождите, возьмите его снова и повторите попытку.

Nate Eldredge 21.03.2022 16:21

@NateEldredge внутри цикла while мне нужно рассчитать время, прошедшее с момента последнего приема пищи в потоке или потоках, которые ожидают, что их соответствующее логическое значение станет ложным, и если оно превышает определенный предел, я должен остановить симуляцию, поэтому я должен заставить поток спать, прежде чем снова удерживать мьютекс? я имею в виду, что вместо того, чтобы спать, я мог бы немного поработать, например, подсчитать прошедшее время.

soufiane yakoubi 21.03.2022 16:57

@NateEldredge, это решение, которое я искал, не могли бы вы превратить свои комментарии в ответ, чтобы я мог его принять?

soufiane yakoubi 21.03.2022 23:04
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
14
57
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ваша спиновая петля while (data->locked[i]); — это гонка данных; вы не удерживаете блокировку во время ее чтения data->locked[i], поэтому другой поток может взять блокировку и записать в ту же переменную, пока вы ее читаете. На самом деле, вы полагаетесь на то, что происходит. Но это неопределенное поведение.

Непосредственные практические последствия заключаются в том, что компилятор может удалить тест (поскольку в отсутствие гонки данных data->locked[i] не может меняться между итерациями) или вообще удалить цикл (поскольку теперь это бесконечный цикл, а нетривиальные бесконечные циклы — это UB). Конечно, возможны и другие нежелательные последствия.

Таким образом, вы должны удерживать мьютекс при проверке флага. Если оно ложно, вы должны удерживать мьютекс до тех пор, пока не установите его в значение true и не выполните другую свою работу; в противном случае возникает гонка, в которой другой поток может получить его первым. Если это правда, то отбросьте мьютекс, немного подождите, возьмите его снова и повторите попытку.

(Вероятно, вам следует проверить, насколько долго длится «немного времени», и какую работу вы решите выполнять между ними. В зависимости от того, какие алгоритмы справедливости использует ваша реализация pthread, вы можете столкнуться с ситуациями, когда take_forks удается повторно получить заблокировать, даже если put_forks также ожидает блокировки.)

Конечно, в «настоящей» программе вы бы так не поступили; вы бы использовали переменную условия.

Другие вопросы по теме