0

In an answer to one of my other questions, I was told that use of new Task(() => { }) is not something that is a normal use case. I was advised to use Func<Task> instead. I have tried to make that work, but I can't seem to figure it out. (Rather than drag it out in the comments, I am asking a separate question here.)

My specific scenario is that I need the Task to not start right when it is declared and to be able to wait for it later.

Here is a LinqPad example using new Task(() => { }). NOTE: This works perfectly! (Except that it uses new Task.)

static async void Main(string[] args)
{
    // Line that I need to swap to a Func<Task> somehow.
    // note that this is "cold" not started task  
    Task startupDone = new Task(() => { });

    var runTask = DoStuff(() =>
    {
        //+++ This is where we want to task to "start"
        startupDone.Start();
    });

    //+++ Here we wait for the task to possibly start and finish. Or timeout.
    // Note that this times out at 1000ms even if "blocking = 10000" below.
    var didStartup = startupDone.Wait(1000);

    Console.WriteLine(!didStartup ? "Startup Timed Out" : "Startup Finished");

    await runTask;

    Console.Read();
}

public static async Task DoStuff(Action action)
{
    // Swap to 1000 to simulate starting up blocking
    var blocking = 1; //1000;
    await Task.Delay(500 + blocking);
    action();
    // Do the rest of the stuff...
    await Task.Delay(1000);
}

I tried swapping the second line with:

Func<Task> startupDone = new Func<Task>(async () => { });

But then the lines below the comments with +++ in them don't work right.

I swapped the startupDone.Start() with startupDone.Invoke().

But startupDone.Wait needs the task. Which is only returned in the lambda. I am not sure how to get access to the task outside the lambda so I can Wait for it.

How can use a Func<Task> and start it in one part of my code and do a Wait for it in another part of my code? (Like I can with new Task(() => { })).

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
Vaccano
  • 78,325
  • 149
  • 468
  • 850

3 Answers3

2

The code you posted cannot be refactored to make use of a Func<Task> instead of a cold task, because the method that needs to await the task (the Main method) is not the same method that controls the creation/starting of the task (the lambda parameter of the DoStuff method). This could make the use of the Task constructor legitimate in this case, depending on whether the design decision to delegate the starting of the task to a lambda is justified. In this particular example the startupDone is used as a synchronization primitive, to signal that a condition has been met and the program can continue. This could be achieved equally well by using a specialized synchronization primitive, like for example a SemaphoreSlim:

static async Task Main(string[] args)
{
    var startupSemaphore = new SemaphoreSlim(0);
    Task runTask = RunAsync(startupSemaphore);
    bool startupFinished = await startupSemaphore.WaitAsync(1000);
    Console.WriteLine(startupFinished ? "Startup Finished" : "Startup Timed Out");
    await runTask;
}

public static async Task RunAsync(SemaphoreSlim startupSemaphore)
{
    await Task.Delay(500);
    startupSemaphore.Release(); // Signal that the startup is done
    await Task.Delay(1000);
}

In my opinion using a SemaphoreSlim is more meaningful in this case, and makes the intent of the code clearer. It also allows to await asynchronously the signal with a timeout WaitAsync(Int32), which is not something that you get from a Task out of the box (it is doable though).

Using cold tasks may be tempting in some cases, but when you revisit your code after a month or two you'll find yourself confused, because of how rare and unexpected is to have to deal with tasks that may or may have not been started yet.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 2
    Side note: neither `await` not `.Wait` start a task (it stays cold) https://stackoverflow.com/questions/53156253/does-or-does-task-wait-not-start-the-task-if-it-isnt-already-running... I would have never guessed that behavior... – Alexei Levenkov Apr 28 '20 at 23:27
  • 1
    SemaphoreSlim worked perfectly and as you said, more clearly shows what I am trying to do. Thank you! – Vaccano Dec 01 '20 at 19:31
0

First, the type of your "do this later" object is going to become Func<Task>. Then, when the task is started (by invoking the function), you get back a Task that represents the operation:

static async void Main(string[] args)
{
  Func<Task> startupDoneDelegate = async () => { };
  Task startupDoneTask = null;

  var runTask = await DoStuff(() =>
  {
    startupDoneTask = startupDoneDelegate();
  });

  var didStartup = startupDoneTask.Wait(1000);

  Console.WriteLine(!didStartup ? "Startup Timed Out" : "Startup Finished");
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    Thank you for the answer, unfortunately when `startupDoneTask.Wait(1000)` is executed it throws a `NullReferenceException` (Because the lambda has not set the task yet.) – Vaccano Apr 28 '20 at 19:56
  • Added the `await`. Yes, you can only call `Wait` on the task after it has been created. – Stephen Cleary Apr 29 '20 at 01:44
  • Adding the `await` makes the main flow wait for the entire execution of `DoStuff()` to finish. The `startupDoneTask.Wait` call is pointless then (because not only is startup done, but so is the whole method.) Basically, adding that changes the method to do something different from the example. – Vaccano Apr 29 '20 at 22:00
0

I always try my hardest to never have blocking behavior when dealing with anything async or any type that represents potential async behavior such as Task. You can slightly modify your DoStuff to facilitate waiting on your Action.


static async void Main(string[] args)
{
    Func<CancellationToken,Task> startupTask = async(token)=>
    {
        Console.WriteLine("Waiting");
        await Task.Delay(3000, token);
        Console.WriteLine("Completed");
    };
    using var source = new CancellationTokenSource(2000);
    var runTask = DoStuff(() => startupTask(source.Token), source.Token);
    var didStartup = await runTask;
    Console.WriteLine(!didStartup ? "Startup Timed Out" : "Startup Finished");
    Console.Read();
}

public static async Task<bool> DoStuff(Func<Task> action, CancellationToken token)
{
    var blocking = 10000;
    try
    {
        await Task.Delay(500 + blocking, token);
        await action();
    }
    catch(TaskCanceledException ex)
    {
        return false;
    }
    await Task.Delay(1000);
    return true;
}

JohanP
  • 5,252
  • 2
  • 24
  • 34
  • I don't think this code guarantees that startup will take at most 1000 ms as OP's code. I.e. if `blocking` is set to 10000 the whole thing will wait 10500ms till `action` has chance to check for cancellation... – Alexei Levenkov Apr 28 '20 at 23:48
  • @AlexeiLevenkov yes, you are right, it did not timeout. I have made an edit and this now timeouts correctly, albeit due to Task.Delay, if there was actual blocking behavior, then I'm not sure how to get past that. – JohanP Apr 28 '20 at 23:55