Тестирование асинхронных методов в Visual Studio 2017 с .net 4.5+ блокирует два потока. Запуск того же кода в .net core 2.0 не блокирует

В последние несколько дней у меня была относительно болезненная проблема с головокружением.

При выполнении модульных тестов, предназначенных для проверки асинхронного поведения в Visual Studio 2017, вызовы асинхронных методов кажутся заблокированными, но ТОЛЬКО при вызовах веб-API (обещаю, сначала я локально имитировал службы, но затем мне понадобились некоторые интеграционные тесты). с API, чтобы доказать, что веб-запросы ведут себя одинаково).

В конце концов я позвонил другу, чтобы тот помог мне все обдумать, и он переписал те же самые тесты и показал, что его тесты ведут себя так, как ожидалось. Единственным отличительным фактором было то, что он писал свои тесты в VS2017 с .net Core 2.0.

Пример кода:

    [TestMethod]
    public void TestRestSharpExecuteTaskAsync()
    {
        var tasks = new List<Task>();

        Task.WhenAll(Enumerable.Range(1, 10).Select(async s =>
        {
            // Using RestSharp, but also tested using raw WebRequests
            var restClient = new RestClient("http://google.com");
            var request = new RestRequest("/", RestSharp.Method.GET);

            Debug.WriteLine(string.Format("{0} | DEBUG: BEGIN-ExecuteTaskAsync()", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff")));
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            var response = await restClient.ExecuteTaskAsync(request);
            stopwatch.Stop();
            Debug.WriteLine(string.Format("{0} | DEBUG: END-ExecuteTaskAsync() completed in {1} milliseconds", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff"), stopwatch.ElapsedMilliseconds));
        })).Wait();
    }

Что в настоящее время меня смущает, так это то, что когда этот код выполняется в VS2017 с .net 4.5+ (также протестирован 4.6.2), результаты показывают, что явно есть ожидающие задачи:

11/30/2017 10:45:45.214 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:45.253 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:45.253 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:45.253 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:45.253 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:45.254 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:45.254 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:45.254 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:45.254 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:45.254 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:45.368 | DEBUG: END-ExecuteTaskAsync() completed in 151 milliseconds
11/30/2017 10:45:45.368 | DEBUG: END-ExecuteTaskAsync() completed in 115 milliseconds
11/30/2017 10:45:45.414 | DEBUG: END-ExecuteTaskAsync() completed in 161 milliseconds
11/30/2017 10:45:45.420 | DEBUG: END-ExecuteTaskAsync() completed in 166 milliseconds
11/30/2017 10:45:45.466 | DEBUG: END-ExecuteTaskAsync() completed in 212 milliseconds
11/30/2017 10:45:45.473 | DEBUG: END-ExecuteTaskAsync() completed in 218 milliseconds
11/30/2017 10:45:45.520 | DEBUG: END-ExecuteTaskAsync() completed in 266 milliseconds
11/30/2017 10:45:45.522 | DEBUG: END-ExecuteTaskAsync() completed in 268 milliseconds
11/30/2017 10:45:45.570 | DEBUG: END-ExecuteTaskAsync() completed in 315 milliseconds
11/30/2017 10:45:45.578 | DEBUG: END-ExecuteTaskAsync() completed in 323 milliseconds

Однако при запуске в той же IDE VS2017 с проектом .net Core 2.0 задачи выполняются асинхронно, как и ожидалось:

11/30/2017 10:45:11.922 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:11.976 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:11.976 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:11.977 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:11.977 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:11.977 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:11.977 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:11.977 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:11.977 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:11.977 | DEBUG: BEGIN-ExecuteTaskAsync()
11/30/2017 10:45:12.153 | DEBUG: END-ExecuteTaskAsync() completed in 230 milliseconds
11/30/2017 10:45:12.153 | DEBUG: END-ExecuteTaskAsync() completed in 176 milliseconds
11/30/2017 10:45:12.153 | DEBUG: END-ExecuteTaskAsync() completed in 175 milliseconds
11/30/2017 10:45:12.153 | DEBUG: END-ExecuteTaskAsync() completed in 175 milliseconds
11/30/2017 10:45:12.153 | DEBUG: END-ExecuteTaskAsync() completed in 175 milliseconds
11/30/2017 10:45:12.153 | DEBUG: END-ExecuteTaskAsync() completed in 176 milliseconds
11/30/2017 10:45:12.153 | DEBUG: END-ExecuteTaskAsync() completed in 176 milliseconds
11/30/2017 10:45:12.153 | DEBUG: END-ExecuteTaskAsync() completed in 175 milliseconds
11/30/2017 10:45:12.153 | DEBUG: END-ExecuteTaskAsync() completed in 176 milliseconds
11/30/2017 10:45:12.154 | DEBUG: END-ExecuteTaskAsync() completed in 177 milliseconds

Это конфигурация IDE? Ошибка в .net 4.x? Что дает?

РЕДАКТИРОВАТЬ: проблема становится более заметной, когда я попадаю в конечную точку, с помощью которой я контролирую время отклика:

В VS2017 .net Core 2.0:

12/01/2017 12:03:29.725 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:29.770 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:29.771 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:29.771 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:29.771 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:29.771 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:29.771 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:29.771 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:29.771 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:29.772 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:34.005 | DEBUG: END-ExecuteTaskAsync() completed in 4233 milliseconds
12/01/2017 12:03:34.005 | DEBUG: END-ExecuteTaskAsync() completed in 4232 milliseconds
12/01/2017 12:03:34.005 | DEBUG: END-ExecuteTaskAsync() completed in 4233 milliseconds
12/01/2017 12:03:34.005 | DEBUG: END-ExecuteTaskAsync() completed in 4279 milliseconds
12/01/2017 12:03:34.005 | DEBUG: END-ExecuteTaskAsync() completed in 4233 milliseconds
12/01/2017 12:03:34.005 | DEBUG: END-ExecuteTaskAsync() completed in 4233 milliseconds
12/01/2017 12:03:34.005 | DEBUG: END-ExecuteTaskAsync() completed in 4233 milliseconds
12/01/2017 12:03:34.005 | DEBUG: END-ExecuteTaskAsync() completed in 4233 milliseconds
12/01/2017 12:03:34.005 | DEBUG: END-ExecuteTaskAsync() completed in 4234 milliseconds
12/01/2017 12:03:34.005 | DEBUG: END-ExecuteTaskAsync() completed in 4235 milliseconds

В VS2017 .net 4.5:

12/01/2017 12:03:49.972 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:50.010 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:50.011 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:50.011 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:50.011 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:50.011 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:50.011 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:50.011 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:50.011 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:50.011 | DEBUG: BEGIN-ExecuteTaskAsync()
12/01/2017 12:03:54.132 | DEBUG: END-ExecuteTaskAsync() completed in 4158 milliseconds
12/01/2017 12:03:54.132 | DEBUG: END-ExecuteTaskAsync() completed in 4121 milliseconds
12/01/2017 12:03:58.189 | DEBUG: END-ExecuteTaskAsync() completed in 8178 milliseconds
12/01/2017 12:03:58.191 | DEBUG: END-ExecuteTaskAsync() completed in 8180 milliseconds
12/01/2017 12:04:02.236 | DEBUG: END-ExecuteTaskAsync() completed in 12225 milliseconds
12/01/2017 12:04:02.239 | DEBUG: END-ExecuteTaskAsync() completed in 12228 milliseconds
12/01/2017 12:04:06.300 | DEBUG: END-ExecuteTaskAsync() completed in 16288 milliseconds
12/01/2017 12:04:06.302 | DEBUG: END-ExecuteTaskAsync() completed in 16290 milliseconds
12/01/2017 12:04:10.362 | DEBUG: END-ExecuteTaskAsync() completed in 20350 milliseconds
12/01/2017 12:04:10.364 | DEBUG: END-ExecuteTaskAsync() completed in 20352 milliseconds

Опять же, IDE та же, код идентичен. Прикладная разница заключается в версии .net.


person Timothy Bock    schedule 01.12.2017    source источник
comment
Здесь уже поздно, поэтому я могу упустить что-то очевидное, но как вы можете сказать из опубликованной вами статистики, что есть ожидающие задачи?   -  person Kenneth K.    schedule 01.12.2017
comment
Может быть, это слишком близко к моему сну, но я скопировал два ваших выходных образца в текстовые файлы, сравнил их в BeyondCompare, и единственные различия, которые я вижу, - это временные метки и продолжительность, что, конечно, следовало ожидать. Я не понимаю, какое отношение ваш текстовый вывод имеет к какому-либо ожиданию или выполнению асинхронности.   -  person Nathan    schedule 01.12.2017
comment
Ваш метод тестирования должен быть асинхронной задачей, а не void.   -  person MickyD    schedule 01.12.2017
comment
@KennethK., см. мою правку выше для тестовых прогонов, которые более четко показывают проблему.   -  person Timothy Bock    schedule 01.12.2017
comment
Вы должны помнить, что оба RestRequest не предназначены для потокобезопасности. Хотя я удалил некоторые общие свойства, я не уверен, что могу рекомендовать использовать один экземпляр для выполнения параллельных вызовов... Эта работа в процессе.   -  person Alexey Zimarev    schedule 06.12.2017


Ответы (1)


Блокировка не связана с async механизмами.

Судя по вашим логам в .NET 4.5 одновременно обрабатываются два запроса. Ваш код в .NET 4.5+ достигает предела 2-connection-per-host, действующего по умолчанию в System.Net:

System.Net по умолчанию использует два соединения для каждого приложения на хост.

Подробнее здесь: https://docs.microsoft.com/id-id/dotnet/framework/network-programming/best-practices-for-system-net-classes

Вы можете изменить лимит, как описано ниже. Цитата из https://social.msdn.microsoft.com/Forums/en-US/1f863f20-09f9-49a5-8eee-17a89b591007/asynchronous-httpwebrequest-maximum-connections-best-approach-threads-or-delegates?forum=netfxnetcom

Сказав, что вы можете увеличить количество подключений на хост следующим образом:

а) Увеличьте максимальное значение для ВСЕХ хостов, измените ServicePointManager.DefaultConnectionLimit.

b) Увеличьте максимальное значение для конкретного узла, чтобы получить ServicePoint для узла, вызвав ServicePointManager.FindServicePoint, а затем изменив ServicePoint.ConnectionLimit.

Обратите внимание, что

а) ВСЕ эти изменения относятся только к домену приложения и не повлияют на ограничения подключения других процессов.

б) вы конкурируете за пропускную способность сети со всеми другими приложениями или потоками в том же приложении; и если вы увеличите лимит соединения слишком сильно, вы можете голодать других. в) вам нужно поэкспериментировать, чтобы найти золотую середину; для чего должен быть лимит соединения.

см. также: Как я могу программно удалить 2 лимит соединений в WebClient

В .NET Core это ограничение не накладывается по умолчанию в соответствии с: ServicePointManager.DefaultConnectionLimit в .net ядро?

person felix-b    schedule 01.12.2017
comment
Феликс, спасибо за наводку. Похоже, что отсутствующий клей (в .net 4.x) должен был программно установить ConnectionLimit (во время расследования я предположил, что существует искусственный лимит соединения, но не смог найти никаких ссылок на то, где его нужно установить правильно) . После установки System.Net.ServicePointManager.DefaultConnectionLimit = X асинхронное поведение заработало, как и ожидалось. - person Timothy Bock; 01.12.2017