Продолжаем разговор о синхронизации. В прошлом посте я рассказал о спинлоке, при этом мы совсем не коснулись проблемы которую решают примитивы синхронизации - race condition или просто гонка.
Что такое Race Condition?
Race condition - ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода. Своё название ошибка получила от похожей ошибки проектирования электронных схем.
Классический пример RC - работа с счетчиком из нескольких потоков. Без синхронизации и барьеров памяти. Более практиичный пример ситуации гонки - заказ товара через сайт (приложен к посту). Если не синхронизировать между собой участников системы то поведение программы может стать нежелательным (в случае магазина - можем напродавать товаров больше чем у нас физиечски есть).
Спинлоки разобранные вчера помогают решать подобные проблемы, но не лишены недостатков. Есть ли альтернатива спинлокам? Да, и это мьютексы.
Что такое Мьютекс?
Определение у мьютекса такое же как у спинлока, его цель - защита критической секции в коде. Но есть важный ньюанс - мьютекс передает управление потоком планировщику чтобы дать возможность другим потокам делать полезную работу (в отличии от спинлока который не отпускает ресурс и гоняет бесконечный цикл). Мьютекс в отличии спнлока пассивен и спит пока ждет своей очереди😊
Плюсы мьютекса:
- Экономия ресурса CPU по сравнению со спинлоком.
Минусы:
- В некоторых случаях будет медленнее чем спинлок, так как отпускать ресурс и возвращать его себе через планировщик будет дороже чем просто подержать.
Как я и говорил ранее - по умолчанию всегда стоит использовать именно его. В большинстве языков в публичное API выставлен именно mutex, чтобы найти spinlock нужно постараться.
Важный момент - функциональность блокировок практически во всех языках включает в себя не только активное / пассивное ожидание и гарантии эксклюзивного доступа, но и барьеры памяти. Именно поэтому нам как программистам явно не приходится объявлять их, мы работаем только с абстракцией блокировка, остальное от нас скрыто для нашего же спокойствия.
Пример кода в зависимости от архитектуры CPU инициирующего барьер.
static inline void memory_barrier(void)
{
#if defined(__x86_64__) || defined(__i386__)
__asm__ __volatile__("mfence" ::: "memory");
#elif defined(__aarch64__)
__asm__ __volatile__("dmb ish" ::: "memory");
#else
#error "Unsupported architecture"
#endif
}
Пример кода на языке С (pthread.h)
