Почему запуск сотни задач async занимает больше времени, чем запуск сотен потоков?
Почему запуск сотни задач async занимает больше времени, чем запуск сотен потоков?
У меня есть следующий тестовый класс:
public class AsyncTests
{
public void TestMethod1()
{
var tasks = new List<Task>();
for (var i = 0; i < 100; i++)
{
var task = new Task(Action);
tasks.Add(task);
task.Start();
}
Task.WaitAll(tasks.ToArray());
}
public void TestMethod2()
{
var threads = new List<Thread>();
for (var i = 0; i < 100; i++)
{
var thread = new Thread(Action);
threads.Add(thread);
thread.Start();
}
foreach (var thread in threads)
{
thread.Join();
}
}
private void Action()
{
var task1 = LongRunningOperationAsync();
var task2 = LongRunningOperationAsync();
var task3 = LongRunningOperationAsync();
var task4 = LongRunningOperationAsync();
var task5 = LongRunningOperationAsync();
Task[] tasks = {task1, task2, task3, task4, task5};
Task.WaitAll(tasks);
}
public async Task<int> LongRunningOperationAsync()
{
var sw = Stopwatch.StartNew();
await Task.Delay(500);
Debug.WriteLine("Completed at {0}, took {1}ms", DateTime.Now, sw.Elapsed.TotalMilliseconds);
return 1;
}
}
Насколько можно судить, TestMethod1
и TestMethod2
должны делать то же самое. Один использует TPL, два используют простые ванильные нити. Один занимает 1:30 минут, два - 0,54 секунды.
Почему?
Ответы
Ответ 1
Метод Action
в настоящее время блокируется с использованием Task.WaitAll(tasks)
. При использовании Task
по умолчанию используется ThreadPool
, это означает, что вы блокируете общие потоки ThreadPool
.
Попробуйте следующее, и вы увидите эквивалентную производительность:
-
Добавьте неблокирующую реализацию Action
, мы будем называть ее ActionAsync
private Task ActionAsync()
{
var task1 = LongRunningOperationAsync();
var task2 = LongRunningOperationAsync();
var task3 = LongRunningOperationAsync();
var task4 = LongRunningOperationAsync();
var task5 = LongRunningOperationAsync();
Task[] tasks = {task1, task2, task3, task4, task5};
return Task.WhenAll(tasks);
}
-
Измените TestMethod1
, чтобы правильно обработать новый возвращаемый ActionAsync
метод ActionAsync
public void TestMethod1()
{
var tasks = new List<Task>();
for (var i = 0; i < 100; i++)
{
tasks.Add(Task.Run(new Func<Task>(ActionAsync)));
}
Task.WaitAll(tasks.ToArray());
}
Причина, по которой у вас была низкая производительность, заключается в том, что ThreadPool
будет "медленно" создавать новые потоки, если это необходимо, если вы блокируете несколько потоков, доступных у вас, вы столкнетесь с заметным замедлением. Вот почему ThreadPool
предназначен только для выполнения коротких задач.
Если вы намереваетесь выполнить длинную операцию блокировки с помощью Task
, тогда не забудьте использовать TaskCreationOptions.LongRunning
при создании экземпляра Task
(это создаст новый базовый Thread
вместо использования ThreadPool
)..
Некоторое дополнительное доказательство проблемы ThreadPool
, следующее также устраняет вашу проблему (НЕ используйте это):
ThreadPool.SetMinThreads(500, 500);
Это демонстрирует, что "медленное" нерестование новых потоков ThreadPool
вызывало ваше узкое место.
Ответ 2
Задачи выполняются по потокам из threadpool. Threadpool как ограниченное количество потоков, которые повторно используются. Вся задача или все запрошенные действия ставятся в очередь и выполняются этими потоками, когда они неактивны.
Предположим, что ваш threadpool имеет 10 потоков, и вы ожидаете 100 задач, затем выполняются 10 задач, а остальные 90 задач просто ждут в очереди до тех пор, пока не будут завершены первые 10 задач.
Во втором методе тестирования вы создаете 100 потоков, посвященных их задачам. Поэтому вместо 10 потоков, выполняемых одновременно, выполняется 100 потоков.