线程安全的异步非重入任务

时间:2017-05-03 14:21:53

标签: .net multithreading asynchronous task-parallel-library

如何构建异步任务,以便一次最多运行一个任务实例?如果在前一个实例运行时调用了一次或多次任务,则应完成上一个实例,然后该任务应再运行一次。

任务调用可以来自任何线程。任务没有参数,也没有结果;调用方法签名如下:Task DoItAsync()

此类按需,非重入任务的用例包括执行后台索引和服务器同步。

3 个答案:

答案 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时发布到广播块的任何内容都将覆盖之前的任何请求。