.net 线程安全异步不可重入任务
如何构造异步任务,使其一次最多运行一个任务实例?如果在前一个实例运行时调用该任务一次或多次,则前一个实例应完成,然后该任务应再运行一次 任务调用可以来自任何线程。任务没有参数,也没有结果;调用方法签名如下:.net 线程安全异步不可重入任务,.net,multithreading,asynchronous,task-parallel-library,.net,Multithreading,Asynchronous,Task Parallel Library,如何构造异步任务,使其一次最多运行一个任务实例?如果在前一个实例运行时调用该任务一次或多次,则前一个实例应完成,然后该任务应再运行一次 任务调用可以来自任何线程。任务没有参数,也没有结果;调用方法签名如下:Task DoItAsync() 这种按需、不可重入任务的用例包括执行后台索引和服务器同步。这是一个包装器,用于保存要运行的操作,并根据需要负责运行该操作,以便在完整的运行完成后通知调用方 /// <summary> /// Runs an asynchronous action
Task DoItAsync()
这种按需、不可重入任务的用例包括执行后台索引和服务器同步。这是一个包装器,用于保存要运行的操作,并根据需要负责运行该操作,以便在完整的运行完成后通知调用方
/// <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;
});
}
}
//
///运行异步操作,以便一次最多运行一个操作实例。
///如果在前一个实例运行时调用该操作一次或多次,
///前一个实例完成,然后该操作再运行一次。
///
公共类RepeatableActionRunner
{
枚举运行状态{NotRunning,RunningOnce,runningandwillrunreach};
只读函数操作;
运行状态;
Task currentTask=Task.CompletedTask;
Task nextTask=Task.CompletedTask;
只读对象lockObject=新对象();
公共RepeatableActionRunner(函数操作)
{
这个动作=动作;
}
///
///运行操作并返回在操作完成时完成的任务。
///
///此方法是线程安全的。
公共任务RunAsync()
{
锁定(锁定对象){
开关(运行状态){
案例RunState.NotRunning:
返回StartTaskAsync();
case RunState.runningandwillrunnear:
返回下一个任务;
违约:
runState=runState.runningandwillrunreach;
return nextTask=currentTask.ContinueWith(=>{
锁定(锁定对象)
返回StartTaskAsync();
}).Unwrap();
}
}
}
任务StartTaskAsync()
{
runState=runState.RunningOnce;
return currentTask=action(){
锁定(锁定对象)
运行状态=运行状态-1;
});
}
}
这是一个经过调整的版本,它使用信号量等待,因此如果我们确实需要等待锁释放,我们将异步等待
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.
}
该类已经允许您向块发布请求,并让它使用指定的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);
您可以创建一个类来抽象块或多个块,例如:
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;
}
}
类MyProcessor
{
动作块_重建块;
MyProcessor()
{
_rebuildBlock=newactionblock(异步dt=>await-RebuildIndex(dt));
}
公共空间重建()
{
_重建block.Post(DateTime.Now);
}
专用异步任务重建索引(日期时间时间戳)
{
//...
}
公共任务StopAsync()
{
_rebuildBlock.Complete();
返回_rebuildcblock.Completion;
}
}
ActionBlock可以链接到TPL数据流命名空间中的其他块,以创建处理步骤管道,类似于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;
//创建块
var folderBlock=newtransformmanyblock(folder=>Directory.EnumerateFiles(folder));
var csvBlock=newtransformblock(filePath=>ParseCsv(filePath));
var batchBlock=新的batchBlock(1000);
var dbBlock=newactionblock(行=>RunSqlBulkCopy(行));
//联系他们
var options=newdataflowlinkoptions{PropagateCompletion=true};
LinkTo(csvBlock,选项);
csvBlock.LinkTo(批块,选项);
LinkTo(dbBlock,选项);
//处理100个文件夹
foreach(文件夹中的变量路径)
{
folderBlock.Post(路径);
}
//文件夹已完成
folderBlock.Complete();
//等待整个管道完成
等待dbBlock完成;
如果希望一次只对一个请求排队,可以创建一个管道,其中包含一个和一个队列长度为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);
var execOptions=new ExecutionDataflowBlockOptions{BoundedCapacity=1};
var-rebuilddblock=newactionblock(异步dt=>await-RebuildIndex(dt),execOptions);
var广播=新广播块(msg=>msg);
var options=newdataflowlinkoptions{PropagateCompletion=true};
broadcast.LinkTo(重建块,选项);
此后,在执行
Rebuild
时发布到广播块的任何内容都将覆盖以前的任何请求 我将使用线程并简单地中止(通过发送事件[!])一个正在运行的线程并启动一个新线程,或者在可能的情况下向我的线程发送事件,以通知他再次执行所有操作。在这种情况下,支持增量更新。因此,与其中止,不如运行到完成,然后再次运行以获取任何新的更改。这也应该可以很好地工作。您只需向线程发出“重启”的信号。首先完成当前运行,然后线程开始下一次(增量)运行。ActionBlock一次包含一个队列调用。计时器可以定期向块发送消息以触发exeuction@PanagiotisKanavos使用基于计时器的轮询方法,您必须等待计时器启动,并等待资源(包括手机上的电池)每次计时器不必要地触发时都会浪费。唯一要注意的是,如果DoItNowAsync
在第一次wait
之前有一长段不可等待的代码,则可以长时间保持锁。
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);