Pull to refresh

Comments 24

Я думаю, многие считают, что где-то есть какой-то заблокированный поток просто потому что сами только так и пишут… Из опыта объяснения асинхронности коллегам получается такой вот вывод.
А можете объяснить, что именно вы пытаетесь объяснить?

Потому что в вашем объяснении то и время встречаются различные потоки либо прерывания, которые так или иначе участвуют в операции ввода/вывода. Возможно какой-то отдельный поток не создается на уровне приложения, однако где-то на уровне ОС и драйверов какие-то потоки все же задействованы. И если назвать процесс записи внутри устройства потоком, то он-то как раз и жертвует собой, в то время, когда другие процессы/потоки не заняты этой операцией?)
Поток — это вполне определенный объект ядра операционной системы. Процесс записи внутри устройства не является потоком, потому что записи о нем в таблице потоков уровня ОС нет.

Автор пытается объяснить вот это: «Но никакой поток не был заблокирован, ожидая завершения операции.»
На уровне ОС и драйверов потоки действительно есть. И их, вы не поверите, ровно N штук, где N — число ядер в вашей системе. Просто потому, что каждый процессор в каждый момент времени чего-то там делает. Раз что-то там работает — вот вам и поток исполнения.

Но обслуживать «эта ваша ОС» может тысячи запросов одновременно! При этом если вы используете TUX, то ничего, кроме «ядрёных» N потоков у вас так и не будет. Если используете Nginx — будет ещё столько же «пользовательских» потоков. И всё. Там больше ничего нет!

Но такой подход используют только «настоящие джигиты». Большинство разработчиков предпочитают использовать потоки, связанны с запросами. Но нужно понимать, что как раз вот таких потоков в природе нет. Как раз потоки, связанные с запросами — это, действительно, иллюзия, создающаяся ядром ОС, рантаймом .NET и т.п. — исключительно для удобства программиста и ни для чего более.
Спасибо за уточнение.

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

Но опять же порождая массу уточнений. Каждый из потоков выполнит свою задачу (отработав ровно столько времени, сколько для этого потребуется) и освободится, и ни один из них не будет простаивать. При этом в определенных ситуациях у них все равно могут возникнуть взаимные блокировки из-за внешних ограничений (н-р пропускная способность шины). Так что говорить об абсолютной свободе или отсутствии потоков в асинхронном I/O не приходится.
Зато можно говорить о потребляемых ресурсах. Как правило, отложенная задача занимает меньше памяти, чем занимал бы отдельный поток своим стеком.
На мой взгляд это подробное рассмотрение одного из возможных примеров, чем оператор await лучше — что переключение контекста происходит не где случайно получилось, а только там, где оно нужно, притом это определяется на самом «глубинном» уровне, на котором нужность переключения контекста известна лучше всего. И при этом для верхних уровней обеспечивается полная абстракция, обеспечивающая независимость от конкретных железа и ОСей.
Если устройство поддерживает режим прямого доступа к памяти (DMA), то выполнение команда заключается всего лишь в записи адреса буфера в регистр устройства.

Но если оно не поддерживает, то поток всё же есть?
Совершенно не обязательно. Возможна отправка данных устройству по прерываниям, когда устройство усвоило очередную порцию данных, то происходит прерывание, в обработчике которого скармливается очередная порция. Просто в отсутствии поддержки DMA эти порции будут сильно меньше.
При наличии DMA оно часто тоже будет выполняться не в один DMA request. В этом плане запись по байту/слову и по N-байт через DMA (где N ограничено, например, размером DMA буфера и/или регистра для указания количества байт в данной DMA опреации) отличается только количеством прерываний, которое словит процессор. А какие кошеr'ные ошибки бывают в аппаратной реализации DMA…

пример косяков в DMA STM32F40x/41x
DMA2 data corruption when managing AHB and APB peripherals in a
concurrent way


When the DMA2 is managing AHB Peripherals (only peripherals embedding FIFOs) and also APB transfers in a concurrent way, this generates a data corruption (multiple DMA access).

When this condition occurs:
• The data transferred by the DMA to the AHB peripherals could be corrupted in case of a FIFO target.
• For memories, it will result in multiple access (not visible by the Software) and the data is not corrupted.
• For the DCMI, a multiple unacknowledged request could be generated, which implies an unknown behavior of the DMA.

AHB peripherals embedding FIFO are DCMI, CRYPTO, and HASH. On sales types without CRYPTO, only the DCMI is impacted. External FIFO controlled by the FSMC is also impacted.

www.st.com/st-web-ui/static/active/en/resource/technical/document/errata_sheet/DM00037591.pdf
Это на дотнете нет. В Mono вполне себе есть отдельный I/O поток, вечно висящий в блокирующем вызове epoll.
Справедливое замечание. Но все же 1 поток для epoll — это далеко не то же самое, что и по потоку на операцию.
Поток есть в любом случае. Просто потому, что сама концепция IOCP основана на потоках. Другое дело, что он действительно не блокируется в процессе обработки запроса. И вполне понятно почему — большинство операций ввода/вывода на физическом уровне являются асинхронными (возврат результата через прерывание). Однако посредник в виде потока всё же имеется.

А если говорить про .net, то там скорее всего будет вообще два потока — IOCP и собственно тот, в котором обрабатываем результаты.

Реальная же асинхронность без потоков возможна только если разрешить обработку прерываний из пользовательского софта (как в DOS'e), но это уже совсем другая тема…
Да, тут автор немножко схитрил. Просто один IOCP ждет завершения очень большого количества операций, а автор сосредоточился на развеивании заблуждения «один асинхронная операция — один заблокированный поток».
Просто потому, что сама концепция IOCP основана на потоках
Но при этом есть и режим Overlapped IO без IOCP — вот там используется APC и потоков и правда нет. Правда, в .NET чистый APC использовать нельзя…
Так все-таки, получается, есть поток, кроме вызывающего? Кто-то же должен все-таки запустить продолжение выполнения метода после получения результата асинхронной операции в каком-нибудь доступном потоке? А если речь не о чтении файла, а запросе к БД, кто, например, отсчитывает таймаут?
Тайм-аут отсчитывает таймер. Есть такая штука в виндах, похожа на поток, но не поток. На линуксе, наверное, это будет сигнал SIGALRM (точно не смотрел).

В случае Overlapped IO за запуск APC отвечает система. Он работает примерно как сигнал в никсах. В случае Overlapped IO через IOCP есть один выделенный поток который ждет на IOCP.
Если можно, продолжу, хотелось бы разобраться до конца в этой теме(= Все эти штуки, «похожие на поток», ну и IOCP, они же занимают процессорное время? Т. е., в простом случае однопроцессорной, одноядерной машины выполнение других задач блокируется на время запуска коллбэка, обработку из очереди IOCP задачи на запуск коллбэка или очередной тик таймера (хотя тут тоже не пойму, таймер то должен в реальном времени работать)?
Процессорное время занимает только выполняющийся код. IOCP — это системный объект, а не код. Таймер — не код.

Конечно же операционная система тратит какое-то время на обслуживание структур данных, но очень небольшое.

Колбек — это код, он, конечно же, тратит процессорное время когда до него дошло дело.
Если рассмотреть на уровне голого WinAPI без всяких ".NET", то узким местом, по-моему, является APC. В пользовательском режиме вызов APC на потоке возможен только тогда, когда этот поток заблокирован (Alertable Wait). Некрасиво. Если вы поставили асинхронную операцию в очередь и ожидаете ее завершения по уведомлению через APC — то какой-то поток должен либо ждать в режиме Alertable Wait, либо периодически опрашивать статус операции, что еще хуже, т.к. нерационально использует процессорное время.

Вместо этого лучше использовать уведомление не по APC, а по Event. Event можно сигнализировать прямо из ядра, из DPC. Ваш поток делает следующее:

1) Заказывает некоторое количество асинхронных операций ввода-вывода с уведомлением по Event
2) Выполняет какие-то другие операции, например, обрабатывает данные из завершившихся ранее операций ввода-вывода;
3) Проверяет, завершились ли какие-либо из запущенных ранее операций ввода-вывода. Если завершились — переход к п.1.
4) Если работы нет и операции ввода-вывода не завершились — ожидание с помощью WaitForMultipleObjects. Поток может быть разбужен как по завершению операций ввода-вывода, так и по другим событиям.
5) Переход к п.1.
Во-первых, как вы собираетесь проверять состояние события на 3м шаге? Насколько я знаю, это можно сделать только через ожидание с нулевым тайм-аутом, однако такое ожидание будет Alertable Wait. Таким образом, задачи «проверить, не установлено ли событие» и «проверить, не пришли ли APC» решаются одинакого — но тогда в чем принципиальная разница?

Или вы будете проверять операции ввода-вывода по одной? Но это же будет еще дольше, чем ожидание с нулевым тайм-аутом.

Во-вторых, есть такая вещь, как IO Completion Port. Фактически, это и есть то самое событие, которое вы и предлагали сделать. И, кстати, .NET, как было справедливо замечено выше, использует именно его.

В-третьих, несмотря на всю асинхронность, .NET никогда не был однопоточной платформой, и наличие отдельного потока, занимающегося ожиданием на IOCP, не является недостатком.
как вы собираетесь проверять состояние события на 3м шаге?

GetOverlappedResult. Ну, можно и через ожидание с нулевым таймаутом.
однако такое ожидание будет Alertable Wait

Зависит от флага, передаваемого функции ожидания (bAlertable). Может быть и просто ожидание без вызова APC (non-alertable)
задачи «проверить, не установлено ли событие» и «проверить, не пришли ли APC» решаются одинакого — но тогда в чем принципиальная разница?

Пожалуй, вы правы. Принципиальной разницы нет. Ну разве что, может быть, вызов GetOverlappedResult более дешев, чем вызов SleepEx с последующим вызовом APC. Но это надо исходники винды смотреть или хотя бы ReactOS.
Или вы будете проверять операции ввода-вывода по одной? Но это же будет еще дольше, чем ожидание с нулевым тайм-аутом.

Действительно, вы правы. По одной операции проверять долго. И если их запущено много — то наверно лучше пользоваться APC. Можно, конечно, назначить каждой операции по одному объекту Event и потом ждать нескольких с помощью WaitForMultipleObjects — но далеко не факт, что это будет эффективнее, чем через APC.
Зависит от флага, передаваемого функции ожидания (bAlertable). Может быть и просто ожидание без вызова APC (non-alertable)
Я имел в виду, что если делать любое ожидание — то можно заодно и «прочитать» очередь APC совершенно бесплатно.
Sign up to leave a comment.

Articles