Как вы заметили, я люблю попиздеть. И ещё реже по делу, как то диктуют святые мужские традиции.
Futex и WaitOnAddress - это просто удобный механизм уходить в ожидание. Именно поэтому просто навесить какой-нибудь CAS на существующий мьютекс было идеей ниже среднего - удачи придумать, как нормально спать с примитивами, на то не рассчитанными.
The futex() system call provides a method for waiting until a certain condition becomes true. It is typically used as a blocking construct in the context of shared-memory synchronization. When using futexes, the majority of the synchronization operations are performed in user space. A user-space program employs the futex() system call only when it is likely that the program has to block for a longer time until the condition becomes true. Other futex() operations can be used to wake any processes or threads waiting for a particular condition.
То есть вызываешь FUTEX_WAIT, говоришь "за этим поинтером лежит 0хА". Если там 0хА, то поток уходит в сон. Просыпается, когда кто-то другой вызывает FUTEX_WAKE. В котором, кстати, ещё можно указывать, сколько спящих будить.
Сам мьютекс делается примерно так:
/* Simplified pthread mutex */
// 0 = unlocked,
// 1 = locked,
// 2 = locked with waiters
int mutex_word = 0;
void pthread_mutex_lock(int *mutex_word)
{
// Fast path: try to take the lock with an atomic CAS
if (atomic_cmpxchg(mutex_word, 0, 1) == 0)
return; // got it, no kernel call
// Slow path: there's contention, call into the kernel
do {
// Mark that there are waiters
atomic_set(mutex_word, 2);
// Sleep until value != 2
futex(mutex_word, FUTEX_WAIT, 2, NULL, NULL, 0);
} while (atomic_cmpxchg(mutex_word, 0, 2) != 0);
}
void pthread_mutex_unlock(int *mutex_word)
{
// Fast path: no waiters
if (atomic_xchg(mutex_word, 0) == 1)
return; // just clear, no kernel call
// Slow path: there are waiters, wake one
futex(mutex_word, FUTEX_WAKE, 1, NULL, NULL, 0);
}
В лучшем случае - мы просто дрочим mutex_word из 0 в 1, и наоборот. Потому что locking is only expensive when there's contention (это цитата а не выебон). Когда возвращаем, смотрим, не нужно ли оно кому, если нужно - будим его.
Кстати фанфакт: у фьютексов ещё есть режим с приоритетами. Если поток с высоким приоритетом спит, пока локом владеет поток с низким приоритетом, то последнему на время приоритет повышается. Чтобы съебал побыстрее.
Фокус в том, что вообще все мьютексы устроены буквально идентично. Даже в Go, хотя там они не удержались и всё-таки наебнули CAS дважды. В NetBSD, FreeBSD и Dragonfly тоже есть futex, в Винде эквивалентный WaitOnAddress - редкий момент унификации. Даже я переизобрёл свой мьютекс, когда нужно было по-честному мультиплексировать записи в сокет. Прям буквально 1 в 1.
Ну, а на правах инжирнера, существование одного фундаментального примитива приносит мне просто эстетическое удовольствие. The less the kernel knows, the better. А это уже выебон.
Но про Go я бы поговорил поподробнее. У них мьютекс интересный такой: есть starvation mode, когда кто-то дольше миллисекунды не может забрать себе лок, тогда владение мьютексом напрямую переходит к этому бедняжке. Правда, пока эта миллисекунда не прошла, происходит что-то очень близкое к busy loop. Просто потому, что запарковать горутину очень, очень дорого, а локи обычно долго не живут.
А ещё, сам sync.Mutex состоит из
state int32 и sema uint32. Они двое разделяют семантику mutex_word из сниппета - state для атомарного счётчика, sema как уникальный идентификатор для рантайма. Ну, точнее, его адрес, в самом sema обычно лежит просто 0. В рантайме есть своя субсистема семафор, которая как раз и использует фьютексы и WaitOnAddress, когда можно (а когда нельзя, то фоллбэк к обычным семафорам по сисколлам). Этот sema и используется, как "якорь", на котором futex будет засыпать.