Комментарии 89
Интересный кейс с C#. Кто-то может на пальцах объяснить, что они такого в языке сделали, что теперь джаву кратно превосходят?
Ну таски - это не треды, у них нет стека, только контекст. Такая абстракция кооперативной многозадачности.
Есть пул тредов, который выполняет таски по мере возможности. Если таски занять длительной CPU работой, то обработка всех остальных тасков встанет.
Думаю, если вместо тасков использовать треды (но зачем?), то разница по памяти будет незначительной.
Здесь джаву тоже тестируют на виртуальных тредах, а не на обычных. Для них так же справедливы все преимущества, о которых вы говорите. Разве что async-await на завезли. Наверное, дело в каких-то оптимизациях хранения объектов в куче. В джаву все никак не доедет Project Valhalla с value-объектами, и только весной следующего года будет релиз с уменьшением хедеров объектов.
В Java все таки потоки, им стандартный стек нужен, пусть и небольшой. В С# пока код спит, до следующего async исполнения, стековые переменные сохраняется в объект (если точнее, то наоборот, в объекте состояние, вытаскиваемое в стек по необходимости) а стек выделяется только при исполнении.
Нет никаких там дополнительных потоков, те же таски но на уровне JVM исполняемые платформенными потоками-носителями. Вся киллер фича Java подхода - это отсутствие необходимости явного этим управления. Для этого в коде буквально напиханы If-чики
public static void sleep(long millis) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
long nanos = MILLISECONDS.toNanos(millis);
ThreadSleepEvent event = beforeSleep(nanos);
try {
if (currentThread() instanceof VirtualThread vthread) {
vthread.sleepNanos(nanos);
} else {
sleep0(nanos);
}
} finally {
afterSleep(event);
}
}
Наверное, дело в каких-то оптимизациях хранения объектов в куче.
Нет все проще. Если не ошибаюсь, в Java только к 2017 году проснулись и осознали что оказывается в современном яп нужна нормальная поддержка неблокирующей асинхронности. Можно было скопировать async/await который уже был отработан много лет но в Oracle решили стать нитакусиками и начали пилить свои идеи.
Как результат, в Kotlin корутины зарелизили в 2018 году а в Java виртуальные потоки увидели мир только через 6 лет после этого.
Итого мы сейчас в Java имеем виртуальные потоки, которые по факту те же корутины без особых преимуществ, только без необходимости писать async/await и пока что непонятно насколько эффективные в будущем.
Просто для сравнения: в C# async/await появился в 5.0 версии в 2012 году.
Отсутствие async/await синтаксиса это огромное преимущество. Async распространяется и в отдельном взятом проекте, и на уровне всей экосистемы языка как плющ. Из-за этого вся экосистема языка становится раздробленной, возникают проблемы совместимости между библиотеками, необходимость поддержки нескольких версий библиотек, трудность перевода на асинк старых больших "синхронных" проектов и т.п. Раст и питон тому примеры.
Конкретно в .NET никакой проблемы с async/await нет. Есть давно устоявшиеся паттерны вызова async-over-sync и наоборот - это помогает переводить легаси по мере необходимости. В новых проектах вы просто сразу пилите async.
Есть давно устоявшиеся паттерны вызова async-over-sync и наоборот
Можете привести примеры или ссылку, где почитать? Меня интересует вызов async
из синхронного кода. Иногда попадаются библиотеки, предоставляющие только асинхронный API, для них приходится писать синхронную обёртку и хотелось бы это делать правильно.
Находил когда-то официальное микрософтовское руководство по асинхронному программированию и там была фраза, что существует много способов вызова async
из синхронного кода, но все они плохие, поэтому либо делайте асинхронным весь проект, либо не используйте асинхронность вообще. Получается, ситуация изменилась?
Меня интересует вызов
async
из синхронного кода.
Вроде всё просто:
вместо
var result = await asyncMethod();
пишем
var result = asyncMethod().Result;
или
asyncMethod().Wait();
если нет возвращаемого значения. Можно словить дедлок, но тоже выход есть.
Ничего не изменилось, все они плохие.
Всё ещё нужно понимать, что и зачем ты делаешь, и тогда будет в целом достаточно делать task.GetResult().
Поддерживаю. Подход выбранный Java намного более прагматичный. А ключевые слова типа async/await и suspend это также допольнитая "раскраска(colored)" кода. О всем этохо хорошо расказал Иван Углянский в своем докладе. И сделал сравнение по имплеменатции "легковесных" потоков во многих языках https://www.youtube.com/watch?v=kwS3OeoVCno
но в Oracle решили стать нитакусиками и начали пилить свои идеи.
вы так пишете, словно умнее всех их архитекторов. все кто сталкивался с async/await знают что это натуральный рак кода, стоит добавить в одном месте async и вам придется отрефакторить пол проекта. ну и в целом это не избавляет от граблей со случайной блокировкой потока и влечет кстати перфомансные проблемы, которые хороши измеримы на kotlin - там как раз решили никого не ждать и сделали async/await (если вкратце jit сложнее инлайнить код) . Трудно сказать насколько сильно нужна эта асинхронищина на беке, в том смысле что если у вас пару мест асинхронного вызова, то можно использовать апи, которое еще с java 8 было, не самое красивое, с нюансами, но работает. По этому может и с большим запозданием, но в java это сделано так как должно (вам не надо знать ни про какие async/await и использовать еще специальный api, которые реально неблокирующий), а лучше всего в go.
Трудно сказать насколько сильно нужна эта асинхронищина на беке, в том смысле что если у вас пару мест асинхронного вызова, то можно использовать апи, которое еще с java 8 было, не самое красивое, с нюансами, но работает.
Если это шутка то не очень понятная.
По этому может и с большим запозданием, но в java это сделано так как должно
Есть такое хорошее выражение "лучшее - враг хорошего". Может быть async/await это не идеальное решение, но это решение которое работало, работает и будет работать. В то время как другие ЯП (JS, TS, Swift, Kotlin, Python, Rust) скопировали не идеальное но рабочее решение и многие годы используют их в боевых проектах, в Java семь лет пытались сделать "лучшее" решение, которое еще не факт что себя оправдает. Хотя нет же, в Java все это время закрывали проблему реактивными библиотеками, там прям страх и ненависть в комплекте и стакан молока за вредность, но зато не async/await.
в java это сделано так как должно
только время покажет должное ли оно
вам не надо знать ни про какие async/await
Как показывает практика знать придется еще больше, но про другое
Если это шутка то не очень понятная.
лично я из своего опыта редко видел какие-то асинхронные подзадачи в коде, видимо особенность проектов, монолит, который делает много работы сам, а не раскидывает ее другим (ну и клиентов не сильно много, можем позволить держать поток блокированным). все эти случаи вполне нормально решались через простой future даже.
которое еще не факт что себя оправдает
в смысле не оправдает? сделали полноценные виртуальные потоки, как в го (с некоторыми минусами, но близко), закрыли руками все опасные места. итого у нас как минимум 1) есть нормальный стек трейс 2) мы не боимся, что в коде, где-то глубоко, там где мы не контролируем, случайно окажется блокировка (да, сейчас в java есть проблема с synchronized, но ее решают). эти 2 пункта киллер фичи. Как бы по этому в C# и добавили сахар с async/await в 2012, потому что работы не очень много на него надо, а нормально когда сделают? Я например недавно ковырял spring rest client который асинхронный + oauth и мне совершенно не понравилось, эта реативщина одна большая проблема и усложнение всего на ровном месте. Уж не знаю как бы повлияло наличие async/await но наличие виртуальных потоков сняло бы все проблемы сразу. Единственное за что можно поставить java минус - это все делают медленно, потому что, как я понимаю, занимается этим десяток людей.
только время покажет должное ли оно
тут ноу комментс у меня. вам сделали конфетку, а вы тянетесь по привычке к сухарику, потому что он такой много где еще и его давно рекламируют продавцы. Я этот феномен не первый раз наблюдают и не могу его объяснить.
лично я из своего опыта редко видел какие-то асинхронные подзадачи в коде
Все эти корутины и виртуальные потоки это история не столько про асинхронность сколько про эффективную утилизацию ресурсов давая возможность не блокировать потоки. Любой backend это практически одни IO операции.
сделали полноценные виртуальные потоки, как в го (с некоторыми минусами, но близко)
Что там общего с Go? В Go потоки не деляться на платформенные и виртуальные, там вся работа на уровне языка идет с горутинами. Горутины сделаны по stackful схеме, в Java виртуальные потоки по stackless. Горутины порождаются явно, и не нужно думать в любой точке кода в каком контексте мы работаем, потому что все работает на горутинах. На Java мы будем работать в неявном контексте. Ну разве что общее что и там и там поддержка со стороны рантайма.
Как бы по этому в C# и добавили сахар с async/await в 2012, потому что работы не очень много на него надо
И как итог, в C# 12 лет как пишут по новому а в Java все эти годы давились реактивщиной, или просто забивали на нужды разработчиков. В принципе можно было и другие проблемы также решать: нужен новый сборщик мусора? Подождите лет 20, зато мы выкатим самый модный.
вам сделали конфетку, а вы тянетесь по привычке к сухарику
Вот когда это все добро обкатают, внедрят поддержку на всех необходимых библиотеках, ни у кого ничего не отвалится или отвалиться и все починят, и когда это обкатается на больших проектах, вот тогда да, можно будет похвалить. Потому что на деле может оказаться, как часто бывает шило на мыло.
Любой backend это практически одни IO операции.
можно с другой стороны зайти: если вы делаете не прокси сервер, то сильно ли вы потеряете, если вместе с диском и базой зависнет еще и машина с приложением? ;) все равно есть какой-то пул потоков, типичный бекенд это oltp, нет никакого смысла пускать 5 тысяч клиентов на одну базу чтобы они все вместе ее грузили по чуть-чуть, эффективнее дать доступ только 50-и в один момент. соответственно у вас и на сервере будет пул из условно 50-и потоков и очередь для всех остальных. я к тому что это не всегда правда высеченная в камне, но есть варианты и есть разный бекенд. Да если мне надо сходить и проверить рекапчу в гугл, то это идеально для настоящей асинхронщины (или если у меня notification сервис), но в остальном хватает случаев когда бек себя отлично чувствует и на обычном блокирующем io, исходя из предполагаемой специфики применения.
в Java виртуальные потоки по stackless
вы полностью не правы и видимо не в курсе что сделали в java. У виртуального потока есть свой стек, за счет этого как раз и имеем нормальный тред дамп, реализация похожа именно на go.
И как итог, в C# 12 лет как пишут по новому а в Java все эти годы давились реактивщиной
я так понимаю что боялись рака async/await, вам же надо что-то делать со стандартным апи, иначе какой смысл в асинхронщине, если вызов чтения с файла все сломает и заблокирует. Т.е. пришлось бы менять еще апи или вводить новое, такое же но с припиской async, или городить какой-то волшебный метод в который все надо заворачивать. Сборщик мусора тоже теперь самый модный кстати ;) в принципе по части самой vm java сильно лучше .net, теперь вот оракл еще ее и сделан заново (graalvm).
Вот когда это все добро обкатают, внедрят поддержку на всех необходимых библиотеках,
все что не содержит в себе synchronized работает хорошо, как-то особо катать там нечего. на данный момент не хватает только возможности кастомизировать пул потоков (стратегию его работы, а тут есть нюансы) и как-то отбираться работу у потока, который не в базу пошел, а сел считать нейронные сети на пол часа.
Речь вообще о том, что java и .net как бы похожие, но в аспектах применения и положения на рынке сильно разные. По-этому пошли разными путями. В ms более гибкие и не считают что они кому-то должны обратную совместимость, в oracle делают что-то новое, когда в этом есть реальная необходимость и без этого никак, там ничего не хотят добавлять просто по приколу, например в лепешку разобьются, чтобы не добавить новое ключевое слово. Сильно java упала в популярности из-за того, что в ней небыло асинхронщины на уровне языка? По-моему не сильно.
У виртуального потока есть свой стек, за счет этого как раз и имеем нормальный тред дамп
Согласен тут видимо меня немного занесло.
oracle делают что-то новое, когда в этом есть реальная необходимость и без этого никак, там ничего не хотят добавлять просто по приколу, например в лепешку разобьются, чтобы не добавить новое ключевое слово.
11->17
теперь в языке два switch с разной семантикой, + yield + record + sealed + permits + сломанная семантика get/set в records, + сам синтаксис records сломал весь стандартный привычный синтаксис.
Все что вы пишите было актуально до 11 версии. Сейчас уже никто в лепешку не разбиваеться.
Сильно java упала в популярности из-за того, что в ней небыло асинхронщины на уровне языка?
Java популярна по причине того что исторически заняла финансовый сектор и успешно держит а не по причине качества, быстродействия или каких то фич языка. Там все обкатано и большой пул разработчиков а это главное.
в принципе по части самой vm java сильно лучше .net
Сильное заявление. А аргументы будут?
теперь вот оракл еще ее и сделан заново (graalvm)
Молодцы конечно, в .net с 2019 года AOT есть из коробки. Без установок дополнительных VM и прочего.
можно с другой стороны зайти: если вы делаете не прокси сервер, то сильно ли вы потеряете, если вместе с диском и базой зависнет еще и машина с приложением? ;) все равно есть какой-то пул потоков, типичный бекенд это oltp, нет никакого смысла пускать 5 тысяч клиентов на одну базу чтобы они все вместе ее грузили по чуть-чуть, эффективнее дать доступ только 50-и в один момент.
Да действительно, используются 200 потоков когда можно 8, обрабатываются запросы параллельно или пусть постоят в очереди. какая разница. Прилетело 1000 запросов и мы вывозим по кэшам базам и интеграциям обрабатывать их параллельно, не беда, пусть постоят в очереди для их же блага.
все что не содержит в себе synchronized работает хорошо, как-то особо катать там нечего.
Это конечно очень здорово что Вы сказали что все работает хорошо и тестить там нечего но я предпочту старый добрый метод опытной эксплуатации на больших проектах как показатель качества работы и ее зрелости.
Java популярна по причине того что исторически заняла финансовый сектор и успешно держит а не по причине качества, быстродействия или каких то фич языка. Там все обкатано и большой пул разработчиков а это главное.
это объяснение которое "хипстеры" хотят слышать о том почему их модный язык не используется там где есть java. по части скорости vm она быстрее всех, даже средняя программа на вроде бы компилируемым go хуже будет - на гитхабе есть CardRaytracerBenchmark (и еще один похожий, сейчас не нахожу), там люди один вычислительный код пилят на разных платформах. В java все по канонам, все на объектах, но за счет оптимизаций после прогрева кода там нулевое выделение памяти, хотя в коде мы вроде на каждый чих создаем объект vector. Так что по части самой vm там все лучше чем например .net и по-моему netчики никогда особо не спорили на этот счет (а про node, python даже стыдно говорить, они на таких тестах проиграют 50-100x). Другая причина может быть в том, что java появилась давно и давно кроссплатформенная, по этому кастомер может купить мейнфрейм от ibm и там тоже будет java (хоть и от ibm), но очевидно не на одних только мейнфреймах java держится.
теперь в языке два switch с разной семантикой, + yield + record + sealed + permits + сломанная семантика get/set в records, + сам синтаксис records сломал весь стандартный привычный синтаксис.
ну тут вы сильно сгущаете, но насчет yield и get/set в рекордс могу согласиться, в принципе я видел что на стековерфлоу тот же Brian Goetz отвечает на подобные вопросы, надо поисать. С yield очевидно не хотели вводить новое ключевое слово. В остальном я не вижу ничего спорного и ломающего в нововведениях, ни в свичах ни в sealed классах.
Молодцы конечно, в .net с 2019 года AOT есть из коробки. Без установок дополнительных VM и прочего.
ну мы ж не о том кто раньше, в hotspot (обычная джава) тоже был AOT и наверно раньше 19г (добавляла по российская команда из сторонней компании) но в том виде он оказался не нужен и его выпилили, лично я не пробовал его и всю историю не помню. сейчас вот другой заход, но смысл graalvm куда больше чем просто компиляция в exe - там 1) jit компилятор на самой java, более агрессивный и продвинутый (потому что с hotspot сегодня разрабам сложно разобраться и добавлять новое), итого +20% к перформансу на текущий день 2) возможность выполнять вместе все остальное, типа питона или wasm 3) ну и компилировать можно, но не ради этого все затевалось, а ради 1 и 2.
Да действительно, используются 200 потоков когда можно 8, обрабатываются запросы параллельно или пусть постоят в очереди. какая разница. Прилетело 1000 запросов и мы вывозим по кэшам базам и интеграциям обрабатывать их параллельно, не беда, пусть постоят в очереди для их же блага.
я к тому что не каждая база умеет переваривать тысячи конкурентных запросов и не каждый сервис, и так чтобы он вис подолгу а не на 5 мс. с точки зрения хипстеров это все конечно отстой, но они же парадоксально не замечают что их любимая платформа работает с одним потоком на процесс (хоть тут речь не про .net и go). Поэтому те кто реально на java деньги зарабатывал не испытывал каких-то особых проблем, даже "одноклассники", а у них стек на java (просто как пример хайлоуда с большим канкарэнси). В общем время шло, а люди как-то с java не разбегались ну и теперь вознаграждены что-ли ;)
Банальный ответ:
Microsoft вложил в развитие асинхронного рантайма и сокращение аллокаций 10млн человеко-часов. Я думаю в сумме как половина остальных языков из теста.
продвинутый ответ:
Код на C# из статьи вообще не порождает потоков и не использует пул. Delay порождает объект с одним полем, а один поток таймера (один на весь процесс) меняет значение поля через 10 сек и запускает асинхронное продолжение в конце.
по сути это вырожденный случай, потому что если бы была хоть какая-то реальная работа, то результаты забегов были бы совсем другие.
с другой стороны асинхронность в современном мире это не про параллельность вычислений, а про параллельность ожидания. И c#/dotnet под это очень хорошо заточен.
простите, а сколько ожидать придётся по тому коду который в этой статье написан на C#? 10 секунд ?
по непроверенному моему коду на компетенцию соотвествия где я складываю 1+1
1 миллион раз я получаю примерно 640 милисекунд
А если питон запустить через PyPy или другие варианты?
Сам спросил, сам ответил: Код из статьи, 100.000 тасков:
PyPy_v7.3.17 (Python 3.10): 236.3MB
Python 3.9.1: 178.2MB
Python 3.12.1: 140MB
Python 3.13.1: 140.4MB
Асинхронность это когда есть коллекция тасков. В таске хранятся локальные переменные и номер строки с которой продолжить. Такси и коллекция занимают место в памяти. Оптимизировать по этому параметру мне кажется смысла не имеет, так как в реальности процессор и сеть будут узкими местами.
Не скажите. Есть у меня сервис, на Java, который работает с граф данными. под нагрузкой этот сервис кушает 4Gb оперативки.
Ради интереса переписал его на Rust. При тех же данных, при той же нагрузке. Версия на Rust: потребляет в 2 раза меньше CPU и вместо 4Gb кушает 200Mb.
Поэтому для меня очевидно, что имеет смысл оптимизировать по этим параметрам.
А вы профилировали Яву прежде, чем начать переписывать на Раст?
И в хвост и в гриву.
Apache Gremlin весьма прожорлив и создаёт тонны промежуточных обьектов.
В моей реализации Gremlin спецификации операций выделения памяти на порядки меньше.
Плюс сам граф хранится в памяти компактнее.
Т.е. дело не в Яве, а в какой-то сторонней библиотеке. Так и стоит формулировать.
Я про расход памяти на корутину. Точнее на ту часть корутины, которая техническая и не зависит от кода.
Автору большой минус за графики. Цифры с огромным количеством нулей и единица измерения в килобайтах. Было бы логичнее масштабировать дальше до гигабайтов.
Java, если её явно не ограничить, берёт память про запас. Так что тут может быть весьма сильная наводка.
Я бы попробовал для каждого теста подбирать параметр -Xmx. Подбирая его так, чтобы он был минимальным, но позволял выполняться приложению.
Читерство в чистом виде!
Вы в проде тоже станете душить по памяти приложение пока оно не сдохнет?
Так то там в любом языке можно шлифануть напильничком :)
Но в целом сравнение действительно не совсем актуальное - треды/горутины это не то же самое что async-и.
В c# тоже есть такие же настройки для уменьшения аппетитов gc, но тут они не применялись.
Ну почему же? Мы же смотрим именно аппетиты задачи по памяти? Значит должны смотреть именно на реально использованную память, а не на то, что jvm взяла прозарас. Так можно jvm заставить хоть 64гб, хоть 1тб забрать себе. Но реально используемая память от этого не изменится.
Ну про запас то оно не просто так берет?
Конечно нет!
Но и смысл таких тестов не в том чтобы посмотреть у какой среды какие эвристики по выделению памяти, а в том, чтобы узнать сколько потребляют корутины/асинки-авейты/зелёные треды/у кого что.
А в таком случае надо смотреть на реально используемую память а не на жировые запасы, которые по факту могут и не использоваться никогда.
Грубо говоря нужна оценка "сколько влезет зелёных потоков в 1гб хипа", а не "сколько захочет забрать у системы тот или иной gc при 100500 потоках.
К слову у OpenJDK много разных gc. И при разных gc исходный тест без явных ограничений может показать разные результаты. Потому как эвристики будут разные.
просто так, лучше больше чем меньше. по дефолту по-моему 25% от хипа и под 75% на машинах с малым количеством памяти. берется с запасом и с избытком
Вообще если такой тест делать по уму, то он ой как не просто строится.
По хорошему там для jvm надо загрузить таски в пул, а потом через jmx выуживать сколько хипа и оффхипа потрачено.
Смотреть на процесс снаружи для jvm несколько бессмысленно.
Сравнивать горутины и async - это как автомобили со смартфонами.
Там где под капотом по сути своей колбеки, у го практически полновесные процессы (свой стек).
Полноценных async-ов в golang пока не завезли. Но уже немного пилят в рамках итераторов.
Я так понимаю, что есть, то и сравнивали. Будут другие возможности запускать потоки, будут их использовать. Скажем, не было бы в шарпах тасков, в тестах были бы треды.
И корутины и горутины решают одну и ту же задачу - легковесное управление большим числом асинхронных неблокирующих задач. То что там в реализации используется stackful подход вместо stackless не делает из автомобиля смартфон.
Ну не совсем. Одно дело python с GIL на одном ядре со своим Async и совсем другое golang на машине со 100500 ядрами. И вот это будет уже не одинаковое решение казалось бы одинаковой задачи.
Ну а короче - асинхронность не равна параллелизму.
Смотря что мы решаем. Если наша задача оптимизировать большое количество IO операций с минимальными вычислениями на CPU то разница между одним и несколькими потоками уже не будет такой драматичной - большую часть времени задачи будет находиться в состоянии ожидания. Так как горутины в основном для этого и создавались (так как с вычислительными CPU операциями хорошо справляются и обычные потоки) то справедливо заметить что в данном контексте как раз горутины и async это решениям одной и той же проблемы, немного отличающимися но все равно довольно близкими методами. Если мы возьмем не питон а C# или Kotlin где неблокирующая асинхронность перекликается с параллелизмом то получим очень близкие решения.
Так в тесте никакой работы не производится. Т.е. измеряется размер всех стеков при stackful подходе и при stackless. Результаты в принципе ожидаемы. Вот только с реальными задачами миллион горутин, выполняющих работу, - обычный кейс даже на обычном серверном железе, а что будет с другими языками, особенно если учитывать ещё расходы на CPU, а не только на память?
В Go неудивительно, так как горутина инициализируется со стеком в 2 КБ. Но если очень сильно надо и понимаешь, что делаешь, собрать свой компилятор со своим размером начального стека, например, в 1 КБ.TinyGo вообще, если не ошибаюсь, позволяет установить размер через аргумент -stack-size=8KB.
К слову было бы любопытно включить сюда ещё и Kotlin с его корутинами включить.
Мне тоже стало любопытно, поэтому вот несколько тестов с ним:
Результаты получены на Arch Linux, Intel(R) Core(TM) i7-3770
Kotlin 2.1.0
Код бенчмарка:
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.time.measureTime
fun main(args: Array) {
val tasksCount = args[0].toInt()
val time = measureTime {
runBlocking {
val tasks = List(tasksCount) {
async { delay(10000) }
}
tasks.awaitAll()
}
}.inWholeMilliseconds
println("$tasksCount tasks finished in $time ms.")
}
Kotlin native:
1 task: 6.856 MB
10k tasks: 20.404 MB
100k tasks: 142.932 MB
1M tasks: 1406.352 MB
Kotlin - OpenJDK 17:
1 task: 48.760 MB
10k tasks: 68.352 MB
100k tasks: 133.264 MB
1M tasks: 775.896 MB
Kotlin - GraalVM 23:1 task: 107.652 MB
10k tasks: 119.716 MB
100k tasks: 190.916 MB
1M tasks: 782.960 MB
А полное время выполнения этого теста замерялось?
У меня подозрение, что async на самом деле работает через пул и потребеление памяти будет пропорционально размеру этого пула. При этом параллелизм в миллион корутин, ясное дело, не соблюдается и полное время теста будет кратно больше 10 сек.
Упрощённо асинк работает через очередь. Корутины можно создавать пока памяти хватит. Асинк может паралельно ждать все корутины. Пул под капотом просто ускорит их запуск.
не измерялось, а стоило бы. вцелом не очень понятно что хотел узнать автор и как потом пользоваться результатами. + как я понимаю только java и go имеют понятный стек вызовов и не зависнут если случайно блокирующийся метод попадется или объект синхронизации. за удобство и безопасность платим памятью.
и не зависнут если случайно блокирующийся метод попадется или объект синхронизации.
Ну, за блокирующими методами нужно следить. В том же tokio в расте есть свой тредпул для блокирующих вызовов, который не влияет на обычную асинхронку.
Ну и специальные мютексы, которые могут прохождение .await
точек переживать корректно.
Так что в общем и целом это не проблема, но я согласен что подход Go и ко более интуитивен т.к. там асинхронность встроена в сам язык, а не приделана снаружи потом.
В C# тоже всё в порядке со стеком вызовов и там тоже ничего не повиснет.
без временной конкретики 1 миллион тасков 131.1КB C++23(я мог ошибиться). а нет я ошибся не сделаю не хватит опыта)
спасибо за статью я более менее разобрался с асинхронным выполнением
Скрытый текст
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex m_console;
std::vector<std::thread> t;
std::vector<int> v(10000, 0); // 1 start, 2 end, 3 skip
int k = 0;
void foo(int i)
{
// simulate expensive operation
v[i] = 1;
std::this_thread::sleep_for(std::chrono::seconds(10));
v[i] = 2;
}
void Log()
{
for (int i = 0; i < 10000; i++)
{
std::lock_guard<std::mutex> lock(m_console);
std::thread th = std::thread([=]()
{ foo(i); });
std::thread::id th1 = th.get_id();
t.push_back(std::move(th)); //<=== move (after, th doesn't hold it anymore
std::cout << "Thread started :" << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
void Lag1()
{
for (int i = 0; i < 10000; i++)
{
std::lock_guard<std::mutex> lock(m_console);
if (v[i] == 2 && v[i] != 3)
{
v[i] = 3;
k++;
t[i].join();
std::cout << "Thread eraser :" << i << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
int main(int argc, char *argv[])
{
// don't call join
std::thread one(Log);
for (;;)
{
if (k == 10000)
{
std::cout << "Threads END WORK" << std::endl;
break;
}
Lag1();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
one.join();
return 0;
}
вот что у меня получилось с автоочищением памяти в момент исполнения, но код не так чтобы професиональный - у меня получилось вобщем
Прогулялся по ссылкам, не нашел, как именно автор считает оперативную память, ведь потоки и процессы это сущность ОС, и она для своих нужд может потреблять оперативную память.
Потребление памяти процессами, по-моему, очевидным образом видно в ps/top/whatever, причём тут ремарка про "сущность ОС" не понятно.
Ох, потребление памяти это НЕ ОЧЕВИДНО, и сильно зависит от выбранной ОС и ее настроек.
Единственное что имеет смысл использовать - это сравнивать свободную память перед стартом и после, тщательно подбирая тестовое окружение, что бы минимизировать его влияние на результат.
В конечном счете нас волнует как сильно мы сможем нагрузить машину и как выбор языка программирования/фреймворка отразится на наших возможностях
Тупой пример, не важно что приложение по top будет потреблять оперативную память, если из-за повышенного потребления памяти самой ОС из-за него станет выше.
Столько общих слов.
Есть ли конкретные сравнения когда потребление памяти процессом сильно влияло на общее потребление системы вне этого процесса?
Да, треды сами по себе требуют в ядре какой-то памяти для учёта и шедулинга, но их стек и прочее хранится в памяти процесса, а это основное их потребление.
Ну и делать миллион тредов это убийство ОС, так что в любом случае они не окажут большого влияния на потребление.
когда пытался вьехать в тему о чем тут, если делать без реализации по join память будет выделяться в этом случае надо настроить диспатч и смотреть кто завершил работу, по другому я не знаю, допустим не знаю какая реализация у других языков внутри, описал в общих чертах, так же отмечу если вы выделите на домашнем пк кучей 1 лям thread пк ляжет, так как нету диспатча который нужен выполненому thread, всё зависит от языка как реализованы такие вызовы в каком либо языке и есть ли в таком языке инструмент отслеживания выполнения thread, тоесть всё приходит либо к ексклюзивному выделению памяти с последующими нюансами либо к диспатчингу в момент выполнения
Тут вроде речь про выполнении в одном процессе через тредпул, число потоков по умолчанию сравнимо с числом ядер - так что накладные расходы ОС копейки. Чтобы реально запустить миллион потоков одновременно, ресурсов нужно побольше )
может проблема в задачах?
Сравнивать апельсины с яблоками - нехорошо. Необходима хоть какая-то нагрузка, иначе это просто нечестное сравнение. Или же мы должны спросить, сколько памяти займет миллион ожиданий и тогда код на языках с зелёным тредами будет другим. Включая го:
var wg sync.WaitGroup
for range 1e6 {
wg.Add(1)
time.AfterFunc(10*time.Second, wg.Done)
}
wg.Wait()
и давайте посмотрим, сколько это займет памяти теперь
А что такое полезная нагрузка? Весь смысл асинхронных задач - в асинхронном ожидании по сути. Отправили байтики по сети, ждём ответа, делаем другие задачи.
А процессором молотить лучше в пуле тредов, а не в асинхронке.
Ну вот хотя бы это самое асинхронное ожидание и надо было сделать. А то в коде на C# те самые операции, которых миллион, даже оператора await не содержат...
Тест чисто за память, никакого реального выполнения не требуется и не планируется.
Вот и интересно было бы посмотреть на память, требуемую минимальной сопрограмме. А её-то и не показано, задача таймера - совсем не то же самое, что и сопрограмма.
Не знаю как там в других языках, но в C# это по идее (до запуска) будет чисто объект для хранения переменных, если они имеются. Ну т.е. около нуля будет весить таска, пока её не запустили.
Так ведь значение, возвращаемое Task.Delay, весит столь же около нуля. А потому я ожидаю, что общая занимаемая память увеличится примерно вдвое.
Я навскидку не помню, есть ли в таске хоть что-то, чтобы она занимала память. Т.е. если таска 0 - то да, это будет увеличение значительное. А если таска чего то весит, то увеличение может оказаться на треть например.
Там хранятся, поимо заголовка объекта: номер задачи, делегат для исполнения, пользовательский объект состояния, ссылка на планировщик задач, состояние задачи в виде набора флагов, ссылка на продолжения, ссылка на ContingentProperties (это отдельный объект с редкоиспользуемыми полями).
И ещё несколько полей может быть добавлено в классе-наследнике (к примеру, DelayPromise хранит ссылку на таймер).
Горутины же продолжают оставаться неэффективными в потреблении ресурсов.
В Go под каждую горутину выделяется 2кб стека по умолчанию. В чем смысл этого сравнения, если в этих горутинах ничего не происходит?
Решительно непонятно что вы тут обсуждаете, нет ни методики воспроизведения, ни параметров стенда.
Забайтились на попугаев в вакууме
Жаль, PHP со Swoole не протестировали
Код на Delphi, если что
program Testtaskshabr;
uses
System.SysUtils,
System.Classes,
System.Threading;
begin
var numTasks := Integer.Parse(ParamStr(1));
var tasks: TArray<ITask> := [];
for var i := 0 to numTasks - 1 do
tasks := tasks + [TTask.Run(procedure begin Sleep(1000 * 10) end)];
TTask.WaitForAll(tasks);
end.
По моим тестам, потребление
До 10к задач: 6 мб
100к задач: 45мб
1кк: 420мб
Может ли быть дело в разных gorw-factor-ах списков? На примере с растом создавать массив как Vec::with_capacity(num_tasks)
вместо Vec::new()
. Хотя может и сам компилятор это отлавливает и оптимизирует.
Сколько памяти нужно в 2024 году для выполнения миллиона конкурентных задач?