Sequential processing of asynchronous tasks

前端 未结 5 574
旧时难觅i
旧时难觅i 2020-12-08 02:53

Assume the following synchronous code:

try
{
    Foo();
    Bar();
    Fubar();
    Console.WriteLine(\"All done\");
}
catch(Exception e) // For illustration         


        
相关标签:
5条回答
  • 2020-12-08 02:58

    Now, I haven't really used the TPL much, so this is just a stab in the dark. And given what @Servy mentioned, perhaps this won't run completely asynchronously. But I figured I'd post it and if it's way off the mark, you can downvote me to oblivion or I can have it deleted (or we can just fix what needs fixing)

    public void RunAsync(Action onComplete, Action<Exception> errorHandler, params Action[] actions)
    {
        if (actions.Length == 0)
        {
            //what to do when no actions/tasks provided?
            onComplete();
            return;
        }
    
        List<Task> tasks = new List<Task>(actions.Length);
        foreach(var action in actions)
        {
            Task task = new Task(action);
            task.ContinueWith(t => errorHandler(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
            tasks.Add(task);
        }
    
        //last task calls onComplete
        tasks[actions.Length - 1].ContinueWith(t => onComplete(), TaskContinuationOptions.OnlyOnRanToCompletion);
    
        //wire all tasks to execute the next one, except of course, the last task
        for (int i = 0; i <= actions.Length - 2; i++)
        {
            var nextTask = tasks[i + 1];
            tasks[i].ContinueWith(t => nextTask.Start(), TaskContinuationOptions.OnlyOnRanToCompletion);
        }
    
        tasks[0].Start();
    }
    

    And it would have usage like:

    RunAsync(() => Console.WriteLine("All done"),
                ex => Console.WriteLine(ex),
                Foo,
                Bar,
                Fubar);
    

    Thoughts? Downvotes? :)

    (I definitely prefer async/await though)

    EDIT: Based on your comments to take Func<Task>, would this be a proper implementation?

    public void RunAsync(Action onComplete, Action<Exception> errorHandler, params Func<Task>[] actions)
    {
        if (actions.Length == 0)
        {
            //what to do when no actions/tasks provided?
            onComplete();
            return;
        }
    
        List<Task> tasks = new List<Task>(actions.Length);
        foreach (var action in actions)
        {
            Func<Task> nextActionFunc = action;
            Task task = new Task(() =>
            {
                var nextTask = nextActionFunc();
                nextTask.ContinueWith(t => errorHandler(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
                nextTask.Start();
            });
            tasks.Add(task);
        }
    
        //last task calls onComplete
        tasks[actions.Length - 1].ContinueWith(t => onComplete(), TaskContinuationOptions.OnlyOnRanToCompletion);
    
        //wire all tasks to execute the next one, except of course, the last task
        for (int i = 0; i <= actions.Length - 2; i++)
        {
            var nextTask = tasks[i + 1];
            tasks[i].ContinueWith(t => nextTask.Start(), TaskContinuationOptions.OnlyOnRanToCompletion);
        }
    
        tasks[0].Start();
    }
    
    0 讨论(0)
  • 2020-12-08 03:06

    What you have here is essentially a ForEachAsync. You want to run each async item, sequentially, but with some error handling support. Here is one such implementation:

    public static Task ForEachAsync(IEnumerable<Func<Task>> tasks)
    {
        var tcs = new TaskCompletionSource<bool>();
    
        Task currentTask = Task.FromResult(false);
    
        foreach (Func<Task> function in tasks)
        {
            currentTask.ContinueWith(t => tcs.TrySetException(t.Exception.InnerExceptions)
                , TaskContinuationOptions.OnlyOnFaulted);
            currentTask.ContinueWith(t => tcs.TrySetCanceled()
                    , TaskContinuationOptions.OnlyOnCanceled);
            Task<Task> continuation = currentTask.ContinueWith(t => function()
                , TaskContinuationOptions.OnlyOnRanToCompletion);
            currentTask = continuation.Unwrap();
        }
    
        currentTask.ContinueWith(t => tcs.TrySetException(t.Exception.InnerExceptions)
                , TaskContinuationOptions.OnlyOnFaulted);
        currentTask.ContinueWith(t => tcs.TrySetCanceled()
                , TaskContinuationOptions.OnlyOnCanceled);
        currentTask.ContinueWith(t => tcs.TrySetResult(true)
                , TaskContinuationOptions.OnlyOnRanToCompletion);
    
        return tcs.Task;
    }
    

    I added in support for canceled tasks as well, just to be more general and because it took so little to do.

    It adds each task as a continuation of the previous task, and all along the line it ensures that any exceptions result in the final task's exception being set.

    Here is an example usage:

    public static Task FooAsync()
    {
        Console.WriteLine("Started Foo");
        return Task.Delay(1000)
            .ContinueWith(t => Console.WriteLine("Finished Foo"));
    }
    
    public static Task BarAsync()
    {
        return Task.Factory.StartNew(() => { throw new Exception(); });
    }
    
    private static void Main(string[] args)
    {
        List<Func<Task>> list = new List<Func<Task>>();
    
        list.Add(() => FooAsync());
        list.Add(() => FooAsync());
        list.Add(() => FooAsync());
        list.Add(() => FooAsync());
        list.Add(() => BarAsync());
    
        Task task = ForEachAsync(list);
    
        task.ContinueWith(t => Console.WriteLine(t.Exception.ToString())
            , TaskContinuationOptions.OnlyOnFaulted);
        task.ContinueWith(t => Console.WriteLine("Done!")
            , TaskContinuationOptions.OnlyOnRanToCompletion);
    }
    
    0 讨论(0)
  • 2020-12-08 03:11

    Just for the sake of completeness, that's how I would implement the helper method suggested by Chris Sinclair:

    public void RunSequential(Action onComplete, Action<Exception> errorHandler,
                              params Func<Task>[] actions)
    {
        RunSequential(onComplete, errorHandler,
                      actions.AsEnumerable().GetEnumerator());
    }
    
    public void RunSequential(Action onComplete, Action<Exception> errorHandler,
                              IEnumerator<Func<Task>> actions)
    {
        if(!actions.MoveNext())
        {
            onComplete();
            return;
        }
    
        var task = actions.Current();
        task.ContinueWith(t => errorHandler(t.Exception),
                          TaskContinuationOptions.OnlyOnFaulted);
        task.ContinueWith(t => RunSequential(onComplete, errorHandler, actions),
                          TaskContinuationOptions.OnlyOnRanToCompletion);
    }
    

    This ensures that each subsequent task is only requested when the previous one completed successfully.
    It assumes that the Func<Task> returns an already running task.

    0 讨论(0)
  • 2020-12-08 03:14

    Here's how it would work with async:

    try
    {
        await FooAsync();
        await BarAsync();
        await FubarAsync();
        Console.WriteLine("All done");
    }
    catch(Exception e) // For illustration purposes only. Catch specific exceptions!
    {
        Console.WriteLine(e);
    }
    

    This would work on .NET 4.0 if you installed the (prerelease) Microsoft.Bcl.Async package.


    Since you're stuck on VS2010, you can use a variant of Stephen Toub's Then:

    public static Task Then(this Task first, Func<Task> next)
    {
      var tcs = new TaskCompletionSource<object>();
      first.ContinueWith(_ =>
      {
        if (first.IsFaulted) tcs.TrySetException(first.Exception.InnerExceptions);
        else if (first.IsCanceled) tcs.TrySetCanceled();
        else
        {
          try
          {
            next().ContinueWith(t =>
            {
              if (t.IsFaulted) tcs.TrySetException(t.Exception.InnerExceptions);
              else if (t.IsCanceled) tcs.TrySetCanceled();
              else tcs.TrySetResult(null);
            }, TaskContinuationOptions.ExecuteSynchronously);
          }
          catch (Exception exc) { tcs.TrySetException(exc); }
        }
      }, TaskContinuationOptions.ExecuteSynchronously);
      return tcs.Task; 
    }
    

    You can use it as such:

    var task = FooAsync().Then(() => BarAsync()).Then(() => FubarAsync());
    task.ContinueWith(t =>
    {
      if (t.IsFaulted || t.IsCanceled)
      {
        var e = t.Exception.InnerException;
        // exception handling
      }
      else
      {
        Console.WriteLine("All done");
      }
    }, TaskContinuationOptions.ExcecuteSynchronously);
    

    Using Rx, it would look like this (assuming you don't have the async methods already exposed as IObservable<Unit>):

    FooAsync().ToObservable()
        .SelectMany(_ => BarAsync().ToObservable())
        .SelectMany(_ => FubarAsync().ToObservable())
        .Subscribe(_ => { Console.WriteLine("All done"); },
            e => { Console.WriteLine(e); });
    

    I think. I'm not an Rx master, by any means. :)

    0 讨论(0)
  • 2020-12-08 03:22

    You should be able to create a method to combine two tasks, and only start the second if the first succeeds.

    public static Task Then(this Task parent, Task next)
    {
        TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
        parent.ContinueWith(pt =>
        {
            if (pt.IsFaulted)
            {
                tcs.SetException(pt.Exception.InnerException);
            }
            else
            {
                next.ContinueWith(nt =>
                {
                    if (nt.IsFaulted)
                    {
                        tcs.SetException(nt.Exception.InnerException);
                    }
                    else { tcs.SetResult(null); }
                });
                next.Start();
            }
        });
        return tcs.Task;
    }
    

    you can then chain tasks together:

    Task outer = FooAsync()
        .Then(BarAsync())
        .Then(FubarAsync());
    
    outer.ContinueWith(t => {
        if(t.IsFaulted) {
            //handle exception
        }
    });
    

    If your tasks are started immediately you can just wrap them in a Func:

    public static Task Then(this Task parent, Func<Task> nextFunc)
    {
        TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
        parent.ContinueWith(pt =>
        {
            if (pt.IsFaulted)
            {
                tcs.SetException(pt.Exception.InnerException);
            }
            else
            {
                Task next = nextFunc();
                next.ContinueWith(nt =>
                {
                    if (nt.IsFaulted)
                    {
                        tcs.SetException(nt.Exception.InnerException);
                    }
                    else { tcs.SetResult(null); }
                });
            }
        });
        return tcs.Task;
    }
    
    0 讨论(0)
提交回复
热议问题