如何构建异步任务,以便一次最多运行一个任务实例?如果在前一个实例运行时调用了一次或多次任务,则应完成上一个实例,然后该任务应再运行一次。
任务调用可以来自任何线程。任务没有参数,也没有结果;调用方法签名如下:Task DoItAsync()
此类按需,非重入任务的用例包括执行后台索引和服务器同步。
答案 0 :(得分:1)
这是一个包装器,用于保存要运行的操作,并根据需要负责运行它,以便在完成一个完整的运行完成后通知调用者。
/// <summary>
/// Runs an asynchronous action such that at most one instance of the action runs at a time.
/// If the action is invoked one or more times while a previous instance is running,
/// the previous instance completes, and then the action runs one additional time.
/// </summary>
public class RepeatableActionRunner
{
enum RunState { NotRunning, RunningOnce, RunningAndWillRunAgain };
readonly Func<Task> action;
RunState runState;
Task currentTask = Task.CompletedTask;
Task nextTask = Task.CompletedTask;
readonly object lockObject = new object();
public RepeatableActionRunner(Func<Task> action)
{
this.action = action;
}
/// <summary>
/// Runs the action and returns a task that completes when the action completes.
/// </summary>
/// <remarks>This method is thread safe.</remarks>
public Task RunAsync()
{
lock (lockObject) {
switch (runState) {
case RunState.NotRunning:
return StartTaskAsync();
case RunState.RunningAndWillRunAgain:
return nextTask;
default:
runState = RunState.RunningAndWillRunAgain;
return nextTask = currentTask.ContinueWith(_ => {
lock (lockObject)
return StartTaskAsync();
}).Unwrap();
}
}
}
Task StartTaskAsync()
{
runState = RunState.RunningOnce;
return currentTask = action().ContinueWith(_ => {
lock (lockObject)
runState = runState - 1;
});
}
}
答案 1 :(得分:1)
这是Edward's original answer的调整版本,它使用信号量等待,如果我们确实需要等待锁定变为空闲,我们会等待异步。
readonly SemaphoreSlim _someSemaphore = new SemaphoreSlim(1);
Task _currentTask = Task.CompletedTask;
Task _nextTask = Task.CompletedTask;
public async Task DoItAsync()
{
Task taskToAwait;
await _someSemaphore.WaitAsync();
try
{
if (!_nextTask.IsCompleted)
{
taskToAwait = _nextTask;
}
else if(_currentTask.IsCompleted)
{
taskToAwait = _currentTask = DoItNowAsync(null);
}
else
{
taskToAwait = _nextTask = _currentTask.ContinueWith(DoItNowAsync).Unwrap();
}
}
finally
{
_someSemaphore.Release();
}
await taskToAwait;
}
async Task DoItNowAsync(Task _)
{
// Do the work, including async operations.
}
答案 2 :(得分:0)
ActionBlock< T>类已经允许您将请求发布到块并让它使用指定的DOP异步执行它们。默认DOP为1.
这样可以确保一次只执行一次执行,后续请求将排队。要根据计划请求执行,您可以使用计时器将请求发布到块。
例如:
//Block field with gratuitous timestamp
ActionBlock<DateTime> _rebuildBlock;
_rebuildBlock=new ActionBlock<DateTime>(async dt=>await RebuildIndex(dt));
//From any thread:
_rebuildBlock.Post(DateTime.Now);
这足以排队并执行请求。如果默认DOP为1,则一次只允许执行一次。
如果您没有更多的请求发送,例如应用程序终止,您告诉该块完成并等待它处理任何待处理的请求:
_rebuildBlock.Complete();
await _rebuildBlock.Completion;
您可以创建一个类来抽象块或多个块,例如:
class MyProcessor
{
ActionBlock<DateTime> _rebuildBlock;
MyProcessor()
{
_rebuildBlock=new ActionBlock<DateTime>(async dt=>await RebuildIndex(dt));
}
public void Rebuild()
{
_rebuildBlock.Post(DateTime.Now);
}
private async Task RebuildIndex(DateTime timestamp)
{
//...
}
public Task StopAsync()
{
_rebuildBlock.Complete();
return _rebuilcBlock.Completion;
}
}
ActionBlock可以链接到TPL Dataflow名称空间中的其他块,以创建处理步骤的管道,类似于Powershell或SSIS管道。
例如,执行批量导入CSV文件的管道可能如下所示:
//Create the blocks
var folderBlock=new TransformManyBlock<string,string>(folder=>Directory.EnumerateFiles(folder));
var csvBlock=new TransformBlock<string,DataRow>(filePath=>ParseCsv(filePath));
var batchBlock=new BatchBlock<DataRow>(1000);
var dbBlock=new ActionBlock<DataRow[]>(rows=>RunSqlBulkCopy(rows));
//Link them
var options=new DataflowLinkOptions{PropagateCompletion=true};
folderBlock.LinkTo(csvBlock,options);
csvBlock.LinkTo(batchBlock,options);
batchBlock.LinkTo(dbBlock,options);
//Process 100 folders
foreach(var path in aLotOfFolders)
{
folderBlock.Post(path);
}
//Finished with the folders
folderBlock.Complete();
//Wait for the entire pipeline to complete
await dbBlock.Completion;
如果您希望一次只排队一个请求,则可以创建一个包含BroadcastBlock的管道和一个队列长度为1的ActionBlock:
var execOptions = new ExecutionDataflowBlockOptions{BoundedCapacity=1};
var rebuildBlock=new ActionBlock<DateTime>(async dt=>await RebuildIndex(dt),execOptions);
var broadcast=new BroadcastBlock<DateTime>(msg=>msg);
var options=new DataflowLinkOptions{PropagateCompletion=true};
broadcast.LinkTo(rebuildBlock,options);
之后,在执行Rebuild
时发布到广播块的任何内容都将覆盖之前的任何请求。