Почему эта функция test_and_set приводит к взаимоблокировке?

#c #multithreading #deadlock

#c #многопоточность #взаимоблокировка

Вопрос:

Я использую два потока для обновления глобальной переменной. Чтобы добиться взаимного исключения, я использую эту test_and_set функцию. Но этот код переходит в взаимоблокировку в какой-то случайный момент во время выполнения. Пожалуйста, помогите.

 #include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>

int a = 0;
atomic_int lock = 0;

int test_and_set(int *lock)
{
    int l = *lock;
    *lock = 1;
    return l;
}

void *func(void * param)
{
    for(int i = 0; i < 100000; i  )
    {
        while(test_and_set(amp;lock));
        a  ;
        lock = 0;
    }
}

int main()
{
    pthread_t p1, p2;
    pthread_create(amp;p1, NULL, func);
    pthread_create(amp;p2, NULL, func);
    
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    
    printf("%d, %dn", a, b);
    return 0;
}
  

Ответ №1:

Да, рано или поздно это приведет к взаимоблокировке.

Вот как:

  1. Поток A «получит блокировку» и будет готов к выполнению a и lock = 0;

  2. Поток B берет верх и выполняет l = *lock; . Поскольку A имеет блокировку l , теперь 1.

  3. Поток A берет верх и выполняет оба a и lock = 0; . Это устанавливает блокировку на ноль.

  4. Поток B принимает управление, выполняет *lock = 1; и возвращает l значение 1.

Теперь это взаимоблокировка. Значение lock равно 1. Поток возвращает 1 из test_and_set и останется в while . Поток B начнет считывать блокировку, но всегда будет получать 1. Нет потока, который установит блокировку на ноль. Взрыв…

Аналогичным образом вы можете создать ситуацию, когда оба потока получают «блокировку» одновременно.

Короче говоря: вы не можете реализовать подобную блокировку.

Из https://en.wikipedia.org/wiki/Test-and-set:

В информатике инструкция test-and-set — это инструкция, используемая для записи 1 (set) в ячейку памяти и возврата ее старого значения в виде одной атомарной (т. Е. Не прерываемой) операции.

У вас нет «одной атомной операции». Поэтому поток A выше может выполнить шаг 3 между потоком B, выполняющим шаг 2 и шаг 4.

Если ваш процессор имеет инструкцию test-and-set, вы можете попробовать использовать код на ассемблере.

Примечание: в «реальном» мире переключение задач не происходит в соответствии с операторами C. Один оператор C обычно реализуется как несколько машинных инструкций. Переключение задачи может произойти в любой машинной инструкции. Принцип тот же.

Ответ №2:

Я еще не понял, где происходит ваша взаимоблокировка, но это неправильный способ реализовать блокировку данных, совместно используемых потоками. Предположим, что lock это 0 и что оба потока вызываются test_and_set одновременно. Они оба увидят, что lock это 0, и оба установят его равным 1. Возвращается значение 0, и поэтому оба потока думают, что у них есть блокировка.

Мьютекс (сокращение от «взаимного исключения») выполняет то, что вы пытаетесь сделать здесь, гарантируя, что вышеупомянутый асинхронный беспорядок не может произойти.

Когда вы вызываете pthread_mutex_lock мьютекс, эта функция не вернется, пока вызывающий поток не получит блокировку. Это означает, что любой другой вызывающий поток pthread_mutex_lock будет блокироваться до тех пор, пока первый поток не освободит мьютекс путем вызова pthread_mutex_unlock .

Вот как вы должны использовать мьютекс.

 int a = 0;
pthread_mutex_t lock;

void *func(void * param)
{
    for(int i = 0; i < 100000; i  )
    {
        pthread_mutex_lock(amp;lock):
        a  ;
        pthread_mutex_unlock(amp;lock);
    }
}

int main() {
    pthread_t p1, p2;

    pthread_mutex_init(amp;lock,NULL);

    ...
}
  

Комментарии:

1. Спасибо за объяснение, но я хочу синхронизировать без мьютекса (это вопрос интервью). Не могли бы вы объяснить, как реализован мьютекс?

2. Я считаю, что мьютексы реализованы в ядре. Я не знаю механики.

3. Вы могли бы посмотреть на блокировки вращения .

4. Похоже, у вас почти реализована блокировка вращения в вашем исходном сообщении. Однако вам нужно, чтобы первые две строки test_and_set выполнялись атомарно.

5. Посмотрите на atomic_exchange from sdatomic.h .