Разница между _ThreadPoolWaitCallback.PerformWaitCallback и _IOCompletionCallback.PerformIOCompletionCallback

Я пытаюсь понять пул потоков завершения iocp и io. Насколько мне известно, ввод-вывод работает как HTTP-запрос через поток завершения iocp и io для выполнения кода обратного вызова. Но я обнаружил некоторую разницу между чтением файла, запросом HTTP и запросом SQL.

class Program
{
    static void Main(string[] args)
    {
        FileAsync().Wait();
        PrintSeparate();
        SqlAsync().Wait();
        PrintSeparate();
        HttpAsync().Wait();
        Console.ReadKey();
    }

    private static async Task FileAsync()
    {
        PrintThreads($"{nameof(FileAsync)}-Entry             ");
        var buffer = new byte[1024];
        using (var file = new FileStream(@"D:\a.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynchronous))
        {
            PrintThreads($"{nameof(FileAsync)}-AfterNewFileStream");
            await file.ReadAsync(buffer, 0, 1024);
            PrintThreads($"{nameof(FileAsync)}-AfterReadAsync    ");
            PrintFirstStack();
        }
    }

    private static async Task HttpAsync()
    {
        PrintThreads($"{nameof(HttpAsync)}-Entry         ");
        var httpClient = new HttpClient();
        var response = await httpClient.GetAsync(@"https://stackoverflow.com");
        PrintThreads($"{nameof(HttpAsync)}-AfterGetAsync ");
        PrintFirstStack();
        await response.Content.ReadAsStringAsync();
        PrintThreads($"{nameof(HttpAsync)}-AfterReadAsync");
        PrintFirstStack();
    }

    private static async Task SqlAsync()
    {
        PrintThreads($"{nameof(SqlAsync)}-Entry            ");
        using (var connection = new SqlConnection("Data Source=***;Initial Catalog=t;User ID=sa; Password=***"))
        using (var command = new SqlCommand("select count(1) from aa", connection))
        {
            PrintThreads($"{nameof(SqlAsync)}-AfterNewConCmd   ");
            await connection.OpenAsync();
            PrintThreads($"{nameof(SqlAsync)}-AfterOpenAsync   ");
            PrintFirstStack();
            var o = await command.ExecuteScalarAsync();
            PrintThreads($"{nameof(SqlAsync)}-AfterExecuteAsync");
            PrintFirstStack();
        }
    }

    private static void PrintThreads(object flag)
    {
        ThreadPool.GetAvailableThreads(out var workThreads, out var ioThreads);
        PrintThreads(flag, workThreads, ioThreads);
    }

    private static void PrintThreads(object flag, int workThreads, int ioThreads)
    {
        Console.WriteLine($"[{flag}] {nameof(workThreads)}: {workThreads}, {nameof(ioThreads)}: {ioThreads}");
    }

    private static void PrintSeparate()
    {
        Console.WriteLine("\n-------------------------------------------------------------------------------\n");
    }

    private static void PrintFirstStack()
    {
        Console.WriteLine(Environment.StackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).Last());
    }
}

Вывод:

[FileAsync-Entry             ] workThreads: 2047, ioThreads: 1000
[FileAsync-AfterNewFileStream] workThreads: 2047, ioThreads: 999
[FileAsync-AfterReadAsync    ] workThreads: 2046, ioThreads: 1000
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

-------------------------------------------------------------------------------

[SqlAsync-Entry            ] workThreads: 2047, ioThreads: 1000
[SqlAsync-AfterNewConCmd   ] workThreads: 2047, ioThreads: 1000
[SqlAsync-AfterOpenAsync   ] workThreads: 2046, ioThreads: 1000
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
[SqlAsync-AfterExecuteAsync] workThreads: 2045, ioThreads: 1000
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

-------------------------------------------------------------------------------

[HttpAsync-Entry         ] workThreads: 2047, ioThreads: 1000
[HttpAsync-AfterGetAsync ] workThreads: 2047, ioThreads: 999
   at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)
[HttpAsync-AfterReadAsync] workThreads: 2047, ioThreads: 999
   at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)

Вывод показывает мне только http-запрос, использующий ioThread для выполнения обратного вызова, почему? Используют ли файл и sql код фреймворка ioThread insdie, а затем ставят обратный вызов пользовательского кода в workthreadpool?

И я тестировал несколько раз , new FileStream(..) всегда брал ioThread, что случилось?


person Hourglass    schedule 17.09.2019    source источник
comment
Фундаментальной особенностью асинхронного ввода-вывода является то, что он не обязательно должен выполняться асинхронно. Этого не произойдет, если у драйвера есть данные, доступные при запуске. Очень вероятно, что с файлом кеш файловой системы активно предварительно кэширует данные файла. Маловероятно, что это произойдет с http, Интернет довольно медленный, поэтому легко извлекать данные быстрее, чем драйвер может буферизовать. Вы не можете догадаться, как будет вести себя sql, но, скорее всего, он будет асинхронным сразу после запуска запроса. Вы никогда не сможете догадаться.   -  person Hans Passant    schedule 17.09.2019
comment
@HansPassant Я изменил sql на WAITFOR DELAY '00:00:10';select count(1) from aa;, но он все еще использовал рабочий поток. Я действительно озадачен этим.   -  person Hourglass    schedule 17.09.2019
comment
Вы не можете предположить, что запросы ввода-вывода для SQL-запросов выполняются непосредственно кодом .NET. Всегда присутствует провайдер, предоставляемый производителем базы данных и обычно написанный на C. который, вероятно, будет использовать IOCP, но вы этого не видите, поскольку он похоронен в неуправляемом коде. Платформа использует APM, поэтому завершение выполняется в рабочем потоке, а не в потоке ввода-вывода.   -  person Hans Passant    schedule 17.09.2019
comment
@HansPassant, но зачем использовать рабочий поток вместо потока ввода-вывода, они оба являются потоком пула потоков CLR. Вы сказали, что файл может иметь предварительные кеши, хорошо, я понимаю, как и await Task.Delay(), здесь нет фактической операции ввода-вывода. Когда обратный вызов ввода-вывода использует поток ввода-вывода, а когда обратный вызов ввода-вывода использует рабочий поток? Вы имеете в виду, что только привязка iocp непосредственно в коде .NET будет использовать поток ввода-вывода, а SQL может привязать iocp в библиотеке C, поэтому .net использует рабочий поток вместо потока ввода-вывода? Спасибо за ответ.   -  person Hourglass    schedule 17.09.2019
comment
referenceource.microsoft.com/#mscorlib/system/threading/   -  person Hans Passant    schedule 17.09.2019