Pull to refresh
52
0
Pavel Romash @ZloyChert

C# & .NET Monk

Send message

Хорошо, был неправ. hEvent не используется

Неправда, есть.

Вот в C# типе для Overlapped
https://github.com/dotnet/runtime/blob/7bfc61b970e28f94782ef7c0cbcbbbc94ef9f5eb/src/coreclr/System.Private.CoreLib/src/System/Threading/Overlapped.cs#L70

А вот и конструктор OverlappedValueTaskSource, где видно, какой колбэк используется.
https://github.com/dotnet/runtime/blob/7bfc61b970e28f94782ef7c0cbcbbbc94ef9f5eb/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs#L66

В колбэке как раз вызывается этот метод. https://github.com/dotnet/runtime/blob/7bfc61b970e28f94782ef7c0cbcbbbc94ef9f5eb/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs#L200
В нем видно, как ставится результат операции.

Я считаю, это вполне достаточно в статье об асинхронности в C#.
Углубляясь дальше, можно потерять суть и уйти в дебри ОС и устройства дисков

Забыл как отвечать на комментарии и ответил в комментарии к посту, а не в ответах.
https://habr.com/ru/post/470830/#comment_24252403

Вы путаете разные понятия.

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

Вы правы насчёт использования ИО портов. Это и не отрицается. Просто данная структура - это то, с помощью чего они используются.

https://docs.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports

https://docs.microsoft.com/en-us/previous-versions/windows/desktop/msmq/ms706972(v=vs.85)

Вот некоторые примеры. В статье описывается асинхронность. И то, как она происходит и из чего рождается. Структура Overlapped участвует в этом процессе и обьясняет, как вызывается продолжение асинхронной операции.

Копать вглубь можно долго. Так можно и за порты уйти. К контроллеру диска и его драйверу в ОС.

Да, верно. Не совсем удачно написал, решил не загромождать постоянными вариациями для void и других возвращаемых. Поправлю в ближайшее свободное.
Зависит от конкретной задачи. В ряде случаев внутри вызванной вами асинхронной операции работа будет отправлена на выполнение в пул потоков, т.е. над ней будет работать отдельный поток. В случаях с операциями ввода-вывода действия по чтению-записи могут быть делегированы на контроллер устройства, не занимая лишних потоков.
Поток выполняется как обычно, далее вызывает асинхронную операцию. Она начинает выполняться одним из вышеперечисленных способов. В вызвавший ее поток возвращается Task — индикатор завершенности этой операции. Далее поток просто засыпает на 1.7с. после того, как он просыпается он встречает await и проверяет, завершена ли операция, представленная тем Task. В данном случае да, завершена. Поэтому мы можем продолжать выполнение в этом же потоке как обычно.
Возвращаемое значение не влияет на асинхронность. Если есть метод, возвращающий void, то его асинхронность зависит от наличия внутри вызовов асинхронных операций с применением await для ожидания их завершения.
Ну в теории да, если кто-то пишет странные методы вроде этого (не дожидаясь запущенных задач).
    public static async Task Main()
    {
        await BlackBox.Method1Async();
        await BlackBox.Method2Async();
        Thread.Sleep(3000);
    }

    public static class BlackBox
    {
        public static async Task Method1Async()
        {
            Console.WriteLine($"Method1Async 1");
            await Task.Delay(500);
            Console.WriteLine($"Method1Async 2");
            Task.Run(async () => // не дождались
            {
                await Task.Delay(500);
                Console.WriteLine($"Method1Async 3");
            });
        }

        public static async Task Method2Async()
        {
            Console.WriteLine($"Method2Async 1");
            await Task.Delay(500);
            Console.WriteLine($"Method2Async 2");
        }
    }

Но в нормальных ситуациях — нет, во всяком случае мне на ум ничего не приходит.
Одна задача многими потоками: обычный асинхронный метод, который выполняется реально асинхронно. Его продолжение может быть выполнено на потоке, отличном от первоначального. В то же время код, который вызывает этот метод не думает об этом. С его стороны — это логически едина язадача, над которой поработало несколько потоков. Примерно это я имел в виду
Под задаче подразумевал те действия, состояние которых отражено в объекте Task. Как-то так. Поток — просто поток выполнения, насколько я помню, нигде не опирался на конкретные детали System.Threading.Thread.
Если есть вопросы конкретно по материалу, задавайте. Мне тогда будет легче пояснить
Ну кроме редких случаев да, вы правы. После первого же await уже выполнение будет возложено на пул. Разве что вы долго и упорно что-то делаете и в конце асинхронно, допустим, отсылаете результаты по сети. Тогда смысл есть.
Пример данного поведения приведен в главе с машиной состояний и ее кодом. Далее следует маленький пример с Thread.Sleep. Там мы запускаем задачу, которая выполняется секунду, далее засыпает на 1.7 секунды и ожидаем запущенную задачу. Т.к. мы спали достаточно долго и запущенная задача уже выполнилась, то нет смысла присоединять продолжения и т.д. Мы просто продолжаем выполнение, т.к. результат к этому моменту доступен. Если вы измените засыпание с 1.7 на что-нибудь меньше секунды (время за которое выполняется задача), то продолжение выполнится в другом потоке, т.е. на консоль будут выведены разные числа.
В реальном мире так обычно бывает, когда операция выполняется в ряде случаев сразу, а в остальных уже дольше. Пример тому файлы (которые я описывал) — если мы записываем достаточно малый (влезающий в буфер) объем данных — то продолжение никуда не присоединяется, т.к. операция завершается синхронно. Если мы пишем большой объем данных, то операция выполняется асинхронно и продолжение будет присоединено к еще не завершенной задаче. Вот еще вам пример с файлами
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
int bufferSize = 4096; // менять здесь. Можно менять или размер буфера, или данных.
int dataSize = 2000; //Если bufferSize > dataSize, то продолжение выполняется синхронно в том же потоке. В противном случае - в другом
using (var fs = new FileStream("aswt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, bufferSize, true))
{
        var ar = new byte[dataSize];
        new Random().NextBytes(ar);
        await fs.WriteAsync(ar);
}
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Видимо, вы не так поняли. Имелось в виду, что помечать метод async, если в нем нет await не имеет смысла. Метод преобразуется в заглушку, будет сгенерирована стейт-машина, но это будет сделано зря, т.к. нигде не будет присоединено продолжений. Компилятор при этом выдаст предупреждение «В данном асинхронном методе отсутствуют операторы await ...».
Конечно, спасибо за исправление, я что-то тупанул и везде lang=«csharp» поставил

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

На то оно и предположение было. Спасибо за уточнение, надо будет поправить в статье.

Я вроде писал где-то, что все примеры для 32 бит. Сейчас перечитаю, мб в дисклеймер внесу. Но вы правы, конечно.
Справедливости ради, в .NET Core еще и структура таблицы методов поменялась. Общая схема работы осталась, но конкретные смещения уже не те.

Да, согласен. Как доберусь до компутатора, сделаю где-нибудь пометку

Обдумывал и такой вариант. Теоретически, можно даже просто переменных нужное количество завести, но это не динамически. Тут, конечно, мой косяк, поленился написать нормальный метод main. Просто суть в том, что так я могу выделить на стеке память под любой размер класса (то бишь для любого класса). А в варианте со структурами (или кучей переменных) придется заводить их на каждый случай жизни, никакой динамики.
А так да, если подобрать нужную структуру, то должно получиться!
Как я написал в самом начале: «Это просто расширение границ, в которых воспринимается язык программирования». То есть интересный пример, показывающий некоторые аспекты внутреннего устройства
Как я выше упоминал, я использовал тот ресурс для получения кода Ассемблера. А они видимо решили использовать 32 бита. В этом плане я даже и согласен с ними. Это классика — 32 бита на ссылку и прочее.
Когда я использовал windbg я наблюдал 64 битный код.
Как я знаю, от Int32 он не зависит, как известно, int — просто элиас для Int32. А число 32 в названии Int32 означает, что мы используем 32 бита для представления числа. Ведь Int64 (он же long) доступен не только на 64 битной платформе. Мы, как разработчики, не должны об этом знать. Об этом должна заботиться среда, а мы просто работаем с 32 или 64 битным числом. И т.к. мы кроссплатформенны, это не наше дело, свалим это на плечи JIT'a
1

Information

Rating
Does not participate
Location
Украина
Registered
Activity