任务排序和重新进入

时间:2014-01-29 06:52:47

标签: c# .net asynchronous task-parallel-library async-await

我有以下情况,我认为可能很常见:

  1. 有一个任务(一个UI命令处理程序)可以同步或异步完成。

  2. 命令的到达时间可能比处理它们的速度快。

  3. 如果命令已有待处理任务,则应对新命令处理程序任务进行排队并按顺序处理。

  4. 每个新任务的结果可能取决于上一个任务的结果。

  5. 应该遵守取消规定,但为了简单起见,我想将其排除在本问题的范围之外。此外,线程安全(并发)不是必需的,但必须支持重入。

    这是我想要实现的基本示例(为了简单起见,作为控制台应用程序):

    using System;
    using System.Threading.Tasks;
    
    namespace ConsoleApp
    {
        class Program
        {
            static void Main(string[] args)
            {
                var asyncOp = new AsyncOp<int>();
    
                Func<int, Task<int>> handleAsync = async (arg) =>
                {
                    Console.WriteLine("this task arg: " + arg);
    
                    //await Task.Delay(arg); // make it async
    
                    return await Task.FromResult(arg); // sync
                };
    
                Console.WriteLine("Test #1...");
                asyncOp.RunAsync(() => handleAsync(1000));
                asyncOp.RunAsync(() => handleAsync(900));
                asyncOp.RunAsync(() => handleAsync(800));
                asyncOp.CurrentTask.Wait();
    
                Console.WriteLine("\nPress any key to continue to test #2...");
                Console.ReadLine();
    
                asyncOp.RunAsync(() =>
                {
                    asyncOp.RunAsync(() => handleAsync(200));
                    return handleAsync(100);
                });
    
                asyncOp.CurrentTask.Wait();
                Console.WriteLine("\nPress any key to exit...");
                Console.ReadLine();
            }
    
            // AsyncOp
            class AsyncOp<T>
            {
                Task<T> _pending = Task.FromResult(default(T));
    
                public Task<T> CurrentTask { get { return _pending; } }
    
                public Task<T> RunAsync(Func<Task<T>> handler)
                {
                    var pending = _pending;
                    Func<Task<T>> wrapper = async () =>
                    {
                        // await the prev task
                        var prevResult = await pending;
                        Console.WriteLine("\nprev task result:  " + prevResult);
                        // start and await the handler
                        return await handler();
                    };
    
                    _pending = wrapper();
                    return _pending;
                }
            }
    
        }
    }
    

    输出:

    Test #1...
    
    prev task result:  0
    this task arg: 1000
    
    prev task result:  1000
    this task arg: 900
    
    prev task result:  900
    this task arg: 800
    
    Press any key to continue to test #2...
    
    
    prev task result:  800
    
    prev task result:  800
    this task arg: 200
    this task arg: 100
    
    Press any key to exit...
    

    它按照要求工作,直到在测试#2中引入重新引入:

    asyncOp.RunAsync(() =>
    {
        asyncOp.RunAsync(() => handleAsync(200));
        return handleAsync(100);
    });
    

    所需的输出应为100200,而不是200100,因为100已有待处理的外部任务。这显然是因为内部任务同步执行,打破了外部任务的逻辑var pending = _pending; /* ... */ _pending = wrapper()

    如何使其适用于测试#2?

    一种解决方案是使用Task.Factory.StartNew(..., TaskScheduler.FromCurrentSynchronizationContext()为每项任务强制执行异步。但是,我不想对可能在内部同步的命令处理程序强加异步执行。此外,我不想依赖于任何特定同步上下文的行为(即依赖于Task.Factory.StartNew应该在创建的任务实际启动之前返回)。

    在现实生活中,我负责AsyncOp以上的内容,但无法控制命令处理程序(即handleAsync内的任何内容)。

3 个答案:

答案 0 :(得分:12)

我几乎忘记了可以手动构建Task,而无需启动或安排它。然后,"Task.Factory.StartNew" vs "new Task(...).Start"让我回到正轨。我认为这是Task<TResult>构造函数实际上可能有用的少数情况之一,以及嵌套任务(Task<Task<T>>)和Task.Unwrap()

// AsyncOp
class AsyncOp<T>
{
    Task<T> _pending = Task.FromResult(default(T));

    public Task<T> CurrentTask { get { return _pending; } }

    public Task<T> RunAsync(Func<Task<T>> handler, bool useSynchronizationContext = false)
    {
        var pending = _pending;
        Func<Task<T>> wrapper = async () =>
        {
            // await the prev task
            var prevResult = await pending;
            Console.WriteLine("\nprev task result:  " + prevResult);
            // start and await the handler
            return await handler();
        };

        var task = new Task<Task<T>>(wrapper);
        var inner = task.Unwrap();
        _pending = inner;

        task.RunSynchronously(useSynchronizationContext ?
            TaskScheduler.FromCurrentSynchronizationContext() :
            TaskScheduler.Current);

        return inner;
    }
}

输出:

Test #1...

prev task result:  0
this task arg: 1000

prev task result:  1000
this task arg: 900

prev task result:  900
this task arg: 800

Press any key to continue to test #2...


prev task result:  800
this task arg: 100

prev task result:  100
this task arg: 200

如果需要,现在还可以通过添加AsyncOp来保护lock来保证_pending线程安全。

答案 1 :(得分:1)

这是一个比accepted answer更糟糕的解决方案,除了线程安全(这不是问题的要求)之外。缺点:

  1. 所有lambda都是异步执行的(没有快速路径)。
  2. executeOnCurrentContext配置会影响所有的lambda(不是每个lambda配置)。

此解决方案使用ActionBlock库中的TPL Dataflow作为处理引擎。

public class AsyncOp<T>
{
    private readonly ActionBlock<Task<Task<T>>> _actionBlock;

    public AsyncOp(bool executeOnCurrentContext = false)
    {
        var options = new ExecutionDataflowBlockOptions();
        if (executeOnCurrentContext)
            options.TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

        _actionBlock = new ActionBlock<Task<Task<T>>>(async taskTask =>
        {
            try
            {
                taskTask.RunSynchronously();
                await await taskTask;
            }
            catch { } // Ignore exceptions
        }, options);
    }

    public Task<T> RunAsync(Func<Task<T>> taskFactory)
    {
        var taskTask = new Task<Task<T>>(taskFactory);
        if (!_actionBlock.Post(taskTask))
            throw new InvalidOperationException("Not accepted"); // Should never happen
        return taskTask.Unwrap();
    }
}

答案 2 :(得分:1)

Microsoft的Rx确实提供了一种简便的方法来执行此类操作。这是一种简单的方法(也许过分简单):

var subject = new BehaviorSubject<int>(0);

IDisposable subscription =
    subject
        .Scan((x0, x1) =>
        {
            Console.WriteLine($"previous value {x0}");
            return x1;
        })
        .Skip(1)
        .Subscribe(x => Console.WriteLine($"current value {x}\r\n"));

subject.OnNext(1000);
subject.OnNext(900);
subject.OnNext(800);

Console.WriteLine("\r\nPress any key to continue to test #2...\r\n");
Console.ReadLine();

subject.OnNext(200);
subject.OnNext(100);

Console.WriteLine("\r\nPress any key to exit...");
Console.ReadLine();

我得到的输出是这样:

previous value 0
current value 1000

previous value 1000
current value 900

previous value 900
current value 800


Press any key to continue to test #2...

previous value 800
current value 200

previous value 200
current value 100


Press any key to exit...

随时可以通过调用subscription.Dispose()来取消。


Rx中的错误处理通常比正常情况下要多一些。这不仅仅是在事物周围抛出try / catch的问题。在发生IO错误之类的情况下,您还可以使用Retry运算符重复该错误操作。

在这种情况下,由于我使用了BehaviorSubject(每次订阅时都会重复其最后一个值),因此您可以轻松地使用Catch运算符重新订阅。

var subject = new BehaviorSubject<int>(0);
var random = new Random();

IDisposable subscription =
    subject
        .Select(x =>
        {
            if (random.Next(10) == 0)
                throw new Exception();
            return x;
        })
        .Catch<int, Exception>(ex => subject.Select(x => -x))
        .Scan((x0, x1) =>
        {
            Console.WriteLine($"previous value {x0}");
            return x1;
        })
        .Skip(1)
        .Subscribe(x => Console.WriteLine($"current value {x}\r\n"));

现在使用.Catch<int, Exception>(ex => subject.Select(x => -x))可以在出现异常时反转查询的值。

典型的输出可能是这样的:

previous value 0
current value 1000

previous value 1000
current value 900

previous value 900
current value 800


Press any key to continue to test #2...

previous value 800
current value -200

previous value -200
current value -100


Press any key to exit...

请注意下半部分的-ve数字。处理了异常,查询得以继续。