Как стать автором
Обновить

Здоровая конкуренция в GO. Главное не перехитрить самого себя

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров13K
Всего голосов 34: ↑32 и ↓2+40
Комментарии21

Комментарии 21

На Java писали или на C# ? Горутины ничего общего не имеют с паттернами многопоточки из языков с серьёзными тредами. Пока горутин меньше 100000 - просто запускайте ещё одну, главное убедитесь, что она завершится так, как ожидается. Если горутин меньше 1000000, это всё ещё окей, но надо иметь в виду нагрузку на железо. Горутины и каналы нужны не для того, чтобы очевидно тяжёлое или медленное считать в несколько потоков, а для того чтобы максимально использовать то, что во всех(!) современных процессорах больше 1 ядра и самое главное - делать это легко! Все (почти) части программы исполняются на максимуме ядер, даже места, которые кажутся простыми и быстрыми, но на практике бывают бутылочным горлышком. Рантайм Go сам создаст столько серьёзных потоков в ОС, сколько имеет смысла, а ваш расчёт не учитывает того, что main - отдельная горутина и многие библиотеки активно используют многопоточность под капотом (но это окей, как я уже говорил).

Кажецца вы не поняли посыл. Я согласен со всем, чт вы сказали, но не понял с какой из моих идей вы спорите =)

Если что, я тогда поясню...

Статья о том, что есть в основном совсем немного общих случаев, когда нам нужна многопоточность.

Первый, самый основной, наверно, - это когда у вас есть несколько акторов, которые просто заставляют ваше приложение создавать новый поток выполнения. К примеру обработка HTTP запросов. Так вот эта на первый взгляд довольно простая тема, это не про эту статью.

Второй случай - это когда вам нужно в процессе какой то обработки взять да и сходить по сети в разные места или в одно, но много раз. Так вот этот случай хорошо параллелится, но тут обязательно нужно ограничение, просто бросить в чужой сервис 100 тысяч запросов - это жестоко и непрофессионально. Статья объясняет, что в таких случаях ограничивать кол-во потоков числом ядер процессора - это ошибка.

Третий случай - это локальные вычисления внутри одной машины. Добавил я эту тему сюда чисто потому, что вышло интересно. Вот тут нужно ограничивать кол-во потоков числом ядер, это факт. Попробуйте ка запустить 100 тысяч параллельных вычислений хэша. Ваш процессор потонет в переключениях контекста и вымываниях кеша. По крайней мере, мой ПК так и не смог справиться с той же самой задачей за разумный срок.

Так что ваше "если горутин меньше миллиона, то это все еще окей" - это сомнительное заявление.

или на C# ?

имя Ибрагим await вам ни о чём не говорит?

Я недавно бился с подобной задачей, делил на некоторые batch данные чтобы отдельно посчитать. В целом пришел к тому же о чем статья. Все очень красиво и похоже на некий маркетинг буллшит – wg и смотрите, многопоточность.

На деле действительно все конструкции wg – история про большую стоимость вызова и надо думать где применять. Т.е разработчик должен заранее понимать что операции которые он "паралелит" – они тяжелые и занимают время. И, безусловно, если заранее неопределенно каков массив данных входит, то надо разрабатывать объвязку которая будет от количества данных запускать только необходимое количество групп.

Еще один вариант остается – через ожидающие каналы. Для тех кто не понимает, поясню – делается условно срез куда создается определенное количество каналов с обрабатывающими гоурутинами....и далее закидываются данные в более свободный канал. В теории эта схема легкая, но сама "обвязка" для выбора "более свободного канала" довольно сложная. Я делал и такое; И могу сказать работает это чуть быстрее (в моем случае это несколько тысяч наносекунд по сравнению с wg sync) бонусом дает более плавную нагрузку т.к заранее определено сколько потоков работает в блоке. Но опять же обвязка "управления" обошлась в 200-300 строк кода.

Подскажите. что это за термин такой "более свободный канал"?

Самое главное, как вы определяете свободность канала?

Признаюсь, что отстал от жизни

Покажите, как определить в канале свободное место, пожалуйста

Из доки Golang: len(Ch)

Либо когда делаете управляющую функцию, лепить свой счетчик

точно. вот я балда. ни разу за все время разработки не видел такого и даже не было необходимости в этом

а знаете, что интересно?

а то, что пока вы делаете cap(ch) - len(ch) - там уже все поменяется и тот канал, который вы определили, как свободный, на самом деле будет уже под завязку

такова конкурентность

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

какой то не гошный подход

а то, что пока вы делаете cap(ch) - len(ch) - там уже все поменяется и тот канал, который вы определили, как свободный, на самом деле будет уже под завязку

Это становится не таким критичным если буффер указан с запасом. Где-то залетит чуть больше, но в целом кривая нагрузки будет более правильная

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

какой то не гошный подход

Если уж на то пошло там можно по всякому реализовать. Мне подошел способ с большим количеством каналов в срезе

Все таки я за то чтобы разработчик выбирал тот или иной способ согласно задаче, а не на основании "тру или не тру" подход.

В своем проекте я работаю над оптимизацией постоянно, для меня очень важна latency каждого отдельного блока. Притом координаты измерения – сотни наносекунд. И если какой-то способ не совсем тру, но даст прирост на тестах 5 наносекунд по сравнению с "тру" подходом – буду пользоваться им

Более показательный пример, подключение "Сишных" модулей в Golang...это воротит многих кто настоящий гошник, но я отлично использую этот функционал для бинарных деревьев. Бинарные деревья – узкое место в Golang и чтобы от него избавиться я их реализовал на "Сишной" библиотеки, оттестировал и получил разницу в скорости почти в 10 раз лучше.

А за счёт чего в го хуже именно деревья? Указатели и gc?

Они тормозят, а вот из-за чего - не знаю. Сам поиск слишком долгий при сравнении с тем же самом на C++

Если ваша реализация деревьев на ГО тормозит, значит вы неправильно написали =P

Вы наверно использовали ссылочные типы (указателями вощем), а надо было построить дерево на слайсе.

Ловите вот пример реализации такого дерева на ГО https://github.com/iv-menshenin/tree

Бэнчмарков не делал и с C++ не сравнивал

Спасибо, будет время и на досуге посмотрю. Вижу что есть отличия от классики, интерфейсы добавлены. Но читая по "диагонали" не нашел того места которое должно реально увеличить скорость.
Разница скорости Go vs C++ была ~10 раз на моем коде. Т.е условные 60 против 6 секунд.

Хочу еще сказать что с Golang довольно долго бьюсь за производительность того или иного. Для себя принял правду:

Что слезть с него – очень сложно. В любом случае каналы/гоурутины и прочие мелкие радости многопоточности – наркотик с которого слезть очень сложно. И даже дело не в том что это просто, а в надежности решения. Если взять один блок моей разработки и попытаться переписать на C++ то это займет очень большое количество времени. И основное время будет потрачено на борьбу с утечками памяти следующая из многопоточности

Но другая правда, что за удобство платим производительностью, притом есть вещи которые работают на космических скоростях и ничуть не уступают C++, а есть вещи типа деревьев или некоторых математических операций которые тебя вводят в некий ступор c немым вопросом: "это вообще почему?". Но опять же хочу отметить что когда я говорю о скорости, но мы говорим о цифрах в пределах наносекунд, т.е это все равно космические скорости особенно если сравнивать со стеком языков типа ноды или питона.

Пока формула успеха моя личная – там где узко, меняем на C++ модуль и не паримся. И рад что такая возможность вообще есть.

а профайлером не стали смотреть? получилась бы интересная статья. я думаю что скорее всего указатели и write barriers влияют.

Где-то залетит чуть больше, но в целом кривая нагрузки будет более правильная

КМК вы пытаетесь делать работу за планировщика. Он под капотом и так не плохо справляется с эвакуацией. Т.е. перекидывает задачи между освободившимися ядрами.

Но опять же обвязка "управления" обошлась в 200-300 строк кода.

А можно не велосипедить и взять стороннюю библиотеку, например https://github.com/alitto/pond

А потом вам нужно на пару нулей в хеше больше, и вот вы уже лезете в C вместе с CUDA.

Зачем мне нули и зачем куда?

Статья же не об этом. А о том, что если вы ограничиваете кол-во запущенных горутин, то этот как минимум должно быть осознанно и обоснованно.

Но так вот в тему. GO тоже умеет в CUDA

В статье подбирают значение, чтобы его хеш оканчивался на определенное количество нулей. Если вам нужен более конкретный хеш, где в конце не, к примеру, 5 нулей а 7, то сложность задачи возрастает экспоненциально. И вот возможностей Go уже не хватает. Следующий уровень-расчет на видеокарте и использование Cuda.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий