Комментарии 13
Возможно я что-то не так понял (У меня мало опыта программирования на Go), но у вас очень странные бенчмарки т.к. вы тестируете только быстрый сценарий (Фактически у вас только одна горутина пытается захватить мьютекс и поэтому всю магию каналов вы фактически не проверяете). Ну и кроме того внутри канала лежит самый обычный мьютекс просто он спрятан от вас… Думаю более правильным и эффективным будет использование Cond (https://golang.org/pkg/sync/#Cond) вместо канала. А вообще странно конечно что такие простые вещи не реализованы в стандартной библиотеке и приходится писать свои велосипеды
По поводу cond, я как то упустил из виду. Бегло прочитав я согласен, что вероятно он подходит больше. Я думаю я перепишу с его использованием или использованием того, что под ним лежит. Но пока по изучаю.
Да бенчи проверяют только быстрый путь. Дополню вместе с переходом на cond.
И видимо нужно добавить еще пару веселых фишек типа поднятие уровня блокировки RLock -> Lock и понижение, и некую приоритезацию для блокировки. Они не дороги в разработке и по идее по ресурсам тоже.
BenchmarkDT_RWTMutexTryLockUnlock-8 1911730 625 ns/op 192 B/op 1 allocs/op
BenchmarkDT_RWMutexLockUnlock-8 2423566 494 ns/op 0 B/op 0 allocs/op
BenchmarkDT_MutexLockUnlock-8 2577855 466 ns/op 0 B/op 0 allocs/op
Потребляется память за счёт того, что пересоздаётся канал.
Оценил Cond. Реализацию с ним плохо представляю без создания доп потока (а это 4 кб) так, что бы корректно реагировать на ctx.Done().
Если я не прав прошу меня попправить.
Бенчмарки кривые…
Я предполагал что будет что-то вида:
mx = Mutex()
for i := 0; i < 100; i++ {
go func() {
for j := 0; j < 1000; j++ {
mx.RTryLock(ctx)
sleep(0.001)
mx.RUnlock()
}
}
}
Плюс к этому:
Основная проблема каналов это то что для обычного мьютекса (не RWMutext) если он освобождается то достаточно разбудить ровно один другой поток и дать ему залочить мьютекс. В случае же канала если у вас один мьютекс ожидает например 100 потоков то вы разбудите их все, а потом одному из них удасться захватить мьютекс. Это ужасно неэффективно и вот собственно этот момент надо сравнить в вашей реализации и в стандартной либе…
Реализацию через Cond не вижу проблем если честно… ровно в том же месте где у вас закрывается канал необходимо выставлять событие "мьютекс освободился" и все кому это интересно могут проснуться и попытаться его захватить
Для незаблокированного мьютекса, тут просто заблокировать и разблокировать когда нет параллельного потока, то есть случай, когда по коду пойдёт быстрый случай:
func BenchmarkRWTMutexTryLockUnlock(b *testing.B) {
ctx := context.Background()
mx := RWTMutex{}
for i := 0; i < b.N; i++ {
mx.TryLock(ctx)
mx.Unlock()
}
}
Для заблокированного мьютекса, тут требуется организовать 2 потока и сделать примерно следующее: блокируем мьютекс, затем создаём рутину (~ 250 нс на моей машине) в которой мьютекс разблокируем, и снова блокируем мьютекс (тут мы начинаем ожидать той самой разблокировки в параллельном потоке) и потом разблокируем мьютекс для следующей итерации.
Получается:
func BenchmarkDT_RWTMutexTryLockUnlock(b *testing.B) {
ctx := context.Background()
mx := RWTMutex{}
for i := 0; i < b.N; i++ {
mx.TryLock(ctx)
go func() {
mx.Unlock()
}()
mx.TryLock(ctx)
mx.Unlock()
}
}
По поводу sync.Cond. Я потоки лочу через select { case } это позволяет мне их разблокировать несколькими независимыми событиями. Если я буду блочить через Cond.Wait() то мне придётся разлочивать Cond на основании <-ctx.Done(), а этого я без дополнительного потока не смогу сделать.
Как-то так.
1-й бенчмарк не имеет смысла впринципе т.к. проверяется по сути скорость работы атомик операций на вашем процессоре. единственное его предназначение это может быть поиск каких то простейших ошибок. В случае если горутин множество 2 горутины слишком мало для того чтобы собственно косяки Вашей реализации стали очевидны. Сравните именно случай где за один мьютекс конкурируют 100 потоков и сравните разницу для вашей реализации и для стандартного мьютекса
Не вижу сложностей если честно… что мешает создать 100-1000 горутин? вопрос только в том что каждая отдельная горутина должна работать достаточно долго для того чтобы можно было пренебречь расходами на ее создание. т.е. если у вас ~ 250 нс уходит на создание горутины значит она должна работать хотя бы несколько секунд и сделать много итераций. Думаю лучший вариант бенчмарка будет сделать простой счетчик и инкрементить его из множества горутин. это позволит симулировать плюс минус реальный сценарий и убедится что все работает корректно. Моя основная идея в том что реализация на каналах будет подвержена проблеме "Проснувшегося стада" (https://en.wikipedia.org/wiki/Thundering_herd_problem) и надо проверить работу этого сценария и подумать можно ли улучшить что-то в реализации для того чтобы его избежать
Если я буду блочить через Cond.Wait() то мне придётся разлочивать Cond на основании <-ctx.Done(), а этого я без дополнительного потока не смогу сделать.
А вот это уже печально, я надеялся что Го позволяет делать select на канал либо Cond… если это не возможно то это очень печально (и еще раз подтверждает мои мысли что Go это недоделанный язык)
1. создаём waitgroup
2. лочим мьютекс
3. создаём 1000 рутин в каждой из которых
3.1 лочим мьютекс
3.2 разлочивам мьютекс
3.3 делаем Done в waitgroup
4. Разлочиваем мьютекс тем самым создаём «лавину» — 1000 одновременно ожидающих мьютексов
5. ждём waitgroup он освободится после последнего анлока
Результаты:
BenchmarkN0T_RWTMutexTryLockUnlock-8 2174 535396 ns/op 36406 B/op 380 allocs/op
BenchmarkN0T_RWMutexLockUnlock-8 2400 447199 ns/op 52 B/op 1 allocs/op
BenchmarkN0T_MutexLockUnlock-8 3020 388154 ns/op 39 B/op 1 allocs/op
Мой мьютекс дольше стандартных на ~ 30 %
А а RW мьютекс дольше простого на ~ 15 %
Код бенча примера
func BenchmarkN0T_RWTMutexTryLockUnlock(b *testing.B) {
ctx := context.Background()
mx := RWTMutex{}
k := 1000
for i := 0; i < b.N; i++ {
var wg sync.WaitGroup
wg.Add(k)
mx.TryLock(ctx)
for j := 0; j < k; j++ {
go func() {
mx.TryLock(ctx)
mx.Unlock()
wg.Done()
}()
}
mx.Unlock()
wg.Wait()
}
}
А давайте в Go сделаем TryLock(context.Context)