Как преобразовать это в задачу async?

Учитывая следующий код...

static void DoSomething(int id) {
    Thread.Sleep(50);
    Console.WriteLine(@"DidSomething({0})", id);
}

Я знаю, что могу преобразовать это в async-задачу следующим образом...

static async Task DoSomethingAsync(int id) {
    await Task.Delay(50);
    Console.WriteLine(@"DidSomethingAsync({0})", id);
}

И если это произойдет, если я звоню несколько раз (Task.WhenAll), все будет быстрее и эффективнее, чем, возможно, использовать Parallel.Foreach или даже вызывать из цикла.

Но на минуту давайте сделаем вид, что Task.Delay() не существует, и мне действительно нужно использовать Thread.Sleep(); Я знаю, что на самом деле это не так, но это код концепции и где Delay/Sleep обычно является операцией ввода-вывода, где нет опции async (например, раннего EF).

Я пробовал следующее...

static async Task DoSomethingAsync2(int id) {
    await Task.Run(() => {
        Thread.Sleep(50);
        Console.WriteLine(@"DidSomethingAsync({0})", id);
    });
}

Но, хотя он работает без ошибок, согласно Lucien Wischik, это на самом деле плохая практика, поскольку она просто разворачивает потоки из пула для выполнения каждой задачи ( он также работает медленнее, используя следующее консольное приложение - если вы меняете обмен между вызовами DoSomethingAsync и DoSomethingAsync2, вы можете увидеть значительную разницу в времени, которое требуется для завершения)...

static void Main(string[] args) {
    MainAsync(args).Wait();
}

static async Task MainAsync(String[] args) {

    List<Task> tasks = new List<Task>();
    for (int i = 1; i <= 1000; i++)
        tasks.Add(DoSomethingAsync2(i)); // Can replace with any version
    await Task.WhenAll(tasks);

}

Затем я попробовал следующее...

static async Task DoSomethingAsync3(int id) {
    await new Task(() => {
        Thread.Sleep(50);
        Console.WriteLine(@"DidSomethingAsync({0})", id);
    });
}

Пересажая это вместо оригинального DoSomethingAsync, тест никогда не завершается, и на экране ничего не отображается!

Я также пробовал несколько других вариантов, которые либо не компилируются, либо не заполняются!

Итак, учитывая ограничение, которое вы не можете назвать любыми существующими асинхронными методами и должно выполнить как Thread.Sleep, так и Console.WriteLine в асинхронной задаче, как вы это сделаете так же эффективно, как и исходный код

Цель для тех из вас, кто интересуется, - дать мне лучшее представление о том, как создавать свои собственные методы асинхронного программирования, где я не звоню никому. Несмотря на многие поисковые запросы, это, по-видимому, та область, где примеров действительно не хватает - в то время как существует много тысяч примеров вызова методов async, которые вызывают другие методы async, в свою очередь, я не могу найти какие-либо преобразования существующего метода void в async-задачу, нет вызова другой асинхронной задачи, отличной от тех, которые используют метод Task.Run(() = > {}).

Ответ 1

Есть два типа задач: те, которые выполняют код (например, Task.Run и друзей), и те, которые отвечают на какое-то внешнее событие (например, TaskCompletionSource<T> и друзья).

То, что вы ищете, это TaskCompletionSource<T>. Существуют разные "сокращенные" формы для обычных ситуаций, поэтому вам не всегда нужно использовать TaskCompletionSource<T> напрямую. Например, Task.FromResult или TaskFactory.FromAsync. FromAsync чаще всего используется, если у вас есть существующая реализация вашего ввода-вывода *Begin/*End; в противном случае вы можете напрямую использовать TaskCompletionSource<T>.

Дополнительные сведения см. в разделе "Задачи, связанные с I/O-задачами" Реализация Асинхронного шаблона на основе задач.

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

Итак, учитывая ограничение, которое вы не можете назвать любыми существующими асинхронными методами и должно выполнить как Thread.Sleep, так и Console.WriteLine в асинхронной задаче, как вы это сделаете так же эффективно, как и исходный код

Я бы использовал какой-то таймер и завершаю TaskCompletionSource<T>, когда срабатывает таймер. Я почти уверен, что реальная реализация Task.Delay все равно.

Ответ 2

Итак, учитывая ограничение, которое вы не можете назвать асинхронные методы и должны выполнить как Thread.Sleep, так и Console.WriteLine в асинхронной задаче, как вы это делаете в способ, который так же эффективен, как исходный код?

IMO, это очень синтетическое ограничение, которое вам действительно нужно придерживаться Thread.Sleep. Под этим ограничением вы все равно можете немного улучшить свой код на основе Thread.Sleep. Вместо этого:

static async Task DoSomethingAsync2(int id) {
    await Task.Run(() => {
        Thread.Sleep(50);
        Console.WriteLine(@"DidSomethingAsync({0})", id);
    });
}

Вы можете сделать это:

static Task DoSomethingAsync2(int id) {
    return Task.Run(() => {
        Thread.Sleep(50);
        Console.WriteLine(@"DidSomethingAsync({0})", id);
    });
}

Таким образом, вы избегаете накладных расходов класса машины конечного автомата, созданного компилятором. Существует тонкая разница между этими двумя фрагментами кода, как распространяются исключения.

Во всяком случае, это не, где узкое место замедления.

(он также работает медленнее, используя следующее консольное приложение - если вы обмен между DoSomethingAsync и вызовом DoSomethingAsync2, вы можете увидеть значительная разница в времени, которое требуется для завершения)

Посмотрите еще раз на свой основной код цикла:

static async Task MainAsync(String[] args) {

    List<Task> tasks = new List<Task>();
    for (int i = 1; i <= 1000; i++)
        tasks.Add(DoSomethingAsync2(i)); // Can replace with any version
    await Task.WhenAll(tasks);

}

Технически, он запрашивает 1000 задач, которые должны выполняться параллельно, каждый из которых предположительно запускается в своем потоке. В идеальной вселенной вы ожидали бы выполнить Thread.Sleep(50) 1000 раз параллельно и завершить все это примерно за 50 мс.

Однако этот запрос никогда не выполняется планировщиком заданий по умолчанию TPL, по уважительной причине: нить - дорогостоящий и дорогостоящий ресурс. Более того, фактическое количество параллельных операций ограничено количеством процессоров/ядер. Таким образом, на самом деле, с размером по умолчанию ThreadPool, я получаю 21 поток пулов (на пике), обслуживающий эту операцию параллельно. Поэтому DoSomethingAsync2/Thread.Sleep занимает намного больше времени DoSomethingAsync/Task.Delay. DoSomethingAsync не блокирует поток пула, он запрашивает только один запрос на завершение тайм-аута. Таким образом, более DoSomethingAsync задачи могут выполняться параллельно, чем DoSomethingAsync2.

Тест (консольное приложение):

// https://stackoverflow.com/q/21800450/1768303

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace Console_21800450
{
    public class Program
    {
        static async Task DoSomethingAsync(int id)
        {
            await Task.Delay(50);
            UpdateMaxThreads();
            Console.WriteLine(@"DidSomethingAsync({0})", id);
        }

        static async Task DoSomethingAsync2(int id)
        {
            await Task.Run(() =>
            {
                Thread.Sleep(50);
                UpdateMaxThreads();
                Console.WriteLine(@"DidSomethingAsync2({0})", id);
            });
        }

        static async Task MainAsync(Func<int, Task> tester)
        {
            List<Task> tasks = new List<Task>();
            for (int i = 1; i <= 1000; i++)
                tasks.Add(tester(i)); // Can replace with any version
            await Task.WhenAll(tasks);
        }

        volatile static int s_maxThreads = 0;

        static void UpdateMaxThreads()
        {
            var threads = Process.GetCurrentProcess().Threads.Count;
            // not using locks for simplicity
            if (s_maxThreads < threads)
                s_maxThreads = threads;
        }

        static void TestAsync(Func<int, Task> tester)
        {
            s_maxThreads = 0;
            var stopwatch = new Stopwatch();
            stopwatch.Start();

            MainAsync(tester).Wait();

            Console.WriteLine(
                "time, ms: " + stopwatch.ElapsedMilliseconds +
                ", threads at peak: " + s_maxThreads);
        }

        static void Main()
        {
            Console.WriteLine("Press enter to test with Task.Delay ...");
            Console.ReadLine();
            TestAsync(DoSomethingAsync);
            Console.ReadLine();

            Console.WriteLine("Press enter to test with Thread.Sleep ...");
            Console.ReadLine();
            TestAsync(DoSomethingAsync2);
            Console.ReadLine();
        }

    }
}

Вывод:

Press enter to test with Task.Delay ...
...
time, ms: 1077, threads at peak: 13

Press enter to test with Thread.Sleep ...
...
time, ms: 8684, threads at peak: 21

Возможно ли улучшить график синхронизации для Thread.Sleep на основе DoSomethingAsync2? Единственный способ, которым я могу думать, это использовать TaskCreationOptions.LongRunning с Task.Factory.StartNew:

Вы должны подумать дважды, прежде чем делать это в любом реальном приложении:

static async Task DoSomethingAsync2(int id)
{
    await Task.Factory.StartNew(() =>
    {
        Thread.Sleep(50);
        UpdateMaxThreads();
        Console.WriteLine(@"DidSomethingAsync2({0})", id);
    }, TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
}

// ...

static void Main()
{
    Console.WriteLine("Press enter to test with Task.Delay ...");
    Console.ReadLine();
    TestAsync(DoSomethingAsync);
    Console.ReadLine();

    Console.WriteLine("Press enter to test with Thread.Sleep ...");
    Console.ReadLine();
    TestAsync(DoSomethingAsync2);
    Console.ReadLine();
}

Вывод:

Press enter to test with Thread.Sleep ...
...
time, ms: 3600, threads at peak: 163

Время становится лучше, но цена для этого высока. Этот код просит планировщик задач создать новый поток для каждой новой задачи. Do not ожидают, что этот поток будет поступать из пула:

Task.Factory.StartNew(() =>
{
    Thread.Sleep(1000);
    Console.WriteLine("Thread pool: " + 
        Thread.CurrentThread.IsThreadPoolThread); // false!
}, TaskCreationOptions.LongRunning).Wait();