11

Given the following code...

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

I know I can convert this to an async task as follows...

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

And that by doing so if I am calling multiple times (Task.WhenAll) everything will be faster and more efficient than perhaps using Parallel.Foreach or even calling from within a loop.

But for a minute, lets pretend that Task.Delay() does not exist and I actually have to use Thread.Sleep(); I know in reality this is not the case, but this is concept code and where the Delay/Sleep is would normally be an IO operation where there is no async option (such as early EF).

I have tried the following...

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

But, though it runs without error, according to Lucien Wischik this is in fact bad practice as it is merely spinning up threads from the pool to complete each task (it is also slower using the following console application - if you swap between DoSomethingAsync and DoSomethingAsync2 call you can see a significant difference in the time that it takes to complete)...

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);

}

I then tried the following...

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

Transplanting this in place of the original DoSomethingAsync, the test never completes and nothing is shown on screen!

I have also tried multiple other variations that either do not compile or do not complete!

So, given the constraint that you cannot call any existing asynchronous methods and must complete both the Thread.Sleep and the Console.WriteLine in an asynchronous task, how do you do it in a manner that is as efficient as the original code?

The objective here for those of you who are interested is to give me a better understanding of how to create my own async methods where I am not calling anybody elses. Despite many searches, this seems to be the one area where examples are really lacking - whilst there are many thousands of examples of calling async methods that call other async methods in turn I cannot find any that convert an existing void method to an async task where there is no call to a further async task other than those that use the Task.Run(() => {} ) method.

svick
  • 236,525
  • 50
  • 385
  • 514
Martin Robins
  • 6,033
  • 10
  • 58
  • 95
  • Your last example never completes because you never call `Start` on the task you create. – Lee Feb 15 '14 at 16:49
  • @lee: If I await new Task(()=> {}).Start() is does not compile because Task.Start() returns void. – Martin Robins Feb 15 '14 at 16:53
  • If you need to wait in an efficient manner without blocking threads you can use [TaskCompletionSource](http://msdn.microsoft.com/en-us/library/dd449174.aspx) instead. – Lee Feb 15 '14 at 16:54
  • You need to start the task before you try to `await` it. `var task = new Task(...); task.Start(); await task;` – Lee Feb 15 '14 at 16:55
  • @lee: Also tried static async Task DoSomethingAsync2(int id) { Task task = new Task(() => { Thread.Sleep(50); Console.WriteLine(@"DidSomethingAsync({0})", id); }); task.Start(); await task; } but this is just as slow as the DoSomethingAsync2 example. – Martin Robins Feb 15 '14 at 16:59
  • @lee: the TaskCompletionSource example you provide also uses Task.Factory.StartNew so it is not that different to my DoSomethingAsync2 example. – Martin Robins Feb 15 '14 at 17:01
  • `Task.Run` is a wrapper around `new Task; task.Start()` so the performance will be almost identical. The `TaskCompletionSource` example is bad, but you should wrap an existing async approach such as the `IAsyncResult` pattern. There are some static helper methods to do this for you. If your operation has no way of async invocation, then you might as well just block the current thread. – Lee Feb 15 '14 at 17:05

2 Answers2

8

There are two kinds of tasks: those that execute code (e.g., Task.Run and friends), and those that respond to some external event (e.g., TaskCompletionSource<T> and friends).

What you're looking for is TaskCompletionSource<T>. There are various "shorthand" forms for common situations so you don't always have to use TaskCompletionSource<T> directly. For example, Task.FromResult or TaskFactory.FromAsync. FromAsync is most commonly used if you have an existing *Begin/*End implementation of your I/O; otherwise, you can use TaskCompletionSource<T> directly.

For more information, see the "I/O-bound Tasks" section of Implementing the Task-based Asynchronous Pattern.

The Task constructor is (unfortunately) a holdover from Task-based parallelism, and should not be used in asynchronous code. It can only be used to create a code-based task, not an external event task.

So, given the constraint that you cannot call any existing asynchronous methods and must complete both the Thread.Sleep and the Console.WriteLine in an asynchronous task, how do you do it in a manner that is as efficient as the original code?

I would use a timer of some kind and have it complete a TaskCompletionSource<T> when the timer fires. I'm almost positive that's what the actual Task.Delay implementation does anyway.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks, but your reference to the Timer and TaskCompletionSource perhaps misses the point; the Thread.Sleep was merely an example of a long running task that I want to complete where I do not have an async method call. Maybe I should have replaced it with a long-running loop for more clarity. – Martin Robins Feb 15 '14 at 17:49
  • 3
    @MartinRobins: The question is "what makes your task long-running"? If it is actually running *code,* then the correct solution is to either just execute it synchronously or use `Task.Run` if you want to run it on a thread pool thread. If it is I/O-based, or completed as the result of an external event, then use `TaskFactory.FromAsync` or `TaskCompletionSource`. There are no other possibilities. – Stephen Cleary Feb 15 '14 at 18:03
  • Can you please elaborate on the TaskCompletionSource option as the examples I can find for this do not appear to fit in my scenario. – Martin Robins Feb 15 '14 at 21:47
  • @MartinRobins: Can you describe your scenario? TCS is used to manually trigger a task completion. You set up whatever callbacks you want (which complete the TCS), and then return the `Task` property. – Stephen Cleary Feb 15 '14 at 21:57
  • @MartinRobins, `Task.Delay` and `Task.Run`+`Thread.Sleep` are not comparable because the last one involves the creation/scheduling of a thread. Each kind scenario has its own best implementation. – Paulo Morgado Feb 16 '14 at 23:28
7

So, given the constraint that you cannot call any existing asynchronous methods and must complete both the Thread.Sleep and the Console.WriteLine in an asynchronous task, how do you do it in a manner that is as efficient as the original code?

IMO, this is a very synthetic constraint that you really need to stick with Thread.Sleep. Under this constraint, you still can slightly improve your Thread.Sleep-based code. Instead of this:

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

You could do this:

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

This way, you'd avoid an overhead of the compiler-generated state machine class. There is a subtle difference between these two code fragments, in how exceptions are propagated.

Anyhow, this is not where the bottleneck of the slowdown is.

(it is also slower using the following console application - if you swap between DoSomethingAsync and DoSomethingAsync2 call you can see a significant difference in the time that it takes to complete)

Let's look one more time at your main loop code:

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);

}

Technically, it requests 1000 tasks to be run in parallel, each supposedly to run on its own thread. In an ideal universe, you'd expect to execute Thread.Sleep(50) 1000 times in parallel and complete the whole thing in about 50ms.

However, this request is never satisfied by the TPL's default task scheduler, for a good reason: thread is a precious and expensive resource. Moreover, the actual number of concurrent operations is limited to the number of CPUs/cores. So in reality, with the default size of ThreadPool, I'm getting 21 pool threads (at peak) serving this operation in parallel. That is why DoSomethingAsync2 / Thread.Sleep takes so much longer than DoSomethingAsync / Task.Delay. DoSomethingAsync doesn't block a pool thread, it only requests one upon the completion of the time-out. Thus, more DoSomethingAsync tasks can actually run in parallel, than DoSomethingAsync2 those.

The test (a console app):

// 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();
        }

    }
}

Output:

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

Is it possible to improve the timing figure for the Thread.Sleep-based DoSomethingAsync2? The only way I can think of is to use TaskCreationOptions.LongRunning with Task.Factory.StartNew:

You should think twice before doing this in any real-life application:

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();
}

Output:

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

The timing gets better, but the price for this is high. This code asks the task scheduler to create a new thread for each new task. Do not expect this thread to come from the pool:

Task.Factory.StartNew(() =>
{
    Thread.Sleep(1000);
    Console.WriteLine("Thread pool: " + 
        Thread.CurrentThread.IsThreadPoolThread); // false!
}, TaskCreationOptions.LongRunning).Wait();
Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486