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

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

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Это решение не стали использовать т.к. это привело бы к формированию большой очереди на локе из запросов, при обработке которых происходит обращение к коллекции на чтение, если одновременно с этим происходит запись. Отвечая на исходный вопрос, пробовали ли — уже не помню.
Дополнительно стоит сравнить оверхед от использования ReaderWriterLockSlim по сравнению с обычным lock на Monitor, в интернетах ходят слухи, что rwlock жирнее, чтобы убедиться в целесообразно его использования вместе с Dictionary.

Статья просто отличная! Отличное сочетание примеров и объяснений. Спасибо!

Почему не использовали стримы http/2 от сервера к клиенту вместо http longpoll? очевидное же решение.

Не понятно, зачем вообще dotnet с его граблями и костылями для конкурентности, если есть голанг, в котором примитивы синхронизации надёжны, как швейцарские часы, и просты. Каков смысл писать сложный и заумный код вместо простого, решающего те же задачи?
Проблема с реализаций Task.Delay здесь опосредована от протокола. Конкретно для этой задачи мы пробовали HTTP/2 и столкнулись с таким же lock convoy'ем и некоторыми другими проблемами.

Почему не взять голанг?
примитивы синхронизации надёжны, как швейцарские часы, и просты

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

Большинство имеющегося в компании кода написано на C#, поддерживается разработчиками, которые пишут на C# и знают особенности .NET. Обосновать переход на Go с отбрасыванием имеющейся кодовой базы по причине более надёжных примитивов синхронизации — сложно, с таким же успехом можно предложить использовать C++ потому что в нём нет GC и ручное управление памятью надёжнее или Python, потому что код на нём не выглядит заумно, да и любую другую технологию по любой другой причине. В новых, изолированных проектах, используется не только .NET, но останавливать разработку уже успешно работающего проекта — большого смысла не вижу.
Скорее всего, такая уверенность останется только до первой проблемы, которая возникнет при их использовании. Нет гарантии не набажить самому или не наткнуться на неожиданное поведение в корнер-кейсе.

С недокументированными утечками CPU/памяти базовых примитивов синхронизации не сталкивался ни разу, в т.ч. на хайлоаде. Проблемы возникают постоянно, но их и порешать проще. Поскольку сам код проще, гарантии надёжнее и есть race detector из коробки
Обосновать переход на Go с отбрасыванием имеющейся кодовой базы по причине более надёжных примитивов синхронизации — сложно

Речь не идёт об отказе от работающей кодовой базы. У вас микросервисы, поэтому инжекция Го может быть абсолютно бесшовной — переносится ровно тот функционал, который есть смысл переводить на Го, и не более. C# прекрасно умеет в grpc и appache thrift, поэтому проблем с масштабируемой разработкой на одновременно C# и Go не будет.
с таким же успехом можно предложить использовать C++ потому что в нём нет GC

Да ладно) Ручная сборка мусора вместо gc — это переход к низкоуровневому программированию, в данном случае этого нет. Как и «Python, потому что код на нём не выглядит заумно» — Го отнюдь не про то, чтобы спрятать сложность за кажущейся простотой.
У вас микросервисы, поэтому инжекция Го может быть абсолютно бесшовной —

У меня в проекте есть C#, Java, Go, Rust, Dart и PHP (это мы фронтенд ещё не трогали). Очень интересно это всё поддерживать. Люди, которые говорят, что "микросервисы позволяют писать каждый микросервис на чём угодно" серьёзно, никогда этого не делали в серьёзном продакшене.


переносится ровно тот функционал, который есть смысл переводить на Го, и не более.

Нет никакого смысла переходить с .net на Go при отсутствии экспертизы. Замена более мощного языка и рантайма на менее мощный — зачем?


Да ладно) Ручная сборка мусора вместо gc — это переход к низкоуровневому программированию

Если вам очень надо, в С++ давно есть GC. То, как вы управляете памятью, не является единственным критерием "низкоуровневого" программия. Вам никто не мешает написать код на С++ нормально и тогда там не надо будет думать про управление памятью, всё почистится автоматически (RAII).

Люди, которые говорят, что «микросервисы позволяют писать каждый микросервис на чём угодно» серьёзно, никогда этого не делали в серьёзном продакшене.
Значит гугл, фейсбук, яндекс и практически весь крупный бизнес делают несерьёзный продакшен. Либо у вас какие-то свои, ни кем не признанные понятия о «серьёзном продакшене». Либо вы что-то делаете не так в разработке микросервисов на разных языках. Или может быть вы живете в танке и не использовали grpc?
Замена более мощного языка и рантайма на менее мощный — зачем?
Если только мощь яп измеряется количеством багов и костылей многопоточности, о которых поведал автор доклада, а так же сложностью развёртывания и количеством системных зависимостей. В этом go не конкурент C#

На вопрос «зачем» в контексте обсуждения доклада я выше ответил — упростить конкурентный код, сделать его менее хрупким и более масштабируемым. Но в целом причин для перехода с C# на Go много. Таких историй десятки если не сотни
Если вам очень надо, в С++ давно есть GC.
, который тащит за собой воз багов да тележку костылей, и в практических задачах не применим, как и все подобные «сборщики мусора». Ибо C++ is a language strongly optimized for liars and people who go by guesswork and ignorance
Вам никто не мешает написать код на С++ нормально и тогда там не надо будет думать про управление памятью, всё почистится автоматически (RAII).
C++ is a horrible language. It’s made more horrible by the fact that a lot of substandard programmers use it, to the point where it’s much much easier to generate total and utter crap with it. (С)
Значит гугл, фейсбук, яндекс и практически весь крупный бизнес делают несерьёзный продакшен.

Ну, все компании в мире должны работать как гугл, фейсбук и яндекс. Все компании в мире имеют точно такой же штат программистов. В какой из них, кстати, работаете вы? Если ни в какой, то продолжайте рассказывать.


Либо у вас какие-то свои, ни кем не признанные понятия о «серьёзном продакшене». Либо вы что-то делаете не так в разработке микросервисов на разных языках. Или может быть вы живете в танке и не использовали grpc?

О, очередной silverbullet. gRPC. И это у нас тоже есть. И многое другое.


Если только мощь яп измеряется количеством багов и костылей многопоточности, о которых поведал автор доклада, а так же сложностью развёртывания и количеством системных зависимостей. В этом go не конкурент C#

Нет, мощь ЯП измеряется наличием инструментов. Например, generics. Когда там Go2 с ненужными generics выходит? О каких системных зависимостях в C# вы говорите, я не знаю. Полностью кросс-платформенное, системонезависимое решение.


Кстати, если вы считаете, что я поддерживаю автора статьи с его костылями и остальным — покажите мне цитату, где я это делаю. Не можете? Не приписывайте мне мыслей из своей головы.


На вопрос «зачем» в контексте обсуждения доклада я выше ответил — упростить конкурентный код, сделать его менее хрупким и более масштабируемым. Но в целом причин для перехода с C# на Go много. Таких историй десятки если не сотни

Конкурентный код в C#, когда пишется с нуля и нормально, не сильно сложнее Go. Во многом даже проще. У меня как бы опыта на обоих языках достаточно.


который тащит за собой воз багов да тележку костылей

Это другой вопрос. Мне в С++ всегда норм и без него было, это вам зачем-то обязателен GC. К слову о. Как там в Go с плагинными GC? Есть? Можно ли свой написать?


C++ is a horrible language. It’s made more horrible by the fact that a lot of substandard programmers use it, to the point where it’s much much easier to generate total and utter crap with it. (С)

Ad hominem. Perfect discussion.


Вы — классический пример того, что когда кончаются аргументы, мы начинаем переходить на личности. Про substandard programmers я хочу напомнить, что именно для них создан Го. Именно поэтому в нём ничего нет. Потому что substandard programmer не может понять генерики. Ссылку найдёте сами.


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

Ну, все компании в мире должны работать как гугл, фейсбук и яндекс.
Так уже давно работают. Монолитные системы в прошлом, для микросервисных никакой разницы нет на каком яп написан микросервис (работающий согласно спеке) ни у кого кроме вас.
Нет, мощь ЯП измеряется наличием инструментов. Например, generics.
В том виде, в котором они существуют в C#, дженерики карго культ, т.к. усложняют понимание кода.
Когда там Go2 с ненужными generics выходит?
Не факт, что там они будут. А если и будут, то точно не в таком виде, как в в dotnet. Первоначальный драфт отклонён ввиду его полной неадекватности
если вы считаете, что я поддерживаю автора статьи с его костылями и остальным — покажите мне цитату, где я это делаю.
Автор детально описал проблемы, с которыми сталкивается типичный легаси проект на C#, в этом смысле ему респект и поддержка. Чтобы опровергнуть тезисы автора, коротких реплик с возражениями не достаточно. Опишите подробно ваши кейсы, покажите результаты бенчмарков.
Конкурентный код в C#, когда пишется с нуля и нормально, не сильно сложнее Go. Во многом даже проще. У меня как бы опыта на обоих языках достаточно.
У меня тоже достаточно, и я пришёл к противоположным выводам. И не только я — см. ссылку в предыдущем моём комментарии.
Как там в Go с плагинными GC? Есть? Можно ли свой написать?
Можно, но не нужно. Штатный покрывает все реальные кейсы.
substandard programmer не может понять генерики.
или думает, что понимает, хотя на самом деле видит лишь вершину айсберга из своего игрушечного маня-мирка
Вы — классический пример того, что когда кончаются аргументы, мы начинаем переходить на личности
А я и не переходил, всего лишь процитировал мнение Торвальдса в тему C++ и raii. Принимать его на свой счёт оснований нет, если только не испытывать эмоциональную привязанность к с++. Аргументов почему это так — достаточно более чем, нет смысла тратить время на них кмк
Про substandard programmers я хочу напомнить, что именно для них создан Го
На официальном сайте языка не сказано ничего про то, что он создан для substandard programmers. Если вы полагаете, что вам лучше знать для кого создан Go, чем его авторам, так это у вас 100% эффект Даннига-Крюгера.
вы спорите не с тезисами целиком, а только с интересующими вас кусками тезисов
Я стараюсь не слишком отклоняться от предмета — костылей и багов с многопоточностью в C# и как с ними проще бороться (перейти на Го). Если вы про то, что raii в C++ позволяет «не думать про управление памятью», так это заблуждение, о чём я вам и ответил цитатой «C++ is a language strongly optimized for liars». что не так то?
В том виде, в котором они существуют в C#, дженерики карго культ, т.к. усложняют понимание кода.

Слишком категоричное на мой взгляд заявление. Пользуемся генериками вовсю и не сказал бы что они усложняют понимание кода. В любом случае это всё очень субъективно и скорее вопрос привычки.


И в чём там должен заключаться карго-культ я если честно вообще не понимаю.


Монолитные системы в прошлом, для микросервисных никакой разницы нет на каком яп написан микросервис (работающий согласно спеке) ни у кого кроме вас.

Проблема часто в том, что если у вас каждый программист(или даже тим) пишет свои микросервисы на своём языке, то если кто-то заболеет/уволится, то очень сложно найти замену. Особенно если она нужна быстро. Поэтому на мой взгляд средней или просто не особо большой фирме логичнее иметь один язык под каждый тип задач(в идеале конечно вообще один на всё, но это практически нереально на данный момент). Второй имеет смысл в качестве "пилотного" если основной не совсем устраивает и ищется альтернатива.


Другое дело когда у тебя ИТ-концерн с тысячами программистов. Там уже совсем другие правила игры.

И в чём там должен заключаться карго-культ я если честно вообще не понимаю
В том, что ни кто не может привести пример из настоящих программ, где без них никак. На практике прекрасно заменяются кодогенерацией и реализацией под конкретный тип в 10 строчек, без оверхэда и роста mental cost. Например, почти все контейнеры из стандартной библиотеки C# успешно (а за частую и более эффективно), заменяются гошными встроенными дженерик-контейнерами — слайсом и хэшмапой. За исключением heap и sort, которые необходимы лишь в небольшом проценте оптимизированных по скорости программ, где стандартные heap и sort на интерфейсах не подходят из-за накладных расходов на вызов интерфейсных функций. Как пример. Ещё можно здесь посмотреть про проблемы в типичной дженерик реализации хэшмапы и как она решена в Го
Проблема часто в том, что если у вас каждый программист(или даже тим) пишет свои микросервисы на своём языке, то если кто-то заболеет/уволится, то очень сложно найти замену
Затраты на решение этой проблемы (в случае перехода с C# на Go ) окупятся за счёт упрощения и унификации кодовой базы и инфраструктуры одновременно — сервисы, написанные на Go, обходятся в 10 раз дешевле аналогичных сервисов на Java-подобных языках на AWS Lambda. Выводы по ссылке подтвтерждаю — для саппорта проекта на Го нужно тупо меньше человекочасов, потому и зп у гоферов в среднем выше, чем в C#
В том, что ни кто не может привести пример из настоящих программ, где без них никак.

По вашему любая функциональность, без которой можно теоретически обойтись, это автоматом "карго-культ"? Интересный подход.
Но если исходить из такой логики, то большая часть функциональности в ЯП это "карго-культ" и все должны на ассемблере программировать :)


Конечно без генериков можно обойтись. Но они заметно облегчают работу в определённых ситуациях. Другое дело что возможно ваша ситуация их не требует. Но это опять же субъективно.


Затраты на решение этой проблемы (в случае перехода с C# на Go ) окупятся за счёт упрощения и унификации кодовой базы и инфраструктуры одновременно

Извините, но это уже совсем о другом. Я нигде не утверждал что надо всем и всегда работать на C#, а не на Go или ещё каком-то другом языке.
И если вам больше нравится/подходит Go, то ЛММ с вами, пишите на нём. Или на джаве. Или на С++. Или ещё на чём-то другом.


Я всего лишь говорю что для средней фирмы не особо логично писать микросервисы на C#, Go, Java, C++ и каких-то других языках одновременно.


П.С. А зарплаты у гоферов выше чем в С# на мой взгляд немного по другой причине. И скорее это потому что среди пишущих на Go больше процент сениоров и меньше джуниоров. Так что возможно что когда язык станет распространённым, то и зарплаты выравняются.

По вашему любая функциональность, без которой можно теоретически обойтись, это автоматом «карго-культ»?
Вы проигнорировали аргументы про оверхэд и mental cost. Если не смотря на них сторонники технологии требуют её внедрения, при этом не могут внятно продемонстрировать её профит на релевантных кейсах, то вот это и есть карго культ, да.
Извините, но это уже совсем о другом
Как же оно о другом, если у вас было об удорожании разработки в следствии отсутствия должной экспертизы по Go, а у меня об удешевлении в следствии некоторых особенностей Go (которые его выгодно отличают от других языков)?
Я всего лишь говорю что для средней фирмы не особо логично писать микросервисы на C#, Go, Java, C++ и каких-то других языках одновременно.
С этим я согласен. Как и с тем, что в каждом конкретном бизнесе свой расклад и переход не всегда оправдан. Но и забивать на бонусы от удешевления разработки (как следствие перехода на Go) тоже не всегда разумно. Особенно в кейсах автора статьи, где бонусы более чем очевидны. Асинхронный код на потоках ос — скверная штука. Постепенный переход с переобучением — один из вариантов решения проблемы.
Вы проигнорировали аргументы про оверхэд и mental cost.

Да нет, не проигнорировал. Но они точно так же применимы к куче другой функциональности в куче других ЯП. И я уверен что и в Go такое найдётся. Вопрос то в том где вы именно карго-культ увидели? Или у вас какое-то свое определение данного понятия?


Как же оно о другом, если у вас было об удорожании разработки в следствии отсутствия должной экспертизы по Go, а у меня об удешевлении в следствии некоторых особенностей Go (которые его выгодно отличают от других языков)?

Вы меня наверное с кем-то спутали. Хотя я пожалуй тоже не согласен что Go это по умолчанию удешевление и упрощение разработки. Если у вас только одни микросервисы, то ещё может быть. Но мало где нужны только микросервисы и больше ничего другого. Да и микросервисы далеко не везде нужны.

Так ведь я выше пояснил своё определение карго культа применительно к программированию — технология ради технологии без внятного технического обоснования. И нет, в Go я такого не припомню.
Вы меня наверное с кем-то спутали.
А как же «если кто-то заболеет/уволится, то очень сложно найти замену»? «Сложно» == «дороже», т.е. удорожание. «если кто-то заболеет/уволится» — недостаток экспертизы.
Если у вас только одни микросервисы, то ещё может быть.
Go категорически не подходит для десктопа, фронтенда, embeded, системного программирования, клиентского геймдева, а так же для ML и вычислений с персистентными неизменяемыми структурами данных. Для всего остального — велкам
Так ведь я выше пояснил своё определение карго культа применительно к программированию — технология ради технологии без внятного технического обоснования. И нет, в Go я такого не припомню.

По вашему определению большая часть рантаймов, фреймворков и языков програмирования это тогда карго культ. Потому что технического обоснования у них особо то и нет и создали их по большому для удобства. И ваш Go тоже к ним относится.


А как же «если кто-то заболеет/уволится, то очень сложно найти замену»? «Сложно» == «дороже»

Сложнее это сложнее. Оно может коррелировать с дороже, а может и не коррелировать. Но это разные вещи.


Для всего остального — велкам

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

По вашему определению большая часть рантаймов, фреймворков и языков програмирования это тогда карго культ
У большинства технологий технологий есть коммерческое обоснование, с которым бесполезно спорить. У «сделайте дженерики в Go (в таком виде как в C#)» нет ни коммерческого ни технического. Предполагаемые удобства людей, привыкших к дженерикам в C#, породили бы массовые неудобства код ревьюеров (усложнение кода), программистов (дольше компиляция) и пользователей (оверхэд). При том что внятного объяснения, какие именно вещи в Go, кроме sort и b tree, надо параметризировать типами, предоставлено не было

Вы почему-то определили Go как "золотую середину" и что-то по умолчанию технически обоснованное. Потом понапридумывали каких-то "массовых неудобств", которые если вообще и существуют, то являются достаточно субъективными.


И потом исходя из этого делаете какие-то глобальные и кактегоричные выводы о коммерческой и технической обоснованности. При этом никаких цифр и расчётов вы не приводите.


П. С. И вообще создаётся такое впечатление что только вы обладаете каким-то тайным знанием, а все остальные недостойны и вообще ни в чём не разбираются и должны вот прямо сейчас последовать вашим советам чтобы себя спасти. Ничего не напоминает? :)

При этом никаких цифр и расчётов вы не приводите.
Привожу, так память у вас слишком короткая. Ссылку давал — сервисы, написанные на Go, обходятся в 10 раз дешевле аналогичных сервисов на Java-подобных языках на AWS Lambda. А что вам цифры-то? У вас исключительно к цифрам сводится любое техническое обоснование? Тогда можно рандомно выбрать любую технологию
создаётся такое впечатление что только вы обладаете каким-то тайным знанием
Опять таки из-за короткой памяти не желания заглянуть в гугл. Так же этим знанием обладают свитчеры с разных языков программирования на Go и ещё овер 9000 компаний. Такой секрет полишенеля. Go золотая серидина потому, что он против бесполезных абстракций и монадирующих программистов. Подробности тут.
Привожу, так память у вас слишком короткая. Ссылку давал — сервисы, написанные на Go, обходятся в 10 раз дешевле аналогичных сервисов на Java-подобных языках на AWS Lambda.

И это всё что у вас есть? Сравнение с java на AWS? Вам самим то не смешно? А давайте сравним его с C++ в embedded и сделаем вывод что Go никуда не годен :)


У вас исключительно к цифрам сводится любое техническое обоснование?

Вы там что-то ещё про комерческое обоснование говорили? Или нет?
Вот давайте простой пример: у нас где-то 100-150 программистов пишуших на С#. Из них может быть десяток-два немного умеют в Go. За последние 15 лет они написали кучу кода. У нас десктоп, мобайл, веб и сервисы. Всё на C#. В клауд нам нельзя и всё на своих серверах.
Сколько нам по вашему примерно будет стоить перейти на Go и когда это окупится для фирмы?


У вас исключительно к цифрам сводится любое техническое обоснование?

А как вы хотите? Чтобы в вашу "коммерческую и техническую обоснованность Go" вам на слово верили? :)


Так же этим знанием обладают свитчеры с разных языков программирования на Go и ещё овер 9000 компаний.

А сколько компаний пишут на Java? Сколько на C#? Что, ти 9000 компаний должны доказывать?


Go золотая серидина потому, что он против бесполезных абстракций и монадирующих программистов.

Правда? А как вы определяете какая абстракция полезная, а какая нет? Вот давайте возьмём банальное наследование. Разве оно не попадает под ваше определение "бесполезности" :


Предполагаемые удобства людей, привыкших к наследованию в Go, порождают массовые неудобства код ревьюеров (усложнение кода), программистов (дольше компиляция) и пользователей (оверхэд).

Так давайте тогда наследование тоже уберём. И кучу других вещей вместе с ним. И будем на ассемблере программировать. В нём ничего бесполезного нет.

Сравнение с java на AWS?
aws по той причине, что для него проще получить статистику. Никакой принципиальной разницы с обычными серверами в данном случае не вижу в упор.

java, C# — какая разница-то? Оба языка похожи. Там ещё котлин есть, с чего бы в C# разработка была дешевле чем на колине? из-за того, что у вас на фирме много десктопных C# программистов?
А как вы хотите? Чтобы в вашу «коммерческую и техническую обоснованность Go» вам на слово верили? :)
Про «коммерческую обоснованность Go» у меня не было, только про техническую. С помощью цифр можно обосновать тактические решения, но не стратегические. Ни кто не выбирает C# из других яп с помощью калькулятора и эксель (или вы выбирали?).
Вот давайте простой пример: у нас где-то 100-150… Сколько нам по вашему примерно будет стоить перейти на Go и когда это окупится для фирмы?
А вы точно уверены, что я предлагал перевести «десктоп, мобайл, веб» на go? Что до сервисов, так тут всё просто. У нас была команда из 52 человек на C# (фин.организация, биллинг). Через 2 года стала 16 человек на Go, из них 5 со знанием C#. Эти параметры плюс минус подтверждают коллеги с аналогичным опытом перехода с C# на Go. Дальше можете взять калькулятор и посчитать ваши риски с параметрами вашего бизнеса.
А как вы определяете какая абстракция полезная, а какая нет?
Опыт.
Вот давайте возьмём банальное наследование. Разве оно не попадает под ваше определение «бесполезности»
Попадает.
Так давайте тогда наследование тоже уберём.
Убрали. В Go нет наследования.
И будем на ассемблере программировать. В нём ничего бесполезного нет.
доведения тезиса до абсурда — так себе контраргумент.
aws по той причине, что для него проще получить статистику. Никакой принципиальной разницы с обычными серверами в данном случае не вижу в упор.

Ну так может быть стоит сначала посмотреть повнимательнее, а потом уже заявления делать? :)


java, C# — какая разница-то?

Действительно С#, Java, Go, Kotlin — какая разница то? :)


Про «коммерческую обоснованность Go» у меня не было

А вот это что было :


Затраты на решение этой проблемы (в случае перехода с C# на Go ) окупятся за счёт упрощения и унификации кодовой базы и инфраструктуры одновременно

У большинства технологий технологий есть коммерческое обоснование, с которым бесполезно спорить. У «сделайте дженерики в Go (в таком виде как в C#)» нет ни коммерческого ни технического.

А вы точно уверены, что я предлагал перевести «десктоп, мобайл, веб» на go?

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


У нас была команда из 52 человек на C# (фин.организация, биллинг). Через 2 года стала 16 человек на Go, из них 5 со знанием C#. Эти параметры плюс минус подтверждают коллеги с аналогичным опытом перехода с C# на Go.

И что это должно доказывать? Я вам точно так же могу сказать что у нас на фирме ещё до моего прихода уволили больше половины программистов и менеджеров и набрали немного новых. И фирма спокойно работает. То есть в вашем примере возможно проблема не в языках, а в конкретных людях.


Кроме того откуда мне знать, может у вас на фирме просто работы меньше стало и вам бы теперь и 16 сишарпников хватило бы за глаза и за уши :)


Опыт

У вас один опыт, у кого-то другой. Почему вы считаете что надо слушать именно вас? Есть какие-то объективные критерии?


Убрали. В Go нет наследования.

И по вашему композиция это проще, понятнее и создаёт меньше оверхеда?


И кстати ООП целиком тогда тоже убираем? Инкапсуляцию? Полиморфизм? Интерфейсы? Классы? Где вы предлагаете остановиться и почему именно там?


доведения тезиса до абсурда — так себе контраргумент.

Этим я показываю абсурдность вашего аргумента в том виде как вы его презентируете.

Это хорошо, что в Go есть встроенные слайсы и хешмапа (а ещё каналы). Осталось добавить встроенные же аналоги Observable, деревьев и ещё кучи не так повсеместно, но всё же часто используемых примитивов.


А потом грустно посмотреть на выросшую спеку языка и сказать: "лучше бы мы дженерики добавили — было бы понятнее".

Observable не нужен. Ни в одном успешном проекте на Go не встречал (разве что у новичков, пришедших из .net, в наивных попытках перетащить привычные костыли из C#). В сообществе «реактивное программирование» в Go осуждается, а pub-sub считается антипаттерном

Деревья тоже не нужны за редким исключением. Нет смысла пихать маргинальные структуры данных в стандартную библиотеку, third-party достаточно
Ни в одном успешном проекте на Go не встречал

И не встретите, поскольку язык для подобного просто не приспособлен. Все, кому нужны Observable, пишут на других языках.


third-party достаточно

Да-да:


Values() []interface{}

Очень, наверное, удобно таким third-party деревом пользоваться.

Все, кому нужны Observable
Вы имеете ввиду — все ментальные маструбаторы вприсядку и любители бесполезных абстракций из допотопно-десктопного программирования? И вы конечно можете показать многопоточный Observable в серьёзном проекте, не так ли?
Очень, наверное, удобно таким third-party деревом пользоваться.
Учитывая насколько редко нужны rb tree, совершенно наплевать на удобства. Ну так и типизированные rb tree тоже есть github.com/ncw/gotemplate
На практике прекрасно заменяются кодогенерацией и реализацией под конкретный тип в 10 строчек, без оверхэда и роста mental cost.

Т.е. копипаста — это нормально, я понял. Это всё, что надо знать про язык го.

В микросервисах в принципе копипаста достаточно частое явление. Особенности архитектуры так сказать.
Да и вообще микросервисы достаточно специальная "вещь" и именно для них Go наверное действительно лучше подходит чем C#.


Вот только и область применения у микросервисов тоже относительно ограниченая. И в мире существуют не только микросервисы и монолиты :)

В микросервисах в принципе копипаста достаточно частое явление. Особенности архитектуры так сказать

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


Да и вообще микросервисы достаточно специальная "вещь" и именно для них Go наверное действительно лучше подходит чем C#.

Я не вижу проблемы написать микросервис на C#. Более того, мы это успешно делали и запускали их в продакшен. Отлично работает. Кода столько же, если не меньше.

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

Мы как бы тоже решили пойти по этому пути. Но всё пихать в пакеты тоже имеет свою проблематику. Да и не всё и не всегда в пакеты можно запихать.


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


Я не вижу проблемы написать микросервис на C#. Более того, мы это успешно делали и запускали их в продакшен. Отлично работает. Кода столько же, если не меньше.

Проблемы я тоже не вижу. А вот что там действительно оптимальнее это совсем другой вопрос. Я в Go совсем не эксперт, так только для себя пытался баловаться. Но вряд ли бы он смог "взлететь" если бы он был хуже С#.

Го не хуже, го — другой. Кому-то его хватает, удачи им. Мне его мало, мне мало гошного рантайма, поэтому я им не пользуюсь.


Го очень прост, любой мидл его учит за 4 дня, поэтому он и взлетел. И в этом же его ограничение, это язык для вечных мидлов с какой-то стороны.


Безусловно, есть сильные программисты на го, но они обычно приводят другие, вменяемые аргументы в пользу го.


Ну и многие из них (кого я знаю) хотят генериков.

Не факт, что там они будут. [...] Первоначальный драфт отклонён ввиду его полной неадекватности

Ну так это ж недостаток языка, а не его достоинство.

Я стараюсь не слишком отклоняться от предмета — костылей и багов с многопоточностью в C# и как с ними проще бороться (перейти на Го). Если вы про то, что raii в C++ позволяет «не думать про управление памятью», так это заблуждение, о чём я вам и ответил цитатой «C++ is a language strongly optimized for liars». что не так то?

Я про то, что вы взяли мой тезис:


Нет никакого смысла переходить с .net на Go при отсутствии экспертизы. Замена более мощного языка и рантайма на менее мощный — зачем?

Изменили его на


Замена более мощного языка и рантайма на менее мощный — зачем?

И начали с ним спорить. Т.е. вы взяли, придумали тезис, приписали его мне и побежали спорить с этим. Это классический полемический приём в случае отсутствия аргументов.


Автор детально описал проблемы, с которыми сталкивается типичный легаси проект на C#, в этом смысле ему респект и поддержка. Чтобы опровергнуть тезисы автора, коротких реплик с возражениями не достаточно. Опишите подробно ваши кейсы, покажите результаты бенчмарков.

"Типичный" — сколько проектов в вашей выборке?

ну и кстати в Го нет такой штуки, как lock convoy. Можно совершенно спокойно писать в канал из миллиона горутин, ни какой утечки процессора при этом не будет. Точно так же можно безопасно одновременно захватывать мьютекс из миллиона горутин (правда, освобождение чуть более дороге)
ну и кстати в Го нет такой штуки, как lock convoy.

Что-то я очень сильно в этом сомневаюсь. Внутри у go все те же примитивы синхронизации, чудес не бывает.
В прмитивы те же лишь по функционалу, а работают абсолютно по другому. «Потоки» другие, и асинхроный под капотом рантайм по другому распределяет между ними «кванты производительности». Когда много горутин («зелёных потоков») пытаются получить доступ к защищённому блокировкой ресурсу, ни какого переключения контекста (с переходом CPU в режим ядра) не происходит. Соответственно не происходит и ухудшения производительности

Совсем никакого переключения контекста может не происходить только в одном случае — когда используется только один нативный поток. Но на многоядерных процессорах это неоптимально.


А как только нативных потоков становится много — становится возможной и ситуация, когда сразу 16 потоков хотят прочитать из канала или получить мьютекс. И все равно всё заканчивается спин-блокировкой с фьютексом.


В итоге всё фундаментальное отличие Go от .NET — в том, что в Go используются stackfull coroutines, а в .NET — stackless. Но на lock convoy это не влияет.

Суммарная сложность такого «переключения» настолько мала в сравнении с dotnet, что на практике не заметна. В Go — О[количество ядер], тогда как в dotnet — O[размер тредпулла]. Например, миллион горутин, одновременно пишущих в канал, никак не влияют на работу старого ноута с I5. Само «О» опять таки в Go значительно меньше, поскольку стек горутины не 1 мб, как у треда ос, а 16 кб.

Тредпул также не будет сильно расти, если не делать блокирующих вызовов (у меня на синтетическом тесте только что получилось 12 потоков на 8 лог. процессорах). А расходы памяти на Task в дотнете ещё меньше чем на стек горутины в Go.

Тредпул также не будет сильно расти, если не делать блокирующих вызовов
Так и в Go можно с таким же успехом использовать lock free алторитмы. Мы же обсуждаем именно очередь на блокировку. Которая в C# — огромная проблема, к тому же возникает внезапно и в самых неожиданных местах (вследствие общей забагованности dotnet), о чём автор поведал. А в Go — не проблема.
А расходы памяти на Task в дотнете ещё меньше чем на стек горутины в Go.
Абсолютно наплевать. Какое отношение может иметь оверхэд асинхронной операции к стеку треда ос и цене context switches?
Так и в Go можно с таким же успехом использовать lock free алторитмы.

Можно. Но исходно-то утверждалось, что в Go достаточно использовать стандартные примитивы. А они не lock free.


Какое отношение может иметь оверхэд асинхронной операции к стеку треда ос и цене context switches?

Самое прямое. Там, где в Go у вас выполняется N горотин, в C# будет NK тасков (где K — средняя глубина асинхронного стека). А нативных потоков много не будет ни там, ни там.

Вы не с тем спорите. В этой ветке речь о том, что в Go нет lock convoy, о которой рассказал автор. По простой причине — нет утечки системных потоков из пула.

Я ни где не утверждал что в Go примитивы синхронизации lock free, или что они серебряная пуля, с чего вы это взяли? Я утверждал, что они надёжны и просты в сравнении с аналогами из C#. Вам пояснить, что такое примитивы синхронизации?
Там, где в Go у вас выполняется N горотин, в C# будет NK тасков (где K — средняя глубина асинхронного стека). А нативных потоков много не будет ни там, ни там.
Пойнт, который вы упустили — в C# при lock convoy нативных потоков таки будет много, вплоть до полного исчерпания асинхронного пула. О чём и шла речь у автора. И сравнивать таски C# с зелёными потоками Го абсолютно не корректно. Потому что горутины можно безопасно блокировать без риска утечки CPU, а таски — нет.
Мы же обсуждаем именно очередь на блокировку. Которая в C# — огромная проблема, к тому же возникает внезапно и в самых неожиданных местах (вследствие общей забагованности dotnet), о чём автор поведал. А в Go — не проблема.


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

Давайте посмотрим, что будет с Go если добавлять таймер на каждый запрос. Открываем time.go и видим
func addtimer(t *timer) {
	tb := t.assignBucket()
	lock(&tb.lock)
	ok := tb.addtimerLocked(t)
	unlock(&tb.lock)
	if !ok {
		badTimer()
	}
}

Как думаете, что будет с Go если вызвать addtimer 10000 раз в секунду из разных goroutine?
Я это называю граблями. И ничего из описанного автором в Go не встречал.
Давайте посмотрим, что будет с Go если добавлять таймер на каждый запрос.
Ничего не будет.
Открываем time.go и видим
Не могли бы вы перейти к сути без наводящих вопросов? Я не знаю, что видите вы. Я вижу набор загадочных не импортируемых имён. Разбираться в том, что такое в этом коде *timer, lock и unlock, пока что желания нет.
В dotnet при создании таймер берется лок, чтобы добавить таймер в очередь таймеров. Если таймеров создается много, то потоки, которые создают таймеры выстраиваются в очередь на этом локе и система начинаем тормозить. Это то, с чем столкнулся автор.

В Go также при создании таймера берется лок, чтобы добавить его в очередь таймеров, что вы можете увидеть, если посмотрите на реализацию таймеров в Go, ссылку я указал выше. То есть и в dotnet и в go происходит одно и то же, и результат будет одним и тем же — система будет «тормозить», выстраиваться в очередь на локе.
Если таймеров создается много, то потоки, которые создают таймеры выстраиваются в очередь на этом локе и система начинаем тормозить
В C# начинает, в Go не начинает. Я три раза объяснил почему так и привёл пример кода. Казалось бы что ещё, ан нет
В Go также при создании таймера берется лок, чтобы добавить его в очередь таймеров
О ужас, блокировка! какой кошмар!
Вообще то Go все операции блокирующие и все операторы синхронные за исключением select и go. И весь пользовательский код блокирующий, включая бизнес логику. И работает это всё мега предсказуемо и мега надёжно. Вы вообще понимаете, что блокирование вычислений в таске дорогое потому, что при этом лочится целый тред ос? А при блокировке горутины в Го тот тред ОС, в котором она выполнялась, переиспользуется рантаймом для выполнения других горутин? Или это настолько сложно для вас, что мы продолжим спор об очевидном?
Вы вообще понимаете, что блокирование вычислений в таске дорогое потому, что при этом лочится целый тред ос?

Конечно я это понимаю.

А при блокировке горутины в Го тот тред ОС, в котором она выполнялась, переиспользуется рантаймом для выполнения других горутин?

А вот это не верно или не всегда верно, пример я привел выше. Вот еще несколько выдержек из исходного кода рантайма Go, сначала timer.go
type timersBucket struct {
	lock         mutex  //обратим внимание, в timersBucket  есть что-то типа mutex
...
}
...
lock(&tb.lock) //tb это timersBucket, вызывается функция lock, в которую передается объект типа mutex.
if !tb.addtimerLocked(t) {
    unlock(&tb.lock)
    badTimer()
}
...


А теперь представим ситуацию, в go 4 потока (потому что 4 ядра) и тысячи goroutine, которые добавляют таймеры. Вопрос — сколько goroutine выполняется одновременно? Ответ — в худшем случае 1, остальные висят в lock(&tb.lock). Прежде чем вы начнете опровергать, посмотрите на runtime2.go и эту выдержку из него
// Mutual exclusion locks.  In the uncontended case,
// as fast as spin locks (just a few user-level instructions),
// but on the contention path they sleep in the kernel.
// A zeroed Mutex is unlocked (no need to initialize each lock).
type mutex struct {
	// Futex-based impl treats it as uint32 key,
	// while sema-based impl as M* waitm.
	// Used to be a union, but unions break precise GC.
	key uintptr
}

Выделю отдельно вот этот кусок — but on the contention path they sleep in the kernel

Повторю свой тезис — чудес не бывает, рантайм go также как и рантайм dotnet, можно сломать если делать «странные» вещи и не понимать как они работают. Можно, например, устроить себе lock contention на мьютексах на создание таймеров.
пример я привел выше
Не привели. Вы показали фрагмент закрытого кода из стандартной библиотеки и привели свои выводы о его работе без объяснений. Пример — это работающий код на Go. Я вам привел пример, в котором, как вы хотели, каждую секунду (с.12) запускается 10000 (с.14) таймеров. Любой желающий может убедится, что никакого лок коновоя со 100% загрузкой CPU он не вызывает. Таймер запускается в с.17. с.18 гарантирует его завершение.

Это же касается и второго примера. Мютекс в Go реализован в совершенно другом пакете. Видите, там нет ничего про «but on the contention path they sleep in the kernel». И лочится он абсолютно по другому нежели чем блокировки в dotnet Покажите пользовательский сценарий с «lock contention на мьютексах на создание таймеров», до тех пор это не более чем догадки
Утверждение 1 — если много потоков конкурируют за один и тот-же примитив синхронизации и делают spin wait, то нагрузка на CPU возрастает и в пределе достигает 100%

Утверждение 2 — и dotnet и go используют spin wait в своей стандартной библиотеке, при неправильном использовании можно получить 100% использование CPU в силу утверждения 1.

С чем из вышеперечисленного вы не согласны?

Касательно вашего комментария
— Я привел пример открытого кода из рантайма go, это не «закрытый» код.
— Как этот код работает должно быть очевидно из исходников и комментариев в них
— Я не хотел никаких примеров, вы сделали синтетический тест, который добавляет 10 000 таймеров в секунду.
— Вы нашли Mutex, а в таймерах mutex. Если посмотреть как работает Mutex, то там видно, что он делает все те же spin lock, а затем блокируется на семафоре.
— Таймеры я привел как один из примеров, где go делает spin wait и блокирует поток выполнения. Таких мест в go достаточно, чтобы в «умелых» руках наступить на эти грабли.

Непонято, чего вы спорите, как написано в статье, конкретно с таймерами проблема исправлена, хоть в 4.8 по умолчанию используется старый вариант, я не смог воспроизвести. Если что, мне это интересно чисто в академических целях, ну чтобы лучше понимать как вообще всё это работает.
Из описания проблемы понятно, что она существует только когда много потоков одновременно пытаются блокировать один ресурс. Но не написали какое количество потоков приводит к этой проблеме, интересен хотя бы порядок.
Тредпул .NET по умолчанию ограничен очень большим числом потоков (на моей тачке 32767), но в реале по умолчанию использует количество потоков равное количеству ядер. При этом если потоки блокируются, то похоже рантайм это отслеживает и увеличивает тредпул автоматически (и уменьшает кстати тоже). И тут вопрос — где описано это поведение? И второй вопрос — можно ведь сразу ограничить размер тредпула количеством ядер, и тогда, как мне кажется, можно избежать этой проблемы?


ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads);
ThreadPool.SetMaxThreads(Environment.ProcessorCount, completionPortThreads);

Эта идея с автоматическим увеличением тредпула наверное хороша для какого-то легаси. Но кажется для новых проектов, особенно на net core, было бы разумно сразу ограничивать размер тредпула каким-то разумным числом, не сильно большим чем число ядер.

Порядок потоков, чтобы система зависла на спинлоке в мониторе должен быть тысячи.


Спорю я с утверждением о том, что в go нельзя устроить contended lock, потому что go что-то делает не так, как dotnet и потому в go таких проблем быть не может в принципе. Моя позиция — внутри что у dotnet, что у go все те же спинлоки, семафоры и мьютексы и все проблемы с ними связанные могут вылезать в самых неожиданных местах.

Хороший пример кстати, я увеличил количество таймеров до 100к, у меня получилось что GO расходует раза в 2 больше процессора и раз в 10 больше памяти.

Не согласен с 2 в отношении Go. Если ОЧЕНЬ много горутин конкурируют за один и тот-же примитив синхронизации из стандартной библиотеки Go, то скорее исчерпается память из-за оверхэда на функционирование горутины (мизерного), чем нагрузка на CPU вырастет до 100%. Причины объяснял несколько раз, повторяться не хотелось бы.
Я привел пример открытого кода из рантайма go, это не «закрытый» код.
Закрытые в том смысле, что там все имена начинаются с символа в нижнем регистре, а следовательно не могут быть использованы вне пакета runtime. Ни из самого кода, ни из комментариев нельзя сделать вывод, каким образом и для чего этот код вызывается из пользовательского кода. Я просил вас привести пример пользовательского кода, в котором addtimer по вашему мнению вызывается не правильно и загружает CPU на 100%. Вы не привели.
Вы нашли Mutex, а в таймерах mutex
В таймерах Go нет ни Mutex, ни mutex. И вообще этот код к таймерам Go, используемым в прикладном коде гоферами, ни малейшего отношения не имеет. А имеет к реализации рантайма. Не понятно, что вы хотели доказать на примере addtimer. Ну блокирует она системный тред, что с того? Есть ещё паблик функция runtime.LockOSThread, она блокирует явно, в чём крамола то?
Я не хотел никаких примеров, вы сделали синтетический тест, который добавляет 10 000 таймеров в секунду.
Этот код я написал, чтобы снять вопрос, что якобы при создании таймера Go может произойти блокировка потока ОС. Это не правда.
Таймеры я привел как один из примеров, где go делает spin wait и блокирует поток выполнения. Таких мест в go достаточно, чтобы в «умелых» руках наступить на эти грабли.
Ну так это не правда. Go блокирует горутину, поток ОС при этом не блокируется, и другие горутины могут спокойно продолжать свою работу. Это основной принцип CSP модели праллелизма в Go.

Утверждение 2 — и dotnet и go используют spin wait в своей стандартной библиотеке, при неправильном использовании можно получить 100% использование CPU в силу утверждения 1.


Не согласен с 2 в отношении Go. Если ОЧЕНЬ много горутин конкурируют за один и тот-же примитив синхронизации из стандартной библиотеки Go, то скорее исчерпается память из-за оверхэда на функционирование горутины (мизерного), чем нагрузка на CPU вырастет до 100%.


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

Мне такие не известны, лучшее что я знаю, это комбинация spin lock + мьютексподобный примитив синхронизации ядра ОС. Но, из за spin lock, при высокой конкуренции на такой ресурс возрастает нагрузка на CPU.

Обратите еще внимание на вот это сообщение. 21 день назад в трекере go появилось подробное описание высокого потребления CPU при активном использовании таймеров. Вот цитата из сообщения
I decided to use pprof to figure out what's tying up all of this CPU time. It turned out to be getting tied up in runtime.futex,


Проблема spkaeros лишь подтверждает мое утверждение 2 — в рантайме go есть локи и они могут приводить к высокому потреблению CPU. В обсуждении на github тут же предложили решение, аналогичное тому, что я предлагал автору статьи в одном из комментариев. Лишняя демонстрация того, что одинаковые проблемы имеют одинаковые решения вне зависимости от языка и рантайма.
Я с вами соглашусь, если вы предложите механизм синхронизации, который бы обладал такими свойствами, а именно
Хоар уже давно предложил, а Пайк взял и сделал в Го.
Проблема spkaeros лишь подтверждает мое утверждение 2 — в рантайме go есть локи и они могут приводить к высокому потреблению CPU.
Про блокировки в рантайме там речи не идёт. Проблема юзера spkaeros в утечке ресурсов из-за того, что он забыл вызвать ticker.Stop() при выходе из хэндлера. Вполне штатная ситуация и нормальные, рабочие грабли. С таким же успехом можно забыть вызвать mutex.Unlock() и потом искать проблему в рантайме Go. Утёкшие незакрытые мириады тикеров постоянно вызывают runtime.futex, который содержит в себе сисколы. Сисколы в Go значительно дороже чем в dotnet, примерно на порядок. Вот ему pprof и показал runtime.futex. А вовсе не из-за того что «рантайме go есть локи».

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

Кажется вы не понимаете о чем говорите. В dotnet тоже есть green threads и можно иметь миллион «goroutine» только называться это будет Task и async методы. Отличия мельчайшие, на производительность сильно они не влияют. Также как и в Go, dotnet мапит N потоков на M асинхронных задач. Этим N можно управлять, обычно этот N примерно равен количеству ядер.

Операция lock в dotnet это spin lock чаще всего, стоимость этой операции сопоставима с делением long на long (меньше 50 ns), при условии, что за лок нет большой конкуренции (lock convoy, который мне хочется назвать lock contention). Конкуренции за лок большой не будет если все делать по уму.

Проблема в dotnet только от того, что там возможностей больше и люди ими пользуются не понимая до конца, что они делают. А затем приходится изобретать странные вещи. Но это не проблема dotnet, загляните в Go через 5 лет и вы удивитесь, как там все станет непросто и сколько появится способов отстрелить себе ногу.
В dotnet тоже есть green threads и можно иметь миллион «goroutine» только называться это будет Task и async методы.
Должен констатировать — вы и близко не представляете, чем параллельные вычисления в Go отличаются от асинхронных в C#. А различия кардинальные абсолютно во всём — как под капотом, так и снаружи. Тот факт, что тасков можно насоздавать много, ни каким образом не приравнивает их к горутинам.
Конкуренции за лок большой не будет если все делать по уму.
Чтобы сделать конкурентный код «по уму» на C# нужно приложить во сто крат больше усилий, чем на Го. В Го умственные усилия программиста расходуются на решение задач бизенса, а не секс с многопоточность, как в C#
Проблема в dotnet только от того, что там возможностей больше и люди ими пользуются не понимая до конца, что они делают
Так ведь нет. Проблема дотнэт в том, что асинхронный код на системном тредпуле 1) уродливый и сложный 2) глючный. Один только lock convoy чего стоит

А чем, собственно, таски отличаются от goroutine? И там и там N потоков выполняет M «задач», M >> N, если заблокировать поток, то будут проблемы. Так как память разделяется между всеми «задачами», то нужно иногда брать локи и останавливать потоки.


В чем я ошибаюсь относительно go?

Ну чем асинхронность отличается от параллелизма, вы можете сами посмотреть в источниках. Это, кстати, сложный вопрос и ни разу не стыдно его не знать.
Применительно к C# vs Go навскидку.

1.
В C# затраты непосредственно на вызов асинхронного метода могут быть в десять раз больше затрат на вызов синхронного метода. Учитывая количество await-ов это может стать проблемой (и становится). В Go ничего подобного нет, оператор go практически бесплатная штука

2.
В Go заблокированная горутина не блокирует поток ОС (если только очень сильно этого захотите и сделаете в явном виде). Соответственно в Go можно безопасно сделать изменяемое состояние, разделяемая хоть между всеми существующими горутинами.

в C# же любая блокировка (что в прикладном коде, что в системном) внутри асинк. таска лочит поток ОС и нивелирует все преимущества асинхронного программирования превращая его в синхронное с костылями.

3.
var (
    sharedMutableState SharedMutableState
    mu sync.Mutex
)
func UseSharedMutableState(){
    mu.Lock()
    sharedMutableState.use()
    mu.Unlock()
}

Этого кода достаточно, чтобы спокойно вызывать UseSharedMutableState() из любого места программы на Go.

В C# и программист и кодревьюер обязан постоянно держать в голове все возможные сценарии доступа к sahred mutable state чтобы не получить lock convoy c исчерпанием трепулла. Либо тщательно бдить, чтобы доступ к разделяемым ресурсам был строго последовательным. Но помогает это исключительно в тривиальных кейсах. А на большой кодовой базе при наличии в коде sahred mutable state практически гарантированы различные вариации lock contention. Либо дэдлоков, которые по крайней мере в netcore в разы сложнее отыскать, чем в Go

4.
Горутины в Go можно спокойно запускать из любого места программы. Гоферу не надо задумываться о том, как работает логический параллелизм. В C# вы не можете просто взять и запустить асинхронно длительное синхронное вычисление из асинхронного таска. Вам нужно переписать существующий синхронный код в асинхронный со всеми вытекающими. Что конечно же дичь, куча лишних букв в коде и головная боль

5.
Плюс ещё пара экранов текста с перечислением всех багов и глюков асинхронщины в стиле «не возбуждать исключения в async-void методах, потому что...»

Ну чем асинхронность отличается от параллелизма, вы можете сами посмотреть в источниках.


С теорией я знаком, различия в терминологии мне известны. Если что-то мапит M задач на N ядер, то мне плевать называется ли это «асинхронщиной», «многозадачностью», «параллельностью» и т.п. Это все еще мапинг M задач на N ядер. И dotnet и go делают этот самый маппинг. dotnet, кроме всего, умеет подождать на асинхронном IO (IO completion ports, epoll, kqueue, selec, etc) и при появлении данных добавить в очередь на выполнение задачу. Это то, что вы называете «асинхронностью» и это дополнительная фишка к тому самому маппингу. Асинхронность не обсуждаем, ее в go вроде как нет. А вот распределение задач по ядрам и организация их взаимодействия есть и там и там. Вот ее давайте обсуждать.

А различия кардинальные абсолютно во всём — как под капотом, так и снаружи.


Так а что под капотом у go, разве там не тредпул с очередями и шедулером? Может там шедулер какой-то «прорывной», такой что никто до go не додумался сделать?

В C# затраты непосредственно на вызов асинхронного метода могут быть в десять раз больше затрат на вызов синхронного метода. Учитывая количество await-ов это может стать проблемой (и становится). В Go ничего подобного нет, оператор go практически бесплатная штука


Это как это? С чего бы это добавление таски в очередь на выполнение в одном рантайме сложнее на порядок чем в другом?

В Go заблокированная горутина не блокирует поток ОС


Есть SemaphoreSlim с его WaitAsync и аналоги. Работает, абсолютно также как Mutex в go — как только другая таска освободит семафор заблокированные задачи будут добавлены в очередь на выполнение.

В go есть вещи, которые сделаны лучше чем в dotnet и наоборот, мы не об этом вообще говорим. В этой ветке мы говорим о том, чем существенно реализация goroutine отличается от Task. Цель — показать что goroutine значительно более продвинутая технология, которая выполняет все, что может Task, но делает это значительно лучше. А именно, тратит меньше памяти, имеет меньшие накладные расходы, быстрее работает и т.п.

Из нашего обсуждения я пока вижу что
— Для связки горутины с каналами есть удобный синтаксис. В dotnet больше вариантов, нужно выбирать и специального синтаксиса нет.
— Примитивы синхронизации по умолчанию знают про горутины, в dotnet нужно выбирать правильный примитив в зависимости от ситуации
— В Го за это приходится платить сложной интеграцией с нативным кодом. В dotnet вызов C методов почти бесплатный, в Go это целая церемония, в первую очередь из-за шедулера goroutine.

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

Асинхронность не обсуждаем, ее в go вроде как нет
Есть. Можно сказать, что await в Go содержится почти в каждой инструкции. В Go большая часть инструкций на самом деле асинхронна и под капотом работает event loop. Но гоферу про это ничего знать не нужно (не очень то и хотелось).
Может там шедулер какой-то «прорывной», такой что никто до go не додумался сделать?
Вообще то шедулер в рантайме Го работает по другому нежели чем в C# — он не переключает потоки, а «вытаскивает» инструкции из очереди на выполнение и передаёт их потоку горутины. Это действительно продвинутая технология.
Это как это? С чего бы это добавление таски в очередь на выполнение в одном рантайме сложнее на порядок чем в другом?
Из-за SynchronizationContext, в который должен свалиться асинхронный вызов. Я об этом читал в книге Алекса Дэвиса и лично наблюдал на практике в C# разницу в 2 порядка.
Есть SemaphoreSlim с его WaitAsync и аналоги. Работает, абсолютно также как Mutex в go
Вы хитрите сейчас. Не могли бы вы показать пример синхронизации, скажем, твердотельного кеша бд или чего-то аналогичного из практики с помощью SemaphoreSlim? Где несколько тасков одновременно обновляют стейт, взятый из бд. Не покажете, вопрос был риторический. Для предотвращения гонок данных только двоичный семафор подходит.
Для связки горутины с каналами есть удобный синтаксис
Что вы имеете ввиду? Нет в go ни какой связки каналов с горутинами. Есть операторы чтения и записи в канал, и всё.
Примитивы синхронизации по умолчанию знают про горутины
Тот же вопрос. Что именно мьютексы «знают» про горутины?
В Го за это приходится платить сложной интеграцией с нативным кодом.
В Go осуждается интеграция прикладного кода с нативным. Ни кому это не нужно.
В dotnet вызов C методов почти бесплатный, в Go это целая церемония, в первую очередь из-за шедулера goroutine.
Не верное утверждение. Вызов «сишного» кода из Go по производительности бесплатный, его цена — отсутствие кроскомпиляции. Например, использование sqlite в Go будет в точности таким же по производительности как в C# (при этом на много меньше бойлерплейта, другой вопрос). Из-за шедулера дорогими будут системные вызовы. Как правило на это совершенно наплевать.
Из-за SynchronizationContext, в который должен свалиться асинхронный вызов.

Осталось понять откуда возьмётся SynchronizationContext где-то кроме GUI-приложения.


Вызов «сишного» кода из Go по производительности бесплатный, его цена — отсутствие кроскомпиляции.

Ничего подобного, его цена — возможная блокировка потока. Сишный-то код ничего не знает ни про горутины, ни про сегментированный стек.


В Go осуждается интеграция прикладного кода с нативным. Ни кому это не нужно.

Ну да, если работает плохо — значит никому не нужно. Наверное, потому что кому это нужно — те на Go не пишут.

В asp net есть SynchronizationContext. Но есть ещё причины деградации производительности на асинхронные вызовы, в данный момент я их не помню.
Ничего подобного, его цена — возможная блокировка потока
Обычно те, кто используют cgo, знают что делают. Ну и на практике не используется ничего сишного, что может блокировать поток. У меня в коде такого нет точно, у коллег тоже не встречал. Разве что для весьма не стандартных задач, выходящих далеко за рамки нормального применения Go.
Наверное, потому что кому это нужно — те на Go не пишут
Это нужно в основном в системном программировании и gui. Иногда и на Го пишут. Если это не будут узким местом, то почему бы и нет. Но такая необходимость возникает редко, в стандартной библиотеке все необходимое уже есть
В ASP.NET нет SynchronisationContext, он там не нужен. Если бы был, то был бы быстрым, потому что не нужно дергать WinAPI. Медленный SynchronisationContext в GUI приложениях, не потому что dotnet, а потому что WinAPI.

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

Вызов «сишного» кода из Go по производительности бесплатный, его цена — отсутствие кроскомпиляции. Например, использование sqlite в Go будет в точности таким же по производительности как в C# (при этом на много меньше бойлерплейта, другой вопрос).

Это неверно. Удобный, интегрированный шедулер = головная боль с вызовом кода, который про этот шедулер не знает. Чудес не бывает.
В net framework SynchronisationContext был, есть и будет, и в asp net для net framework используется он чуть более чем во всех крупных проектах, отнюдь не только в GUI. netcore не всея Руси, переписывать на него легаси будут не только лишь все (мало кто будет это делать) (проще сразу на Go всё переписать, за одно избавится от тонны хлама и бойлерплейта). Поэтому, как бы вам этого не хотелось, проблема SynchronisationContext ни куда не денется в будущем.

Но даже и без контекста синхронизации асинхронные вызовы всё равно будут дороже синхронных. Даже в том случае. когда они по счастливой случайности ничего не блокируют. Потому что таск выполняется последовательно в нескольких потоках, между которыми нужно последовательно передавать стек таска.
В силу того, как устроен шедулер в Go, он обязан вызывать любой нативный (а не системный) вызов в отдельном потоке, а затем передавать значение в горутину.
С чего вы это взяли? Ничего он не обязан и ничего подобного не делает если не вызвать LockOSThred. Если сишная функция из прикладного кода блокирует поток, то это проблема прикладного кода. В Go есть много небезопасных вещей, это одна из них. Ещё раз. Обычно те сишные функции, которые вызываются из Го, ничего не блокируют. Нет смысла усложнять и утяжелять рантайм на защиту от того, что происходит в одном кейсе на миллиард.
Удобный, интегрированный шедулер = головная боль с вызовом кода, который про этот шедулер не знает.
Ещё раз. В Go никому не нужен этот код, который может сломать рантайм. Вы пугаете несуществующим призраком
Вообще то шедулер в рантайме Го работает по другому нежели чем в C# — он не переключает потоки, а «вытаскивает» инструкции из очереди на выполнение и передаёт их потоку горутины. Это действительно продвинутая технология.

dotnet работает точно также — есть очередь задач, пул потоков обрабатывает ее. Никаких переключений потоков нет, все точно также как и в Go. Вот TaskScheduler, а вот и очередь, куда попадает таск.

Как следствие из этого
— SynchronisationContext нужен только в GUI, чтобы гарантировать, что таск будет выполнен в GUI потоке. Это требует вызова WinAPI (посылки сообщения в event loop винды), а это не быстро. В других местах его нет, то же самое нужно делать любой GUI библиотеке в винде, dotnet тут ни при чем.
— Добавление/запуск/создание таски стоит столько же, сколько в Go, потому, что это просто добавление задачи в очередь на выполнение.
— SemaphoreSlim работает также как Mutex в Go. Также как и в Go он интегрирован с шедулером. Просто посмотрите в исходник, там все написано.

Давайте вернемся к теме
— Что там под капотом у go не так как у дотнет?
— Как работает волшебный лок, который есть в go и который можно быстро взять вне зависимости от количества потоков, которые за него борются.
dotnet работает точно также
Нет. Попробуем ещё раз. dotnet выполняет инструкции синхронно до await, затем достаётся другой поток из пула потоков и в нём выполняется таск из await, а старый поток возвращается в пулл. При этом контекст переключается из старого потока на новый, и оставшаяся часть таска, следующая после await, выполняется в этом новом потоке. И так на каждый await. При этом заблокированный таск == заблокированный поток ОС.

А вот что происходит в Go вместо этого. Компилятор Go разбивает функцию горутины на серии инструкций, разделённые, утрируя, вводом-выводом и вызовом функций (на самом деле сложнее), При запуске горутины она передаётся одному из работающих потоков ОС, и в дальнейшем все её инструкции выполняются строго в этом потоке. После выполнения каждой такой серии (до ввода вывода или вызова функции) происходит переход к инструкциям другой горутины данного потока. При этом ни какого переключения контекста между потоками (с переходом в режим ядра и копированием стека между потоками как в C#) не происходит, а бесконечный цикл утилизирует 100% процессорного времени в соответствующем потоке ОС и никогда не будет прекращен рантаймом. Вот по этому мьютексы и каналы не блокируют поток ОС — они не могут. А могут лишь создать точку асинхронного переключения выполняющего потока ОС на другие горутины. Поэтому блокировка в Go — это ничто с т.з. производительности в сравнении с C#
SemaphoreSlim работает также как Mutex в Go. Также как и в Go он интегрирован с шедулером
Вы это прямо как мантру повторяете. Я же вам ответил уже, что Mutex в Go ни как не интегрирован с шедулером, а семафор с мьютексом абсурдно сравнивать. Семафор не предотвращает гонки данных, мьютекс предотвращает. Зачем возвращаться к этому ещё раз и ещё раз..
SynchronisationContext нужен только в GUI
SynchronisationContext — самый дебильный способ синхронизации GUI. В винде в GUI можно обойтись без всякого SynchronisationContext в 99% кейсов синхронизации. Достаточно нотификации через SendMessage(WM_COPYDATA, но для индусов, породивших GUI фреймворки для C#, это оказалось слишком сложно
Как работает волшебный лок, который есть в go и который можно быстро взять вне зависимости от количества потоков, которые за него борются.
Потоки за него не борются. За него борются горутины. Переключение контекста горутин — практически бесплатно. Переключение потоков — чудовищно дорого.
Позвольте исправить фактическую ошибку в вашем описании работы dotnet. dotnet — выполняет таску в текущем потоке до await.
затем достаётся другой поток из пула потоков и в нём выполняется таск из await

Нет, затем текущий поток начинает выполнять следующую таску в очереди на выполнение. Никакого «другого потока» в этой схеме нет. Когда заканчивается ввод/вывод/таймер/т.п. запущенный await в очередь задач добавляются задачи и какой-то из потоков тред пула начинает ее выполнять, продолжая выполнение таски.

По вашим словам в go дела обстоят следующим образом. goroutine выполняется до запуска другой функции или ввода/вывода или блокировки. В этот момент запускается следующая горутина. Позволю себе предположить, что все горутины выстраиваются в очередь, из которой поток ОС их достает и выполняет. Возможно это какая-то хитрая очередь с приоритетами, но я сильно сомневаюсь. А раз так, то там просто очередь, как в dotnet, только, по вашим словам, по очереди на каждый поток.

И там и там потоки переключает шедулер ОС, которому плевать поток go или dotnet. Отмечу, никакого «копирования» стека не происходит, переключение потоков так не работает. При переключении потоков восстанавливаются настройки MMU и восстанавливается содержимое регистров процессора. Это дорого, но не чудовищно дорого, тысячи раз в секунду на одном ядре это делать можно без ущерба производительности.

Я вижу много одинакового
— и там и там есть очередь выполнения задач
— и там и там потоки выполняют задачи из этих очередей.
— примерно одинаково выглядят события, по которым обычно происходит переключение между задачами
в dotnet таска переключается в момент вызова «системных» функций, await и вызова SemaphoreSlim.WaitAsync и аналогов
в go горутина переключается в момент вызова «системных» функций, gc, вызова go, вызова примитивов синхронизации (читать тут)
Есть и различия
— dotnet имеет одну очередь из которой много потоков выбирают задачи
— go имеет по очереди для каждого потока

Причем я думаю, что go на самом деле так не делает. Потому что тогда бесконечный цикл в горутине «вырубит» все, что должны выполнятся в этом потоке. Это приведет к неравномерной загрузке CPU. Поэтому, я думаю, что вы не правы и в go, также как в dotnet, есть одна очередь на все горутины.

Буду рад любой обоснованной критике и правкам, желательно с ссылками на конкретный исходный код. Все утверждения выше я уже подкрепил ссылками на конкретные классы и функции.

И тут стоит вернутся к вопросу — а чем go лучше dotnet в плане выполнения goroutine/task?

Теперь рассмотрим различие мьютексов go и SemaphoreSlim. Конкретно SemaphoreSlim.cs строка 631 «return asyncAwaiter». Это та самая точка переключения, в которой поток из тредпула начнет выполнять следующую таску. То есть никакого переключения контекста, никаких вызовов ядра и т.п. в SemaphoreSlim нет.

Вы заметили, что semaphore это не mutex, что верно. Также верно и другое — new SemaphoreSlim(1,1) == mutex, только одна таска сможет захватить такой семафор. Забавно, что функция, которая переключает на следующую горутину называется «runtime_SemacquireMutex», что-то мне подсказывает, что Sem это от Semaphore. Было бы интересно услышать более развернутый комментарий на тему того, что семафор гонки не предотвращает, а мьютекс предотвращает.

Итого
— в go mutex передает управление шедулеру с помощью вызова runtime_SemacquireMutex
— в dotnet управление шедулеру передается с помощью «return asyncAwaiter»

С чем тут спорить?

SynchronisationContext — самый дебильный способ синхронизации GUI. В винде в GUI можно обойтись без всякого SynchronisationContext в 99% кейсов синхронизации. Достаточно нотификации через SendMessage(WM_COPYDATA, но для индусов, породивших GUI фреймворки для C#, это оказалось слишком сложно

А что по вашему делает SynchronisationContext?
WindowsFormsSynchronisationContext вызывает Control.Invoke
— Control.Invoke вызывает «User32.PostMessageW(this, s_threadCallbackMessage);»

Это ничем не отличается от посылки WM_COPYDATA, который вы предлагаете, вместо WM_COPYDATA посылается 'Application.WindowMessagesVersion + "_ThreadCallbackMessage"'
dotnet имеет одну очередь из которой много потоков выбирают задачи

Каждый поток имеет свою локальную очередь (https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler?view=netframework-4.8#Queues). Так что различий с го вообще нет :)


Потому что тогда бесконечный цикл в горутине «вырубит» все, что должны выполнятся в этом потоке.

Если вы о том, что другие горутины/таски не запустятся, сидя в локальной очереди треда, который гоняет while(true){}, то это не так. Потоки как в дотнете, так и го, могут забирать таски из локальной очереди другого потока, если им скучно ничего нет в их локальной и в глобальной очередях.

Век живи, век учись, спасибо.

Нет, затем текущий поток начинает выполнять следующую таску в очереди на выполнение. Никакого «другого потока» в этой схеме нет.
А, ну значит вы просто не в курсе как это работает.
Task.Run(async () =>
            {
                for (var i = 0; i<10; i++)
                {
                    Console.Write("{0} ", Thread.CurrentThread.ManagedThreadId);
                    await Task.Delay(1);                    
                }
            });
            Console.ReadKey();
            // 3 4 4 3 4 3 4 4 4 3
Как вы можете видеть, context switch есть даже в самом примитивном сценарии.
При переключении потоков восстанавливаются настройки MMU и восстанавливается содержимое регистров процессора.
В реальной жизни при блокировках этого достаточно для lock convoy при той нагрузке, которую программа на Go даже не заметит
в dotnet таска переключается в момент вызова «системных» функций, await и вызова SemaphoreSlim.WaitAsync и аналогов
Так ведь не таска в C# переключается, а поток операционной системы переключается. А в Go поток ОС никуда не переключается, он выполняет свои горутины.
dotnet имеет одну очередь из которой много потоков выбирают задачи
Нет. Коллега, вы немножко задолбали. Не поток выбирает задачи, а задача выбирает потоки. Предлагаю к этому более не возвращаться.
Причем я думаю, что go на самом деле так не делает. Потому что тогда бесконечный цикл в горутине «вырубит» все, что должны выполнятся в этом потоке.
Безусловно вырубит. Нет, это не повод уродовать рантайм бессмысленными переключениями между потоками ОС. Те, кто пишет бесконечные циклы, учитывают эту особенность. И, кстати, на практике написать такой бесконечный цикл практически невозможно. Мой пример искусственный, на практике будет инструкция a=12, и компилятор такой код не пропустит
И тут стоит вернутся к вопросу — а чем go лучше dotnet в плане выполнения goroutine/task
Я вам в каждом комментарии отвечаю на этот вопрос. Тем лучше, что в Go нет хлама в многопоточном коде, при этом многопоточный код на Go более надёжный и эффективный. Но вы всё игнорируете и продолжаете своё «не вижу» «не понимаю»
Было бы интересно услышать более развернутый комментарий на тему того, что семафор гонки не предотвращает, а мьютекс предотвращает.
Зачем развёрнутые комментарии на очевидное? Это следует из определения семафора и определения гонки данных.
Ага, а вот go конечно же не переключает потоки :) Я чтобы этот замечательный факт доказать даже пример написал
package main

import (
    "fmt"
    "time"
    "syscall"
)

func main() {
    fmt.Println("Hello, playground ", syscall.Gettid())
    ticker := time.NewTicker(time.Second)
    go func() {
        for t := range ticker.C {
            fmt.Println("Tick at ", t, " tid ", syscall.Gettid())
        }
    }()
		
    time.Sleep(time.Millisecond * 10000)
    ticker.Stop()
    fmt.Println("Ticker stopped ", syscall.Gettid())
}


А вот оно, доказательство того, что Go совсем не так, как dotnet шедулит задачи.

➜ gotest docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest go run main.go
Hello, playground 67
Thread ID: 70
Thread ID: 70
Thread ID: 70
Thread ID: 70
Thread ID: 70
Thread ID: 70
Thread ID: 70
Thread ID: 69
Thread ID: 69
Ticker stopped 69

➜ gotest docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest go run main.go
Hello, playground 63
Thread ID: 67
Thread ID: 66
Thread ID: 65
Thread ID: 65
Thread ID: 65
Thread ID: 63
Thread ID: 65
Thread ID: 65
Thread ID: 65
Ticker stopped 65
➜ gotest docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest go run main.go
Hello, playground 63
Thread ID: 67
Thread ID: 66
Thread ID: 68
Thread ID: 68
Thread ID: 68
Thread ID: 66
Thread ID: 66
Thread ID: 66
Thread ID: 66
Ticker stopped 66

Может быть у вас есть теория, почему я вижу разные TID?

Я не думаю, что syscall.Gettid является корректным и переносимым способом определения нативного потока горутины. А почему вы думаете иначе? откуда такая информация?

А чего он в таком случае вообще в стандартной библиотеке делает?

Ничего. Эта исходный код этой функция сгенерирован из прототипов системных вызовов.

GetTid выдаёт поток ОС, который выполняет кол, он про го ничего не знает. Это аналог managedthreadId

Я склонен с Вами согласится. Пожалуй, в данном вопросе правы вы, а я ошибался. Любой из потоков ОС, используемых рантаймом Го, может в какой-то момент времени отвечать за выполнение кода одной из горутин. Предполагаю, что потоки ОС из рантайма Го, выполняющие горутины, используют общую память, поэтому переключений контекста ядра/пользователя не требуется. Трудно сказать без нагрузочных тестов, насколько это критично в плане различий с производительностью с C#.
На этой радостной ноте согласия я покидаю этот тред. Мне было интересно покопаться в кишках go, надеюсь вам было/будет интересно посмотреть как оно в dotnet устроено. Уверен, вы найдете больше сходства, чем отличий. Ну а выводы из этого каждый делает сам.
Код надо не только написать, но и поддерживать. Средний С# разработчик (не автор кода, а коллега) скорее разберется в заумном C# коде, чем выучит Go. Да и нового разработчика с требованиями «Advance C#» легче и быстрее найти чем C#/Go. F скорее всего и дешевле.
Отличные вы походили по граблям :)

История 1: Task.Delay & TimerQueue

Получается вы стартовали один таймер на каждый запрос? Иначе непонятно как получить слишком много таймеров. Если так, то можно легко обойтись одним concurrent queue или stack (если вам нужно lifo) и одним таймером, срабатывающим раз в секунду.

История 2: SemaphoreSlim

Я бы предложил иметь атомарный счетчик, каждый запрос вначале его увеличивает, затем уменьшает. Если счетчик больше N, то давать 504. Это просто и эффективно.
Положим задача сложнее — мы хотим чтобы а) если запросов меньше N, то он бы выполнялся б) если запросов больше N, то он бы помещался в очередь, где ждал либо таймаута, либо пока один из запросов не выполнится.
Если нужно такое, то я бы переключился на синхронные методы в контроллерах, тогда они будут выполняться в тредпуле. А если их слишком много, то сервер поставит поступающие запросы на ожидание. Если я правильно понял, то это ровно то, что нужно.
Если же по каким-то причиним синхронные методы не подходят, то пишем что-то такое, в каждом методе контроллера
public async Task<MyResponse> MyMethod([FromBody] MyRequest request)
{
    return Task.Factory.StartNew(MyMethodLogic, 
    CancellationToken.None, TaskCreationOptions.DenyChildAttach, MyThrottledTaskScheduler);
}


MyThrottledTaskScheduler — это реализация TaskScheduler с тротлингом запросов. Если вдруг с шедулером не взлетает, то делается свой SynchronisationContext. С ним точно все будет ок.

Внутри этого пула есть вторая часть для объектов, которые ещё находятся в нулевом и первом поколении, и почему-то для них вместо lock-free структуры используется обычный list с lock'ом.

Думаю потому, что взятие лока, при условии, что других потоков нет, это примерно как поделить long на long по затратам времени. То есть это очень и очень быстро при условии отсутствия большой конкуренции за лок. Я бы сильно подумал откуда вообще такая конкуренция за буферы, почему так много потоков, которые пытаются делать IO?

4.3 Индекс

Почему не immutable dictionary, кажется оно должно в этой ситуации работать на ура?

Нужно хранить in-memory индекс <Guid,Guid>

А это свойство позволяет легко вытащить индекс в нативную память. Индекс большой и если там нет «горячих» элементов, то есть чтения и записи разнесены в пространстве, то можно легко его разбить на сегменты и закрыть простым локом каждый сегмент.
Получается вы стартовали один таймер на каждый запрос?

Да. С таймером много решений можно придумать, но если проблема воспроизвелась в уже готовом приложении, в котором, например активно используется Task.Delay — проще сконфигурировать рантайм, прежде чем делать что-либо ещё.


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

Если я правильно понял — предлагается использовать ограничение на количество потоков в тредпуле для реализации троттлинга. Это сработает, но приведёт к тому, что любой код, который хочет попасть на тредпул — будет ждать, пока в тредпуле не освободится или не создастся новый поток. И эта очередь будет обрабатываться в FIFO порядке, что снижает вероятность того, что клиенты не накидают в неё повторные запросы и она успешно разберётся.


MyThrottledTaskScheduler — это реализация TaskScheduler с тротлингом запросов. Если вдруг с шедулером не взлетает, то делается свой SynchronizationContext. С ним точно все будет ок.

Подозреваю, в MyThrottledTaskScheduler или MyThrottledSynchronizationContext придётся написать примерно такой же код, как у нас, магии же не бывает :)


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

Здесь проблемным приложением был сервер распределённой кастомной файловой системы, который открывает сразу множество файлов и раздаёт клиентам их фрагменты (здесь возникнет вопрос о целесообразности использования .NET в нём, но так уж получилось). В других приложениях такого не наблюдали.
К тому же интенсивное IO не было проблемой само по себе, на момент написания приложения проблемы не существовало — она возникла только после обновления рантайма .NET на серверах. .NET Framework позволяет иметь лишь одну версию рантайма (4.х) одновременно — 4.5.2 полностью заменяет 4.5.1, например. .NET Core, во избежание подобных проблем позволяет держать несколько версий рантайма (shared framework) на одной машине side-by-side, либо деплоить рантайм вместе с приложением.


Почему не immutable dictionary, кажется оно должно в этой ситуации работать на ура?

Immutable коллекции, такие как ImmutableList и ImmutableDictionary основаны на деревьях, объекты в которых переиспользуются их различными версиями. Но т.к. это деревья объектов — они дают большой оверхэд по памяти и нагрузку на GC, как и ConcurrentDictionary.


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

Такое решение тоже должно сработать, придётся только подумать о том, как контролировать размеры сегментов и переаллоцировать их при необходимости. Другое дело, что само разбиение на сегменты уменьшит их размеры, что может сделать нецелесообразным их перенос в нативную память (не будут попадать в LOH), а с локом можно по-разному экспериментировать — тот же SpinLock использовать.
В другом проекте инфраструктуры переносили часть объектов, создающих большой memory traffic в нативную память, там дошло до использования TCMalloc, возможно, когда-нибудь и об этом опыте кто-нибудь расскажет.

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


Ну первое, что приходит в голову — на сокетах, конечно.

По поводу семафора и LIFO, вам же не нужен был математически строгий LIFO, достаточно было сделать так, чтобы семафор не захватывал тот, кому уже не актуально. А значит можно было просто передавать разумный тайм-аут в SemaforeSlim.WaitAsync

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