Wildberries — это крутой магазин, в котором работает очень много хороших специалистов. И владельцы, как бы спорно они не управляли, управляют очень эффективно. Тем не менее, по моим сведениям, Татьяна описала "оптимизацию", которая коснулась и её отдела, достаточно точно.
Вот что я придумал, мы:
1. создаём waitgroup
2. лочим мьютекс
3. создаём 1000 рутин в каждой из которых
3.1 лочим мьютекс
3.2 разлочивам мьютекс
3.3 делаем Done в waitgroup
4. Разлочиваем мьютекс тем самым создаём «лавину» — 1000 одновременно ожидающих мьютексов
5. ждём waitgroup он освободится после последнего анлока
Про 100 потоков — организовать сложно, но вот с 2мя тест есть, как раз его написал по исходному замечанию. И он показывает, что обычные мьютексы быстрее на четверть, чем мой. + мой жрёт доп память на каждую итерацию примерно 200 байт.
Для незаблокированного мьютекса, тут просто заблокировать и разблокировать когда нет параллельного потока, то есть случай, когда по коду пойдёт быстрый случай:
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(), а этого я без дополнительного потока не смогу сделать.
По поводу cond, я как то упустил из виду. Бегло прочитав я согласен, что вероятно он подходит больше. Я думаю я перепишу с его использованием или использованием того, что под ним лежит. Но пока по изучаю.
Да бенчи проверяют только быстрый путь. Дополню вместе с переходом на cond.
И видимо нужно добавить еще пару веселых фишек типа поднятие уровня блокировки RLock -> Lock и понижение, и некую приоритезацию для блокировки. Они не дороги в разработке и по идее по ресурсам тоже.
Говоря про zap стоит раскрыть тему sampling-а
Да потерял :(
Спасибо!
Wildberries — это крутой магазин, в котором работает очень много хороших специалистов. И владельцы, как бы спорно они не управляли, управляют очень эффективно. Тем не менее, по моим сведениям, Татьяна описала "оптимизацию", которая коснулась и её отдела, достаточно точно.
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 %
Код бенча примера
Для незаблокированного мьютекса, тут просто заблокировать и разблокировать когда нет параллельного потока, то есть случай, когда по коду пойдёт быстрый случай:
Для заблокированного мьютекса, тут требуется организовать 2 потока и сделать примерно следующее: блокируем мьютекс, затем создаём рутину (~ 250 нс на моей машине) в которой мьютекс разблокируем, и снова блокируем мьютекс (тут мы начинаем ожидать той самой разблокировки в параллельном потоке) и потом разблокируем мьютекс для следующей итерации.
Получается:
По поводу sync.Cond. Я потоки лочу через select { case } это позволяет мне их разблокировать несколькими независимыми событиями. Если я буду блочить через Cond.Wait() то мне придётся разлочивать Cond на основании <-ctx.Done(), а этого я без дополнительного потока не смогу сделать.
Как-то так.
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().
Если я не прав прошу меня попправить.
По поводу cond, я как то упустил из виду. Бегло прочитав я согласен, что вероятно он подходит больше. Я думаю я перепишу с его использованием или использованием того, что под ним лежит. Но пока по изучаю.
Да бенчи проверяют только быстрый путь. Дополню вместе с переходом на cond.
И видимо нужно добавить еще пару веселых фишек типа поднятие уровня блокировки RLock -> Lock и понижение, и некую приоритезацию для блокировки. Они не дороги в разработке и по идее по ресурсам тоже.