Как синхронизируются потоки на низком уровне?

Пытаюсь понять, как можно реализовать синхронизацию самостоятельно.

Сейчас понимание находится на таком уровне: у объекта есть булево свойство, обозначающее доступность объекта. В начале метода проверяем - если флаг "доступен": устанавливаем флаг в "недоступен", выполняем метод, возвращаем состояние "доступен", пробуждаем ожидающие потоки; если вначале он "недоступен", делаем блокировку.

Но тут видится подвох: пока один поток читает значение флага, другой может его перезаписывать или читать одновременно. Или я неправ и работа с базовыми типами атомарная? Возможно, недопонимание кроется ещё в чём-то другом...
  • Вопрос задан
  • 631 просмотр
Решения вопроса 1
@Mercury13
Программист на «си с крестами» и не только
Этого мало.
Первое. Нужны особые операции, которые гарантированно выполняются атомарно. Например (из исходников Delphi)
function InterlockedAdd(var Addend: Integer; Increment: Integer): Integer;
asm
      MOV   ECX,EAX
      MOV   EAX,EDX
 LOCK XADD  [ECX],EAX
      ADD   EAX,EDX
end;

Здесь префикс LOCK (блокирование шины) и даёт атомарность. Также используют операцию XCHG (exchange) — единственная атомарная без префикса LOCK.

На основе этого можно устроить объект, который называется spinlock. Крутим цикл, пока система не скажет: свободно. Из Википедии.
mov eax, spinlock_address
mov ebx, SPINLOCK_BUSY

wait_cycle:
xchg [eax], ebx  ; xchg - единственная инструкция, являющаяся атомарной без префикса lock
cmp ebx, SPINLOCK_FREE
jnz wait_cycle

; < критическая секция захвачена данным потоком, здесь идёт работа с разделяемым ресурсом >

mov eax, spinlock_address
mov ebx, SPINLOCK_FREE
xchg [eax], ebx  ; используется xchg для атомарного изменения
; последние 3 инструкции лучше заменить на mov [spinlock_address], SPINLOCK_FREE -
; это увеличит скорость за счёт отсутствия лишней блокировки шины, а mov и так выполнится атомарно
; (но только если адрес spinlock_address выровнен по границе двойного слова)


От спинлока до настоящего мьютекса остаётся один небольшой шаг. Спинлок потребляет ресурс процессора, не производя полезной работы. После того, как спинлок проработал несколько микросекунд и не освободился, мы говорим: не судьба — и передаём работу другому процессу. Надеюсь, вы в своей мини-ОС как-то научились переключать процессы.

В однопроцессорном ядре никаких спинлоков нет. Если надо захватить ресурс, а он чей-то — сразу отдаём управление другому.

UPD1. Почему не сразу отдавать работу другому процессу. Хороший тон — защищать мьютексом не системные вызовы (которые действительно долги) а какие-нибудь структуры данных вроде связных списков и атомарных struct. Так что вероятность, что объект просидит занятым долго, крайне мала. В настоящем мьютексе есть очередь с приоритетом, которая защищается, как ни странно, спинлоком. И этого достаточно.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
@JustMoose
Программист. Радиолюбитель. Прокрастинатор ;)
Про синхронизацию можно почитать у Рихтера.
Вот в этой книжке:
https://www.ozon.ru/context/detail/id/116668/

Правда, там ничего не сказано про современность, например про Неблокирующую синхронизацию. Чего про неё читать я не знаю.
https://ru.wikipedia.org/wiki/%D0%9D%D0%B5%D0%B1%D...
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы