Изучаем Go. Урок №20. Concurrency (2). WaitGroup. Data Race. Mutex/RWMutex

Поділитися
Вставка
  • Опубліковано 10 лют 2025

КОМЕНТАРІ • 72

  • @Svoboda2000
    @Svoboda2000 2 роки тому +36

    желаю чтобы твой канал стал №1 по Golang

    • @thisisit7267
      @thisisit7267  2 роки тому +3

      Спасибо, буду стараться)

    • @jellyfish6265
      @jellyfish6265 Рік тому +4

      автор куда-то пропал, но он действительно лучший

  • @kiborg6982
    @kiborg6982 2 роки тому +22

    Очень надеюсь, ты дойдешь до конца и запишешь все планируемые уроки!

  • @aidamur
    @aidamur Рік тому +9

    просто нереально полезные видео по многопоточности. Рекомендую всем знакомым, кто учит го. Огромное спасибо. Очень легкая подача, все понятно. Ты святой человек)

  • @АйдосМаульшариф
    @АйдосМаульшариф 7 місяців тому +4

    Бро лучше тебя никто не объясняет. Честно, с тобой я начал искренне понимать глубокие аспекты го.
    Желаю тебе и твоему каналу роста и популярности, и, надеюсь, ты создашь курс по общим паттернам и структуре сервисов в го потому, что с твоим обьяснением это было бы великолепно.
    С меня лайк и подписка!

  • @AstanaKZ1
    @AstanaKZ1 Рік тому +4

    самое лучшее объяснение, получше даже платных уроков! Вы гений! Я пришла после долгих попыток понять RWMutexы

    • @jon4775
      @jon4775 4 місяці тому +1

      как успехи в golang сейчас?

  • @georgiy_kulagin
    @georgiy_kulagin 2 роки тому +2

    Очень круто и подробно объясняешь, спасибо! Надеюсь, новые видео не заставят себя ждать, а то последнее по модулям было 6 месяцев назад =(

  • @MaximRovinsky
    @MaximRovinsky 2 роки тому +4

    Грац с 500 подписчиков) Крутые видео 👍🏼

  • @Роман-э7ф9й
    @Роман-э7ф9й 2 роки тому +2

    Спасибо за уроки! Очень хорошо изложенная информация.

  • @DmSi8
    @DmSi8 2 роки тому +1

    Вы очень доходчиво объясняете. Большое спасибо за качественные уроки!

  • @galiaf1987
    @galiaf1987 5 місяців тому

    По этому видео конкурентность в sql даже понятней становится)

  • @TheDobermanTV
    @TheDobermanTV 2 роки тому

    Самое понятное объяснение про RWMutex. Спасибо

  • @Hande_hoch
    @Hande_hoch 5 місяців тому +1

    спасибо за старания. Можно критику? Не шатай, плз, туда-сюда вверх-вниз код. Захвати один кусок кода и его разбирай, не дрыгая, если нет необходимости показывать разные части кода, которые не влезают в экран. upd: досмотрел. Чел, видос бомбовый! Спасибо еще раз

  • @dasydasy9268
    @dasydasy9268 2 роки тому +1

    Отличное объяснение! Пожалуйста продолжай)

  • @ev1lyas
    @ev1lyas 4 місяці тому

    Спасибо за уроки!

  • @devopsbrain
    @devopsbrain 8 місяців тому +1

    Это просто великолепие.

  • @romanbush5164
    @romanbush5164 Рік тому +1

    Спасибо наконецто понятно преемущество над другим языками, но отсутвие ооп все равно бесит)

  • @Арт1234-с3г
    @Арт1234-с3г 2 роки тому +1

    Супер объяснение

  • @developmentapp
    @developmentapp 2 місяці тому

    Да уж 😄 15 сек 1000 итераций! Пора вам комп менять. 1000000 = ~0,2 с. Спасибо, отлично объясняешь!

    • @richardplantagenet4095
      @richardplantagenet4095 2 місяці тому

      Там задержка стоит)
      Сбрось ее и сравни по времени

  • @stasrubtsov1870
    @stasrubtsov1870 2 роки тому

    Вау!!! Вот это скорость!!!😀

  • @andreichemeris4141
    @andreichemeris4141 2 роки тому +1

    спасибо, все р\понятно

  • @DmitryZinovyev
    @DmitryZinovyev 2 роки тому +5

    Спасибо за уроки, с удовольствием смотрю
    Вопрос про 15 секунд
    1000 раз по одной наносекунде (10 в -9 степени) получается 1 микросекунда (10 в -6 степени)
    Как смогли получится секунды в примере без горутин - загадка
    У меня на m1 вот такие результаты:
    writeWithoutConcurrent:
    1000
    0.000464875
    writeWithoutMutex:
    986
    0.000581708
    writeWithMutex:
    1000
    0.000557375
    То есть все колеблется около стат погрешности

    • @DmitryZinovyev
      @DmitryZinovyev 2 роки тому

      Вот данные с другой машины (MacOS Quad-Core Intel i5)
      writeWithoutConcurrent:
      1000
      0.00042488
      writeWithoutMutex:
      993
      0.000692333
      writeWithMutex:
      1000
      0.000558311
      То же все в пределах погрешностей

    • @slayers2966
      @slayers2966 Рік тому

      Вызов функции слип наверняка занимает больше 1 наносекунды, так что там на вызов тратится больше времени чем на слип. А насчет ваших результатов, то видимо дело в бОльшем количестве логических ядер или более производительном железе.

    • @DmitryZinovyev
      @DmitryZinovyev Рік тому +1

      @@slayers2966 у автора результат порядка 5 секунд (округлим 15 секунд в меньшую сторону) у меня результат порядка 0.0005 секунд, это в 10к раз быстрее, такую разницу не обьяснить расходом на вызов функции sleep и разницей в железе. Даже просто интуитивно пробежать пустой цикл на 1к итераций за 15 секунд это очень медленно для любого языка и железа.
      Проверьте на вашей машине, интересно какие результаты получатся
      Я думаю, что это скорее связано с ос виндоус и/или багом в версии го, которая у автора

  • @dmitrykhrniak451
    @dmitrykhrniak451 2 роки тому

    отлично

  • @ennwy
    @ennwy 2 роки тому +4

    Я скопировал код функции, которая на видео выполнялась 15 секунд с вашего гит хаба. У меня она выполнилась за 2 десятитысячные секунды ( 0.0002 ), после протестировал функцию writeWithMutex, где мы для избежания data race лочили блок кода с записью в переменную, она у меня выполнилась за 5 десятитысячных ( в два с половиной раза медленнее). Я плохо понимаю, для чего мы запускаем кучу горутин, если он все все-равно становятся в очередь и ждут своего времени на лок. Чем это лучше обычного цикла с поочередным выполнением? В нашем случае мы получили даже обратный эффект, так-как нам нужно инициализировать 1000 горутин. Также, как я понимаю, когда горутина спит наносекунду управление берет на себя другая горутина и выходит так, что первая горутина спит больше наносекунды и так постоянно (можете меня поправить, просто предположение).
    Также я не понимаю, как функция в которой мы ждем наносекунду и добавляем единицу ( выполняется в простом цикле ) выполняется 15 секунд. За 1000 итераций команда time.Sleep(time.Nanosecond) в сумме даст одну милионную секунды, а все остальное время будет занимать операция добавления? То есть 1000 операций добавления выполняются (15 - 1 / 1 000 000) секунды? В этом моменте я вообще выпал =)
    Я новичок, наверное многого не понимаю, буду благодарен за ответ =)

    • @thisisit7267
      @thisisit7267  2 роки тому +13

      Вы верно подметили, что как то уж очень долго выполнялся данный код. Я пошел разбираться, и оказалось, что это проблема в реализации работы на Windows, там таймер работает не очень корректно. Тут можно глянуть подробнее: github.com/golang/go/issues/44343
      Также я запустил данный код на MacOs, и действительно код с циклом выполнился в 2 раза быстрей, чем с mutex.
      Насчет большого количества горутин вы абсолютно правы, много горутин - не всегда самый лучший по скорости выполнения результат. В видеоуроке, где я рассказываю про паттерн workersPool используется код, где количество горутин гораздо меньше, а скорость выполнения гораздо лучше.
      При выборе количества горутин очень важно понимать профиль работы вашего кода: это может быть cpu bounded или i/o bounded работа. В случае с cpu bounded (обычно это просто какие то вычисления), как например в нашем цикле с инкрементом - нет смысла запускать 1000 горутин, ибо на их менеджмент у планировщика уходит много ресурсов и времени, проще и быстрее инкрементить счетчик в одной горутине. Но если у вас очень много I/O bounded нагрузки (запросы к бд, сетевые запросы), то тут основная работа не выполняется вашим процессором, а вы просто находитесь в ожидании ответа (я попытался time.sleep имитировать это время ожидания). И вот тут то вы и можете почуствовать преимущество конкуретной работы.
      Простой пример - нужно сделать 5 запросов в разные сервисы. Собрать результат и куда то его вывести/отправить. Каждый запрос отвечает по 1 секунде.
      Если делать последовательно, то вы будете выполнять это минимум 5 секунд.
      Если запустить 5 запросов в отдельных горутинах, то вы можете уложиться за +-1 секунду.
      В общем самое важное - это понимать, когда вы можете распараллелить работу, а когда нет, и как избегать ошибок при параллельной работе)
      Еще хотел про сравнение работы мьютекса и без мьютекса дописать: если брать пример с вышеописанными 5ю запросами - то в первом случае вы выполняете последовательно всю работу: создание переменных, выполение запроса, ожидание ответа, запись ответа в переменную и потом по новой для другого запроса.
      Когда вы запускаете 5 запросов параллельно, скорее всего вы будете использовать mutex только в момент записи результатов в какой-то общий объект. т.е. здесь у вас будет только очередь на запись результатов, а в первом случае очередь на все действия.

    • @ennwy
      @ennwy 2 роки тому

      @@thisisit7267 Спасибо за ответ, стало яснее =)

    • @rst7678
      @rst7678 2 роки тому +1

      Для Ubuntu ---> поменяйте time.Sleep(time.Nanosecond) на time.Sleep(time.Millisecond * 15) и будете чувствовать разницу.

  • @qwertyzyxel9349
    @qwertyzyxel9349 Рік тому

    👍👍👍

  • @stanislawgupaliuk3771
    @stanislawgupaliuk3771 Рік тому

    Для чего ты в участок кода между mu.Lock() и mu.Unlock() добавляешь time.sleep(), когда рассказываешь про RWMutex, конечно оно так дольше будет работать) если убрать, то те же ~0.05 сек, что и до этого) ты как будто искусственно увеличиваешь время)

  • @nika31169
    @nika31169 Рік тому +2

    Сколько бы я не смотрел видео про конкурентность все говорят что 1 горутина занимает 1 ядро процессора, но это не верно в Go встренный планировщик горутин и он позволяет запускать хоть 10к горутин на 1 ядре

    • @thisisit7267
      @thisisit7267  Рік тому +1

      Все верно. Тут скорее про то, что параллельно выполняющихся горутин с cpu bound нагрузкой может быть не больше чем логических ядер процессора. Если есть какая-то io-bound нагрузка (сетевые запросы например), то на 1 ядре можно запустить и 10к горутин, просто планировщик будет отправлять запрос, переключаться на другую горутину, пока от 1 горутины не придет ответ и так для всех

    • @ffonord
      @ffonord Рік тому

      ​@@thisisit7267 по моему ты забыл приложить под видео ссылку "на сбор благодарностей" 🙃​

  • @ВладиславАн-л3х
    @ВладиславАн-л3х 2 роки тому +5

    Наконец-то я начал понимать, как работают мьютексы и WaitGroup, спасибо тебе)
    UPD. Тем не менее я не понимаю, как так быстро выполняются 1к операций если при Lock одна горутина блокирует значение, пока сама его не поменяет, а другие стоят в очереди. Потом другая блокирует, меняет, а остальные- ждут. Это представляется так, будто горутин и нет вовсе, а значение просто переписывается как в обычном цикле, только почему-то в х1000+ раз быстрее
    Хз, массу статей про горутины почитал, но не могу себе это как-то визуально представить.

    • @thisisit7267
      @thisisit7267  2 роки тому +6

      Насчет скорости выполнения, тут зависит от того, что мы сравниваем. Если сравнить параллельную обработку и непараллельную, то в этом примере параллельная очень сильно выигрывает в скорости (или мы спим 1000 наносекунд в одной горутине, или по 1 наносекунде в 1000 горутинах почти одновременно).
      Если сравнивать 2 параллельные обработки между собой, но одна из них использует mutex, то за счет того, что горутины действительно выстраиваются в очередь, это работает медленнее (writeWithoutMutex отработала быстрей, чем writeWithMutex в 5-6 раз).
      Если сделать пример без time.Sleep, то обычный цикл с инкрементом в одной горутине отработает быстрей, так как пример с 1000 горутинами будет замедятся переключениеями между горутинами)

  • @ВикторЛяшенко-ш5ш
    @ВикторЛяшенко-ш5ш 2 роки тому +1

    Стоит объяснить почему на практике лучше использовать не мьютекс, а указатель на мьютекс

  • @annonymous8220
    @annonymous8220 6 місяців тому

    я вот честно говоря не понял :( Если mutex блокирует горутину и они начинают выполнятся по очереди, то почему это отличается от обычного выполнения цикла без горутин ? :(

  • @last2636
    @last2636 2 роки тому +1

    Офигенные советы, как defer wg.Done() отдельное спасибо за это.

  • @evgen_sr
    @evgen_sr 2 роки тому

    Что правильно выбирать WaitGroup или слушать канал в SELECT? Как я понимаю в SELECT мы можем завершить выполнение горутин раньше. Например у меня есть 1000 сайтов, на которые надо сходить, я запускаю 1000 горутин который делают get запрос, и получают статус. Но для отчета хотелось бы понять, какие из 1000 сайтов не ответили. Тогда тут наверное стоит в http.Client добавить Timeout. Как бы Вы сделали?

    • @thisisit7267
      @thisisit7267  2 роки тому

      Я бы наверное создал wg с 1000 задач, и в каждой горутине через select ждал либо ответа от http запроса, либо завершения контекста с таймаутом (в случае с http запросом это таймаут и он внутри сам вернет ошибку ). И при получении ответа/окончания таймаута завершал бы задачу в wg, где в случае таймаута или ошибки логировал бы это куда-нибудь. Если у нас http запрос отвалится по таймауту, то мы выйдем из горутины также быстро, если бы делали это через селект

  • @flectere
    @flectere 7 місяців тому

    Непонятно для чего в данном примере мы лочим горутины для чтения. Ведь данные там в любом случае будут в рандом порядке

  • @vitaliikaverin3120
    @vitaliikaverin3120 2 роки тому +1

    Прошел все твои ролики, надо идти дальше. Какие темы нужно изучать следующими, чтоб не прыгать между десятками каналов и не лезть в ролики, которые я пока не потяну?

    • @thisisit7267
      @thisisit7267  2 роки тому +2

      Мне еще по конкурентности осталось доделать: каналы, select, context. Далее уже зависит от направления, для веба обычно изучают пакет net и net/http. И дальше уже какие-то смежные инструменты, типо работы с бд, кэш, докер, CI, фреймворки. Вот такой roadmap есть кстати, github.com/Alikhll/golang-developer-roadmap

    • @ruauka
      @ruauka 2 роки тому

      @@thisisit7267 В качестве просьбы по web, было бы классно использовать gorilla mux. Спасибо за видео!

  • @Outcast-w8b
    @Outcast-w8b Рік тому

    А если ошибка при counter++ горутин не сможет сделать анлок и все зависнет ?

    • @flectere
      @flectere 7 місяців тому

      будет deadlock

  • @EgrNegr-chugun
    @EgrNegr-chugun Рік тому

    а что у тебя написано в первой функции перед "exit"? в исходном коде такого нет

  • @UAStriker
    @UAStriker 2 роки тому +1

    А почему бы нам с таким раскладом везде не использовать RWMutex, даже когда обновляем счетчик? Или в таком случае счетчик не будет лочиться на чтение в горутинах выше?

    • @thisisit7267
      @thisisit7267  2 роки тому +1

      Счетчик будет лочиться, но если у нас преобладает количество изменений счетчика перед количеством чтений, то производительнее будет использовать обычный mutex. Почему? Потому что RW mutex содержит в себе 2 очереди: записывающие и читающие горутины и проверять 2 очереди при записи дольше и нет смысла. Поэтому если мы только пишем, то быстрее будет обычный mutex. Если пишем и читаем примерно одинаково, или же больше читаем, то тогда есть смысл RWMutex

    • @UAStriker
      @UAStriker 2 роки тому

      @@thisisit7267 спасибо!

  • @uralkhanov1852
    @uralkhanov1852 2 роки тому

    Я правильно понимаю, что mu.Lock() блокирует обращение к именованной области памяти? А если у меня происходит изменение счетчика в разных местах одной го рутины, я везде должен лочить внутри одной го рутины? Если где-то в другом месте изменения счетчика я не поставил лок, го рутина все равно будет вставать в очередь, если область памяти залочена другой го рутиной? Наверное объяснил сумбурно) и спасибо за огромный и полезный труд

    • @thisisit7267
      @thisisit7267  2 роки тому

      mu.lock блокирует не область памяти, а меняет состояние самого мьютекса. Горутины, которые используют этот мьютекс ориентируются именно на состояние мьютекса (залочен или не залочен).
      Если в других горутинах мы не используем мьютекс вообще или используем другой мьютекс, то они ничего не будут знать о нашем первом мьютексе и будут порождать dateRace.
      Именно поэтому очень важно передавать указатель на мьютекс во все горутины, которые используют общий ресурс, чтобы они все сверялись только на состояние одного мьютекса.
      Проще говоря: параллельная запись без мьютекса - dataRace, параллельная запись с разными мьютексами - dataRace, ибо горутины становятся не в общую очередь, а в 2 разные, которые ничего не знают друг о друге

    • @thisisit7267
      @thisisit7267  2 роки тому +1

      Насчет нескольких записей в одной горутине есть 2 варианта:
      1. Лочить мьютекс на все горутину
      2. Лочить только участки с изменением счетчика
      Первый вариант хуже, ибо держит лок очень долго, я позже выпущу видос с объяснением этой проблемы
      Второй вариант лучше, ибо лочит мьютекс только во время полезной работы, но приходится лочить несколько раз. Если лень писать несколько локов подряд, можно конечно попробовать вынести в функцию и переиспользовать)

  • @OneOfWun-n2g
    @OneOfWun-n2g 2 роки тому +1

    Спасибо за видео) Такой вопрос, в последнем примере с RWMutex ты сделал Wg.Add(100) до цикла for, я решил попробовать инкрементировать этот счетчик в цикле, но получил ошибку "WaitGroup is reused before previous Wait has returned", но когда я добавил wg.Add(1) в начале каждой анонимной горутины, то все сработало корректно. Можешь подсказать, почему так происходит?

    • @thisisit7267
      @thisisit7267  2 роки тому +1

      А можно ссылку на итоговый вариант? Я у себя не смог именно эту ошибку воспроизвести.
      Предположил несколько вариантов:
      В цикле 50 итераций, но добавляются 2 горутины на каждой итерации, значит в цикле на каждой итерации нужно было делать wg.Add(2) или 2 раза wg.Add(1)
      Вариант с wg.Add внутри горутины опасный, ибо они могут не успеть себя добавить в ожидание к моменту завершения ожидания - таким образом мы просто не будем ждать эти горутины и завершим программу.
      И вот еще такое нашел: stackoverflow.com/questions/39800700/waitgroup-is-reused-before-previous-wait-has-returned Выглядит как то, о чем я говорил выше - горутины пытаются добавить себя в список ожидания после того как вызвали wait

    • @OneOfWun-n2g
      @OneOfWun-n2g 2 роки тому

      @@thisisit7267 спасибо, разобрался)

  • @dima_odya
    @dima_odya Рік тому

    Всем привет) вижу что канал забросили, но вдруг кто-то ответит))
    Автор говорит что можно запустить количество горутин столько, сколько у тебя ядер, начинаю гуглить и там пишут что можно запустить неограниченное кол-во горутин🙆‍♂
    Может кто-то пояснить?🤯

    • @ffonord
      @ffonord Рік тому

      По факту количество горутин ограниченно оперативной памятью, т.к. выделяется стек под каждую ~2кб (это было отмеченно в предыдущем видео) плюс нужно учитывать сколько дополнительной памяти будет тратиться на "работу" в горутине. Но если наштамповать, например, 100 горутин, при этом имея 8 процессоров, то код будет выполняться параллельно только в 8 "потоков", остальные 92 горутины будут в ожидании очереди, т.е. конкурентное выполнение как в схеме из предыдущего видео с одной лишь разницей, что конкуренция не на одном процессоре, а на 8ми. Тут профит будет зависить от характера работы, которая выполняется в горутинах. Например, если там выполняются блокирующие вызовы, например сетевое обращение к удалённому ресурсу, тут может быть и профит при переключении шедулером на другую горутину, но если в горутинах какие то математические вычисления, т.е. рутинная работа процессора, то профита не будет, а скорее всего будут даже потери производительности на переключение конекста между горутинами (даже учитывая что переключение контекста горутин на много "дешевле" переключения контекста тредов ОС - на переключении горутин тоже есть потери производительности).

    • @ffonord
      @ffonord Рік тому

      кстати говоря, автор канала уже ответил на подобный вопрос, прямо под Вашим (отсортируйте комментарии по новизне)

  • @jon4775
    @jon4775 4 місяці тому

    1 горутина не равно 1 ядро.

  • @ilyadruzh
    @ilyadruzh Рік тому +1

    Не дата рейс, а «гонка данных»

    • @bezborodovanton
      @bezborodovanton 9 місяців тому

      Дата рейс - это не гонка данных, это скорее гонка доступа к данным. Если вы это пишите с идеей о том, что не нужно использовать англицизмы в языке, дак тогда выкиньте из русского языка все остальные заимствованные слова. Ваше имя, например, из древнееврейского языка, а не из русского. Не Илья, а Иван xD

    • @vadimgerich9887
      @vadimgerich9887 Місяць тому

      @@bezborodovanton будем изучать язык программирования "ИДТИ" xD