Я написал эту программу для решения проблема обедающих философов с использованием алгоритма Дейкстры, обратите внимание, что я использую массив логических значений (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);
}
Да, это правда. но мне разрешено использовать только мьютексы, без семафоров, без условных переменных (это присваивание).
Вращение на locked
по определению является гонкой данных. Нет никакой гарантии, что что-то не только что приобрело мьютекс и собирается изменить это значение. Если вы используете этот цикл для любого вида синхронизации, я бы сказал, что все ставки сняты, если только за этим не стоит очень специфический вид дизайна, который делает его разумным. Я ничего не вижу, и в вашем коде нет комментариев, которые могли бы объяснить логику и последовательность. Предостережение: Я не проводил полного анализа вашей программы и не знаком с решаемой проблемой.
хотя бы поспать в этой петле - пожалуйста :-)
@paddy вращение выполняется после снятия замков
Да, это то, что я нахожу непростым. С чисто локальной точки зрения нет никакой гарантии, что после завершения while (data->locked[i]);
это data->locked[i]
снова не будет истинным. Может быть, это логическая гарантия алгоритма, но мне это кажется подозрительным.
@paddy, как вы сказали, это логическая гарантия алгоритма, data->locked[i]
может быть установлено значение true только по номеру потока i
, в то время как для него может быть установлено значение false по потоку LEFT
или RIGHT
(LEFT и RIGHT — это макросы для упрощения вычисления потока индексы).
Я добавил несколько комментариев к важным функциям, чтобы пояснить, что я пытаюсь сделать.
Вам разрешено использовать pthread_mutex_trylock()
? Ссылка: man7.org/linux/man-pages/man3/pthread_mutex_trylock.3p.html
@Kingsley, к сожалению, нет, это функции, связанные с мьютексами, которые мне разрешено использовать: pthread_mutex_init
pthread_mutex_destroy
pthread_mutex_lock
pthread_mutex_unlock
Как сказал Пэдди, while (data->locked[i]);
— это гонка данных; вы не удерживаете блокировку во время чтения, поэтому другой поток может взять блокировку и записать, пока вы читаете. На самом деле, вы полагаетесь на то, что происходит. Но это неопределенное поведение. Непосредственные практические последствия заключаются в том, что компилятор может удалить тест (поскольку в отсутствие гонки данных data->locked[i]
не может меняться между итерациями) или вообще удалить цикл (поскольку теперь это бесконечный цикл, а нетривиальные бесконечные циклы — это UB).
Таким образом, вы должны удерживать мьютекс при проверке флага. Если оно ложно, вы должны затем держать мьютекс, пока вы не установите его в истину и не выполните другую свою работу; в противном случае возникает гонка, в которой другой поток может получить его первым. Если это правда, то отбросьте мьютекс, немного подождите, возьмите его снова и повторите попытку.
@NateEldredge внутри цикла while мне нужно рассчитать время, прошедшее с момента последнего приема пищи в потоке или потоках, которые ожидают, что их соответствующее логическое значение станет ложным, и если оно превышает определенный предел, я должен остановить симуляцию, поэтому я должен заставить поток спать, прежде чем снова удерживать мьютекс? я имею в виду, что вместо того, чтобы спать, я мог бы немного поработать, например, подсчитать прошедшее время.
@NateEldredge, это решение, которое я искал, не могли бы вы превратить свои комментарии в ответ, чтобы я мог его принять?
Ваша спиновая петля while (data->locked[i]);
— это гонка данных; вы не удерживаете блокировку во время ее чтения data->locked[i]
, поэтому другой поток может взять блокировку и записать в ту же переменную, пока вы ее читаете. На самом деле, вы полагаетесь на то, что происходит. Но это неопределенное поведение.
Непосредственные практические последствия заключаются в том, что компилятор может удалить тест (поскольку в отсутствие гонки данных data->locked[i]
не может меняться между итерациями) или вообще удалить цикл (поскольку теперь это бесконечный цикл, а нетривиальные бесконечные циклы — это UB). Конечно, возможны и другие нежелательные последствия.
Таким образом, вы должны удерживать мьютекс при проверке флага. Если оно ложно, вы должны удерживать мьютекс до тех пор, пока не установите его в значение true и не выполните другую свою работу; в противном случае возникает гонка, в которой другой поток может получить его первым. Если это правда, то отбросьте мьютекс, немного подождите, возьмите его снова и повторите попытку.
(Вероятно, вам следует проверить, насколько долго длится «немного времени», и какую работу вы решите выполнять между ними. В зависимости от того, какие алгоритмы справедливости использует ваша реализация pthread, вы можете столкнуться с ситуациями, когда take_forks
удается повторно получить заблокировать, даже если put_forks
также ожидает блокировки.)
Конечно, в «настоящей» программе вы бы так не поступили; вы бы использовали переменную условия.
Я думаю, что ваши блокировки хороши и консервативны (хотя я могу ошибаться), но мне очень не нравится
while (data->locked[i]);
, который взывает к условной переменной