Примитивные примитивы синхронизации - безопасно? - PullRequest
4 голосов
/ 23 апреля 2009

На устройствах с ограниченными возможностями я часто обнаруживаю "фальшивые" блокировки между двумя потоками с двумя bools. Каждый прочитан только одним потоком, и только написан другим. Вот что я имею в виду:

bool quitted = false, paused = false;
bool should_quit = false, should_pause = false;

void downloader_thread() {
    quitted = false;
    while(!should_quit) {
        fill_buffer(bfr);
        if(should_pause) {
            is_paused = true;
            while(should_pause) sleep(50);
            is_paused = false;
        }
    }
    quitted = true;
}

void ui_thread() {
    // new Thread(downloader_thread).start();
    // ...
    should_pause = true;
    while(!is_paused) sleep(50);
        // resize buffer or something else non-thread-safe
    should_pause = false;
}

Конечно, на ПК я бы этого не делал, но на ограниченных устройствах кажется, что чтение значения bool будет гораздо быстрее, чем получение блокировки. Конечно, я обмениваюсь на медленное восстановление (см. «sleep(50)»), когда требуется изменение буфера.

Вопрос - полностью ли он потокобезопасен? Или есть какие-то скрытые ошибки, о которых мне нужно знать, когда имитируют подобные блокировки? Или я вообще не должен этого делать?

Ответы [ 5 ]

6 голосов
/ 24 апреля 2009

Использование значений bool для связи между потоками может работать, как вы и предполагали, но на самом деле есть две скрытые ошибки, как описано в этом сообщении в блоге Виталия Липчинского :

Кэш-когерентность

ЦП не всегда извлекает значения памяти из ОЗУ. Кэши быстрой памяти на кристалле являются одним из приемов, используемых разработчиками ЦП для обхода узкого места фон Неймана . В некоторых многоядерных или многоядерных архитектурах (например, Intel Itanium ) эти кэши ЦП не используются совместно или автоматически синхронизируются. Другими словами, ваши потоки могут видеть разные значения для одного и того же адреса памяти , если они работают на разных процессорах.

Чтобы избежать этого, вам нужно объявить переменные как volatile ( C ++ , C # , java ) или сделать явное изменчивое чтение / запись или использование механизмов блокировки.

Оптимизация компилятора

Компилятор или JITter могут выполнять оптимизации, которые небезопасны, если задействованы несколько потоков. См. Связанный пост в блоге для примера. Опять же, вы должны использовать ключевое слово volatile или другие механизмы для информирования компилятора.

5 голосов
/ 23 апреля 2009

Если вы не понимаете архитектуру памяти вашего устройства в деталях, а также код, сгенерированный вашим компилятором, этот код не является безопасным.

То, что это сработает, не означает, что это сработает. «Ограниченные» устройства, такие как неограниченный тип, становятся все более и более мощными. Например, я бы не поспорил с поиском двухъядерного процессора в сотовом телефоне. Это означает, что я бы не поспорил, что приведенный выше код будет работать.

0 голосов
/ 24 апреля 2009

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

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

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

// new Thread(downloader_thread).start();
// ...
should_pause = true;

Далее запускается нить downloader_thread, и во временном интервале выполняются следующие строки:

quitted = false;
while(!should_quit)
{
    fill_buffer(bfr);

Планировщик выгружает downloader_thread перед возвратом fill_buffer, а затем активирует выполняемый ui_thread.

while(!is_paused) sleep(50);
// resize buffer or something else non-thread-safe
should_pause = false;

Операция изменения размера буфера выполняется, когда downloader_thread находится в процессе заполнения буфера. Это означает, что буфер поврежден, и вы, скорее всего, скоро выйдете из строя. Это не будет происходить каждый раз, но тот факт, что вы заполняете буфер перед тем, как установить is_paused в значение true, повышает вероятность того, что это произойдет, но даже если вы изменили порядок этих двух операций на downloader_thread, у вас все равно будет состояние гонки , но вы скорее всего зашли бы в тупик, а не повредили буфер.

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

Возможно, вам удастся избежать этого в той или иной форме на конкретной платформе / архитектуре, но действительно легко совершить ошибку, которую очень трудно отследить.

0 голосов
/ 24 апреля 2009

Отвечая на вопросы.

Это полностью потокобезопасно? Я бы ответил нет, это не потокобезопасно, и я бы просто не делал этого вообще. Не зная деталей нашего устройства и компилятора, если это C ++, компилятор может свободно переупорядочивать и оптимизировать вещи по своему усмотрению. например Вы написали:

is_paused = true;            
while(should_pause) sleep(50);            
is_paused = false;

, но компилятор может изменить порядок следующим образом:

sleep(50);
is_paused = false;

это, вероятно, не будет работать даже на одноядерном устройстве, как говорили другие.

Вместо того, чтобы брать блокировку, вы можете попытаться сделать лучше, просто сделать меньше в потоке пользовательского интерфейса, чем уступать в середине обработки сообщений пользовательского интерфейса. Если вы считаете, что потратили слишком много времени на поток пользовательского интерфейса, найдите способ аккуратно завершить работу и зарегистрировать асинхронный обратный вызов.

Если вы вызываете sleep в потоке пользовательского интерфейса (или пытаетесь получить блокировку или делаете что-либо, что может заблокировать), вы открываете дверь для зависаний и сбойных интерфейсов. Для этого достаточно 50 мс сна. И если вы пытаетесь получить блокировку или выполнить какую-либо другую блокирующую операцию (например, ввод-вывод), вам нужно иметь дело с реальностью ожидания неопределенного количества времени, чтобы получить ввод-вывод, который имеет тенденцию переводить с глюка на зависание.

0 голосов
/ 23 апреля 2009

Что касается вызова сна, вы всегда можете просто сделать сон (0) или эквивалентный вызов, который приостанавливает ваш поток, позволяя следующему в очереди по очереди.

Что касается остального, это потокобезопасно, если вы знаете детали реализации вашего устройства.

...