Switch new Task(()=>{ }) for Func<Task>

◇◆丶佛笑我妖孽 提交于 2020-04-30 06:31:32

问题


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(() => { })).


回答1:


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.




回答2:


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




回答3:


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


来源:https://stackoverflow.com/questions/61488316/switch-new-task-for-functask

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!