Здравствуйте, alex_public, Вы писали:
_>Здравствуйте, Serginio1, Вы писали:
S>>>>Вместо awaite File.ReadAllTextAsync нужно создавать бэкграунд поток, передавать в него метод в котором вызывать в итоге File.ReadAllText _>>>Потому что для однопользовательского случая такая задача масштабируется гораздо лучше. S>> И чем это лучше? Ты запускаешь отдельный поток. Который нихрена не делает, только ожидает когда данные считаются.
_>И в чём проблема с этим? ) Речь же не про тысячу таких потоков, а про один. )
Ты так и не ответил чем лучше? S>>При этом морозит вспомогательный поток через объекты синхронизации до получения данных.
_>С чего бы это?
S>> При awaite File.ReadAllTextAsync данные передаются сразу в главный поток или если использовать .ConfigureAwaite(false) в любой поток. S>> Чем это маштабируется то лучше?
_>Потому что помимо просто чтения файла в реальном мире у тебя будет ещё и какая-то его минимальная обработка. Например его парсинг. Если использовать твою схему с awaite File.ReadAllTextAsync, где по твоему будет происходить обработка? Если изначальный вызов был у тебя в UI потоке, то и обработка пойдёт там же. И это даже будет нормально работать у тебя на небольших файликах. А потом пользователь загрузит туда гигабайтный файл и всё твоё приложение зависнет на неопределённое время. А для варианта с отдельным потоком, никаких проблем не будет — просто подольше будет идти обработка, но без всяких зависаний приложения.
Обработка будет либо в главном потоке при без .ConfigureAwaite(false) либо в любом потоке из пула при .ConfigureAwaite(false)
я же написал. Ты можешь регулировать, в отличие от твоего подхода.
Ну и если хочется оформи парсинг через Task и примени awaite
Шедулер сам решить где запускать или продолжить задачу https://docs.microsoft.com/ru-ru/dotnet/api/system.threading.tasks.taskcreationoptions?view=netcore-3.1 _>Ты сейчас наверное скажешь, что правильный программист сразу же выделит парсинг в отдельный поток, как потенциально длительную операцию. Вот только тогда сразу появляется простейший вопрос: а зачем нам тогда нужен асинхронный ввод-вывод, если у нас и так создаётся поток под эту задачу?
В .Net уже существуют настройки над IOCP
Не надо забывать и про кроссплатформенность. Да тот же Xamarin.Forms он кроссплатформенный, ну сервера на любой платформе
И не надо изобретать велосипед там, где он уже создан
Здравствуйте, alex_public, Вы писали:
S>>Подождите, автор говорил про iocp потоки пула, как результат работы io на самом верху (механизм передачи результата).
_>И? Какая разница, если в наше приложение данные придут не через фиксированное время после попадания их в оперативную память, а через некоторое неопределённое, когда там освободятся все нужные очереди, планировщики и т.п.? В этом смысле даже обычный синхронный api будет эффективнее.
А какое будет отличие, если железо все равно асинхронное? За исключением того, что у системы будет отъеден и будет простаивать поток,
разницы, кмк, нету.
_>Вообще в этой области есть три классических подхода:
_>1. блокировка (до выполнения задачи) _>2. опрос (о статусе выполнения задачи) _>3. асинхронный вызов (коллбэк)
_>Так вот асинхронный ввод-вывод в винде иногда по незнанию позиционируют как третий вариант, но на самом деле там чисто второй, только слегка закумуфлированный. В линухе тоже самое (epoll), но они хотя бы честно говорят об этом, а не пытаются создать видимость настоящей асинхронности.
Я правильно понимаю, что идет опрос (2) всех зарегестрированных io операций, после чего у готовых дергаетс callback (скорее всего это так)?
А как можно асинхронный вызов осуществить "по-настоящему", а не через опрос+callback? По идее, должно быть эффективнее,
ибо не надо делать O(N) проверок io готовности. Т.е. сам io объект должен дернуть callback, так? Ну тогда венда и делает (3) вариант...
S>>А кто сказал, что эти потоки ждут? Сам факт нахождения их в пуле говорит об обратном -- это всего лишь способ S>>диспетчеризации результата io в приложение. Т.е. iocp поток это не самый обычный поток, скажем так.
_>Ты похоже невнимательно прочитал документацию по IOCP. )
Не уверен, что ссылка которую дали, но вот:
The most efficient scenario occurs when there are completion packets waiting in the queue, but no waits can be satisfied because the port has reached its concurrency limit. Consider what happens with a concurrency value of one and multiple threads waiting in the GetQueuedCompletionStatus function call. In this case, if the queue always has completion packets waiting, when the running thread calls GetQueuedCompletionStatus, it will not block execution because, as mentioned earlier, the thread queue is LIFO. Instead, this thread will immediately pick up the next queued completion packet. No thread context switches will occur, because the running thread is continually picking up completion packets and the other threads are unable to run.
Здравствуйте, ononim, Вы писали:
O>для IPC был добавлен свой _драйвер_, который при переключении контекста client->server насильно выставлял affinity потока-обработчика так, чтобы он начал исполняться на том же CPU, на котором работает поток-requestor. Это дало почти двукратный рост скорости IPC на мелких запросах.
А какие именно расходы оптимизировались в этом случае, не смотрели? Подозреваю, что в первую очередь дело было в локальных данных, втянутых в локальный кэш ядра.
O>Помню я тогда еще удивился — вот есть же такая замечательная функция — SignalObjectAndWait, для которой есть спец-сисколл ZwSignalAndWaitForSingleObject. Вот почему она такую оптимизацию из коробки не делает?
Скорее всего, в общем случае (события/очереди, для которых и было придумано) это не дало бы особого эффекта, а прямая коммуникация клиент-сервер не считается настолько критичным местом, чтобы вводить такую оптимизацию. Насколько часто в типовых применениях винды встречаются подобные сценарии?
ЕМ>А какие именно расходы оптимизировались в этом случае, не смотрели? Подозреваю, что в первую очередь дело было в локальных данных, втянутых в локальный кэш ядра. ЕМ>Скорее всего, в общем случае (события/очереди, для которых и было придумано) это не дало бы особого эффекта, а прямая коммуникация клиент-сервер не считается настолько критичным местом, чтобы вводить такую оптимизацию.
Дело было еще на P4 с гипетредингом. Насколько я помню кэш у логческих ядер общий. Это во первых. А во вторых я сей эффект обнаружил играясь с юзермодной апликухой в пинг-понг парой потоков и парой ивентов (один сигналит второй ждет и наоборот). Обнаружилось, что если процессу проставить жесткий affinity на одно ядро, то количество переключений растет в разы. Так что дело явно не в кэше, а именно в том что винда шедулит другой поток на другой проц потому что текущий занят. Я хз какие там механизмы заставляют другой проц вполнять другой поток (скорее всего IPI шлется другому процессору), но эти механизмы явно имеют неслабые накладные расходы.
SignalAndWaitForSingleObject по сути должна быть оптимизирована под такой сценарий. Она там внутри сигналит ивент и не отходя от кассы — не отпуская лок шедулера — начинает ждать другой ивент. Поток который ждет ивент ставиться в очередь на исполнение в момент сигнала ивента, но ставиться на свободный проц, который не текущий, хотя точно известно, что текущий вот-вот освободиться. И если бы они сделали чтоб ожидающий поток шедулился на текущий процессор ониб ничего не потеряли как минимум.
Как много веселых ребят, и все делают велосипед...
Почитал тут топик. Как, однако, все запущено, не только у ТС.
1) async/await само по себе — вообще не про асинхронность. Асинхронность там — только как одно из мест где его можно применить. А механика — это разновидность продолжений. Все что делает await — это дает команду компилятору разрезать в этой точке императивный код и превратить его в структуру данных.
2) В дотнете есть такая штука как TPL. Вот это уже та самая асинхронность.
3) TPL, опять же, вовсе не означает реальную параллельность и многопоточность, что старый ASP.NET наглядно нам и продемонстрировал.
4) Использование связки await/TPL на практике имеет ровно два применения — параллельное (не многопоточное) выполнение в GUI средах с message loop и серверный софт.
5) В первом случае параллельность нужна чтобы не морозить отрисовку. Оно получило довольно маргинальное применение. Не потому что параллельность не нужна, а потому что порезать на куски код можно и руками, а в GUI таких кусков в 99% случаев ровно два.
6) Во втором случае это применение await на I/O операциях с освобождением воркер-потоков на время ожидания. Это сильно улучшает масштабируемость приложений. И в таком коде методы с целой пачкой await — норма. Руками такое нарезать тяжело, а результат ужасен. Поэтом тут await — must have.
7) Когда речь заходит о многопоточности, то тут await не так чтобы сильно с основными проблемами коррелирует. Тут интересен TPL в чистом виде и его модель fine grained вычислений, для которой await иногда (при сильно разнородном и сложном по структуре распараллеливаемом коде) неплохое подспорье. Но это уже совсем другой разговор.
Расходы на переключение контекста растут с каждым поколением процессоров и не мало. Точнее, нужно говорить росли. Но они измеряются все равно в тактах (сотнях и тысячах). И по факту, с учетом увеличения тактовых частот, они не только не выросли, но и уменьшились десятикратно.
Тем не менее безусловно, при наличии любой простецкой очереди — разребающий поток или потоки не должны просто отак сдаваться, а должны работать. Евгений, вполне справедливо говорит, что 1000 тактов заметить в лупу невозможно. Но если специально написать так, что бы они случались постоянно... дак а чего там считать. Прикладной код и так 20% у меня проводит в ядре, куда уж больше... а ведь это тоже переключения контекста. Прикладные задачи — это же не сумму массива посчитать, а и в ядро сбегать. А значит — переключить контекст.
Здравствуйте, Kolesiki, Вы писали:
K>В теории — да. Но как я показал, от того, что кнопка сразу "отжалась", легче никому не стало — ты всё равно обязан либо иметь механизм ожидания долгой операции, либо тупо подождать её завершения, ибо от неё зависит последущая работа (это не только про сэйв).
Верно. Вопрос только в том что это за механизм, блокирует ли он message loop и насколько он удобен.
Здравствуйте, alex_public, Вы писали:
_>А вот для этих целей запуск отдельных потоков (с использованием внутри них синхронных операций) точно будет правильнее. Не нужен тут ни асинхронный ввод-вывод, ни сопрограммы.
Это зависит от задачи. Если код простой по структуре или он coarse grained — можно и без асинков, потому что колбеков будет мало. А вот если сложный и fine grained — замучаешься пыль с ручными колбеками глотать.
Здравствуйте, Mystic Artifact, Вы писали:
НС>>Почитал тут топик. Как, однако, все запущено, не только у ТС. MA> Озвучил бы поименно, чего уж греха таить.
MA>Прикладные задачи — это же не сумму массива посчитать, а и в ядро сбегать. А значит — переключить контекст.
В ядро сбегать != переключить контекст. TLB не флашится, например. Хотя, с костылями супротив meltdown я в этом уже не уверен.
Как много веселых ребят, и все делают велосипед...
Здравствуйте, ononim, Вы писали:
O>В ядро сбегать != переключить контекст. TLB не флашится, например. Хотя, с костылями супротив meltdown я в этом уже не уверен.
Я абсолютно согласен. Это не равно, но цена тоже нехилая как правило. И да с этими *ненужными* костылями — получили что получили.
Здравствуйте, ononim, Вы писали:
O>>>Асинхронность состоит в том, что пользовательский поток вовсе не обязан все время ждать сообщения. Он может заниматься чем-то другим и периодияеки обрабатывать что там ему наприходило. _>>Вообще то GetQueuedCompletionStatus вполне себе ждёт сообщения, если их нет в очереди. ))) O>
if dwMilliseconds is INFINITE, the function will never time out. If dwMilliseconds is zero and there is no I/O operation to dequeue, the function will time out immediately.
(c) O>Но конечно же так делать далеко не всегда оптимальная стратегия. Но вполне реальная. К примеру имеем видеокодер реального времени (с видеокамеры) с внутренним буфером который он иногда сбрасывает на диск. Типа накодировал себе 16 мегабайт — проверил что предыдущая запись завершилась, и если да — запустил асинхронный сброс текущего буфера, а если нет — ну значит отращиваем буфер для следующей порции видео, видеокамера то ждать не будет. O>Но обычно эффективнее таки ждать вечно когда делать больше нечего. Имеем распаковщик архивов — он запускает асинхронное чтение, и запустив — занимается декодированной порции данных прочитанных операцией стартованной в предыдущей итерации. Как декодировали — ждем довычитки порции данных, запускаем новую операцию на будущее и декодируем нововычитанные данные. O>Но на самом деле самое интересное что можно запустить 100500 IO операций. В случае чтения с различных мест (разных файлов, разных мест однго и того же файла) это позволяет IO manager'у оптимальным образом группировать запросы в NCQ. Если кончено он умеет
Да, это реальные сценарии. Правда для их реализации даже IOCP не требуется, а достаточно обычного "overlapped" API. Я уже не говорю про обсуждаемый в данной теме async из .net, который точно работает не так. )))
Здравствуйте, ononim, Вы писали:
O>>>А ты видимо из тех коментаторов статьи которые путают APC и DPC, да? _>>Ну раз ты так считаешь, что конечно же укажешь место в моих сообщениях, где я их спутал? ) O>В твоих нет, но в первых коментах к той статье там явно начали аргументировать что DPC — это обязательно поток, похоже коментатор исходил из предположения что DPC -это APC. Но это не так. APC — шедулится на поток, DPC шедулится на процессор.
Не, к нулевому кольцу у меня вообще никаких вопросов нет. Вопросы только к 3-му кольцу. И то, претензии даже не к самой реализации, а скорее к её пониманию некоторыми программистами, наслушавшимися байек от еангелистов MS.
O>>>Тот поток который запустил асинхронную ио операцию волен делать что угодно, а не только ждать ее исполнения. К примеру он может запустить еще десяток-два таких же, но других, операций, потом помайнить немного биткоинов и потом лишь проверить результат исполнения запущенных ио операций. В этом-то и состоит асинхронность. _>>Нет, не может, потому как и в данной статье и в данной темке обсуждается не абстрактный асинхронный ввод-вывод в винде, а вполне конкретная реализация его из .net. И в ней для IOCP используется специальный пул потоков, который только этим и занимается. O>Ну, в внутренностях дотнетов я не копенгаген — б-г миловал. Но можно предположить что количество потоков в этом пуле максимум — соразмерно колву логических CPU. То есть все еще можно запустить 100500 операций — а количество потоков в пуле от этого не вырастет, просто они результаты будут все по-очереди разгребать. Условие "IO операция != поток" вполне выполняется.
Так с этим никто и не спорил. Собственно я тут первый говорил, что для тысяч одновременных операций только так и надо. Только вот некоторые программисты, услышав о страшной эффективности "асинхронщины" (и пропустив мимо ушей, что речь шла о тысячах одновременных операций), начинают считать её серебряной пулей и использовать её вообще для всего (даже для банального чтения одного конфигурационного файла). Хотя на самом деле, для большинства случаев классический синхронный ввод-вывод и проще и эффективнее.
Здравствуйте, Sharov, Вы писали:
_>>И? Какая разница, если в наше приложение данные придут не через фиксированное время после попадания их в оперативную память, а через некоторое неопределённое, когда там освободятся все нужные очереди, планировщики и т.п.? В этом смысле даже обычный синхронный api будет эффективнее. S>А какое будет отличие, если железо все равно асинхронное? За исключением того, что у системы будет отъеден и будет простаивать поток, S>разницы, кмк, нету.
Смотри, есть реальная асинхронность (как в МК): при попадание байта в сетевую карту мгновенно (ну на самом деле перед этим процессор выполняет ещё кусок кода, но строго фиксированного) выполняется наш код обработки этого байта. Есть классический синхронный код: пользовательский поток блокируется после запуска операции, при приходе соответствующего байта в сетевую карту, ядро ОС ставит флаг, разблокирующий поток и через какое-то время планировщик потоков при возможности запускает код обработки этого байта. А так называемый асинхронный код в Windows работает для одной одновременной операции в точности как описанный выше синхронный код (в качестве ждущего потока сидит не пользовательский, а iocp поток), только плюс ещё куча ненужных сервисных операций (связанных с очередью), плюс ещё возможно надо будет подождать, когда освободится от текущих задач пользовательский поток (т.к. у нас там сопрограмма и поток может быть занят другими сопрограммами).
_>>Вообще в этой области есть три классических подхода: _>>1. блокировка (до выполнения задачи) _>>2. опрос (о статусе выполнения задачи) _>>3. асинхронный вызов (коллбэк) _>>Так вот асинхронный ввод-вывод в винде иногда по незнанию позиционируют как третий вариант, но на самом деле там чисто второй, только слегка закумуфлированный. В линухе тоже самое (epoll), но они хотя бы честно говорят об этом, а не пытаются создать видимость настоящей асинхронности. S>Я правильно понимаю, что идет опрос (2) всех зарегестрированных io операций, после чего у готовых дергаетс callback (скорее всего это так)?
В асинхронной модели Линуха (ну она на самом деле не его, это я так, для краткости) — что-то типа того (если нет готовых, то обычно блокируемся до их появлениях). А в винде у нас просто есть очередь с сообщениями о завершение операций. Можно проверять (концептуально ничем не отличается от опроса) есть ли в очереди сообщения (если их нет, то тоже обычно блокируемся).
S>А как можно асинхронный вызов осуществить "по-настоящему", а не через опрос+callback? По идее, должно быть эффективнее, S>ибо не надо делать O(N) проверок io готовности. Т.е. сам io объект должен дернуть callback, так? Ну тогда венда и делает (3) вариант...
По настоящему, это если бы ядро винды запускала callback в пространстве целевого процесса непосредственно после обработки данных драйвером. Вне каких-либо потоков и их стеков. Приблизительно как работают сигналы в Линухе. Но это слишком сложный и опасный инструмент для прикладного программиста — лучше уж пускай реакция будет хуже (латентность выше), но зато код обработчика будет выполняться в понятном контексте прикладного потока.
_>>Ты похоже невнимательно прочитал документацию по IOCP. ) S>Не уверен, что ссылка которую дали, но вот: S>
S>The most efficient scenario occurs when there are completion packets waiting in the queue, but no waits can be satisfied because the port has reached its concurrency limit. Consider what happens with a concurrency value of one and multiple threads waiting in the GetQueuedCompletionStatus function call. In this case, if the queue always has completion packets waiting, when the running thread calls GetQueuedCompletionStatus, it will not block execution because, as mentioned earlier, the thread queue is LIFO. Instead, this thread will immediately pick up the next queued completion packet. No thread context switches will occur, because the running thread is continually picking up completion packets and the other threads are unable to run.
Так это как раз описан случай тысяч одновременных операций, уведомления о завершение которых наш пул потоков просто не успевает обрабатывать и поэтому никогда не блокируется. Вот как раз для таких задач и требуется применять асинхронный ввод-вывод, о чём здесь не раз говорили. А вот в случае одной одновременной задачи у тебя там вполне себе будут заблокированные потоки (и скорее всего их будет даже больше, чем в случае обычного синхронного запроса — я не в курсе какой размер пула iocp потоков в .net по умолчанию, но весьма вероятно что больше 1).
Здравствуйте, Ночной Смотрящий, Вы писали:
_>>А вот для этих целей запуск отдельных потоков (с использованием внутри них синхронных операций) точно будет правильнее. Не нужен тут ни асинхронный ввод-вывод, ни сопрограммы. НС>Это зависит от задачи. Если код простой по структуре или он coarse grained — можно и без асинков, потому что колбеков будет мало. А вот если сложный и fine grained — замучаешься пыль с ручными колбеками глотать.
А откуда вообще колбеки возьмутся то? У нас есть некая длительная задача, которую нужно выполнить без заморозки главного потока. Ну так просто запускаем для этого фоновый поток, делаем в нём все нужные нам задачи (через синхронное АПИ ввода-вывода) со сложной логикой и потом просто уведомляем сообщением главный поток (чтобы он например разблокировал соответствующую кнопку в GUI и т.п.) о завершение работы.
Здравствуйте, alex_public, Вы писали:
_>А откуда вообще колбеки возьмутся то? У нас есть некая длительная задача, которую нужно выполнить без заморозки главного потока. Ну так просто запускаем для этого фоновый поток, делаем в нём все нужные нам задачи (через синхронное АПИ ввода-вывода) со сложной логикой и потом просто уведомляем сообщением главный поток (чтобы он например разблокировал соответствующую кнопку в GUI и т.п.) о завершение работы.
Мне как раз париходится и синхронный код с отдельным потоком писать и async awaite.
Отгадай с одного раза где приходится плеваться?
и солнце б утром не вставало, когда бы не было меня
Здравствуйте, alex_public, Вы писали:
_>А откуда вообще колбеки возьмутся то? У нас есть некая длительная задача, которую нужно выполнить без заморозки главного потока.
Это если тебе в этой задаче не нужно что то периодически от пользователя или сети.
_> Ну так просто запускаем для этого фоновый поток, делаем в нём все нужные нам задачи (через синхронное АПИ ввода-вывода) со сложной логикой и потом просто уведомляем сообщением главный поток (чтобы он например разблокировал соответствующую кнопку в GUI и т.п.) о завершение работы.
А если в процессе нужно спросить у пользователя что то?