我有以下情况,我认为可能很常见:
有一个任务(一个UI命令处理程序)可以同步或异步完成。
命令的到达时间可能比处理它们的速度快。
如果命令已有待处理任务,则应对新命令处理程序任务进行排队并按顺序处理。
每个新任务的结果可能取决于上一个任务的结果。
应该遵守取消规定,但为了简单起见,我想将其排除在本问题的范围之外。此外,线程安全(并发)不是必需的,但必须支持重入。
这是我想要实现的基本示例(为了简单起见,作为控制台应用程序):
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);
});
所需的输出应为100
,200
,而不是200
,100
,因为100
已有待处理的外部任务。这显然是因为内部任务同步执行,打破了外部任务的逻辑var pending = _pending; /* ... */ _pending = wrapper()
。
如何使其适用于测试#2?
一种解决方案是使用Task.Factory.StartNew(..., TaskScheduler.FromCurrentSynchronizationContext()
为每项任务强制执行异步。但是,我不想对可能在内部同步的命令处理程序强加异步执行。此外,我不想依赖于任何特定同步上下文的行为(即依赖于Task.Factory.StartNew
应该在创建的任务实际启动之前返回)。
在现实生活中,我负责AsyncOp
以上的内容,但无法控制命令处理程序(即handleAsync
内的任何内容)。
答案 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更糟糕的解决方案,除了线程安全(这不是问题的要求)之外。缺点:
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数字。处理了异常,查询得以继续。