确定长时间运行的异步任务何时完成

时间:2019-04-09 18:31:11

标签: c# async-await

[使用LinqPad示例更新了2018年4月18日-参见结束]

我的应用程序收到作业列表:

var jobs = await myDB.GetWorkItems();

(注意:我们到处都使用.ConfigureAwait(false),我只是不在这些伪代码段中显示它。)

对于每个作业,我们创建一个长期运行的Task。但是,我们不想等待这个长期运行的任务完成。

jobs.ForEach(job =>
{
    var module = Factory.GetModule(job.Type);
    var task = Task.Run(() => module.ExecuteAsync(job.Data));
    this.NonAwaitedTasks.Add(task, module);
};

将任务及其相关的模块实例都添加到ConcurrentDictionary中,以使它们不会超出范围。

在其他地方,我有另一个偶尔调用的方法,它包含以下内容:

foreach (var entry in this.NonAwaitedTasks.Where(e => e.Key.IsCompleted))
{
    var module = entry.Value as IDisposable;
    module?.Dispose();
    this.NonAwaitedTasks.Remove(entry.Key);
}

(注意,NonAwaitedTasks还使用SemaphoreSlim ...锁定了)

因此,想法是该方法将找到所有已完成的任务,然后处置其相关模块,并将其从此词典中删除。

但是......

在Visual Studio 2017中进行调试时,我从数据库中提取了一个作业,同时花时间在已实例化的单个模块中进行调试,同时在该模块上调用了Dispose。在Callstack中查看,可以看到在上面的方法中调用了Dispose,这是因为任务具有IsCompleted == true。但显然,它无法完成,因为我仍在调试它。

  • .IsCompleted属性是否检查错误?
  • 这只是Visual Studio中调试的产物吗?
  • 我会以错误的方式处理吗?

其他信息

在下面的评论中,我被要求提供一些有关流程的其他信息,因为我描述的内容似乎是不可能的(事实上,我希望那是不可能的)。下面是我的代码的简化版本(我删除了取消令牌和防御性代码的检查,但没有任何影响流程的东西。)

应用程序入口点

这是Windows服务。在OnStart()中是以下行:

this.RunApplicationTask = 
Task.Run(() => myApp.DoWorkAsync().ConfigureAwait(false), myService.CancelSource.Token);

“ RunApplicationTask”只是一个属性,用于在服务的生命周期内将正在执行的任务保持在范围内。

DoWorkAsync()

public async Task DoWorkAsync()
{
    do
    {
        await this.ExecuteSingleIterationAsync().ConfigureAwait(false);
        await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
    }
    while (myApp.ServiceCancellationToken.IsCancellationRequested == false);

    await Task.WhenAll(this.NonAwaitedTasks.Keys).ConfigureAwait(false);
    await this.ClearCompletedTasksAsync().ConfigureAwait(false);
    this.WorkItemsTaskCompletionSource.SetResult(true);

    return;
}

因此,在调试时,这是在迭代DO-LOOP,但它没有到达Task.WhenAll(....)。

也请注意,在调用Cancellation请求并完成所有任务之后,我将调用ClearCompletedTasksAsync()。以后再说吧。

ExecuteSingleIterationAsync

private async Task ExecuteSingleIterationAsync()
{
    var getJobsResponse = await DB.GetJobsAsync().ConfigureAwait(false);
    await this.ProcessWorkLoadAsync(getJobsResponse.Jobs).ConfigureAwait(false);
    await this.ClearCompletedTasksAsync().ConfigureAwait(false);
}

ProcessWorkLoadAsync

private async Task ProcessWorkLoadAsync(IList<Job> jobs)
{
    if (jobs.NoItems())
    {
        return ;
    }

    jobs.ForEach(job =>
    {
        // The processor instance is disposed of when removed from the NonAwaitedTasks collection.
        IJobProcessor processor = ProcessorFactory.GetProcessor(workItem, myApp.ServiceCancellationToken);
        try
        {
            var task = Task.Run(() => processor.ExecuteAsync(job).ConfigureAwait(false), myApp.ServiceCancellationToken);
            this.NonAwaitedTasks.Add(task, processor);
        }
        catch (Exception e)
        {
            ...
        }
    });

    return;
}

每个处理器实现以下接口方法: 任务ExecuteAsync(作业);

正是在ExecuteAsync中,正在使用的处理器实例上调用.Dispose()。

ProcessorFactory.GetProcessor()

public static IJobProcessor GetProcessor(Job job, CancellationToken token)
{
    .....
    switch (someParamCalculatedAbove)
    {
        case X:
            {
                return new XProcessor(...);
            }

        case Y:
            {
                return new YProcessor(...);
            }

        default:
            {
                return null;
            }
    }
}

所以在这里,我们得到一个实例。

ClearCompletedTasksAsync()

private async Task ClearCompletedTasksAsync()
{
    await myStatic.NonAwaitedTasksPadlock.WaitAsync().ConfigureAwait(false);
    try
    {
        foreach (var taskEntry in this.NonAwaitedTasks.Where(entry => entry.Key.IsCompleted).ToArray())
        {
            var processorInstance = taskEntry.Value as IDisposable;
            processorInstance?.Dispose();
            this.NonAwaitedTasks.Remove(taskEntry.Key);
        }
    }
    finally
    {
        myStatic.NonAwaitedTasksPadlock.Release();
    }
}

这被称为循环的每次迭代。目的是确保未等待任务的列表保持较小。

就这样...处置似乎仅在调试时被调用。

LinqPad示例

async Task Main()
{
    SetProcessorRunning();

    await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);

    do
    {
        foreach (var entry in NonAwaitedTasks.Where(e => e.Key.IsCompleted).ToArray())
        {
            "Task is completed, so will dispose of the Task's processor...".Dump();
            var p = entry.Value as IDisposable;
            p?.Dispose();
            NonAwaitedTasks.Remove(entry.Key);
        }
    }
    while (NonAwaitedTasks.Count > 0);
}

// Define other methods and classes here

public void SetProcessorRunning()
{
    var p = new Processor();

    var task = Task.Run(() => p.DoWorkAsync().ConfigureAwait(false));
    NonAwaitedTasks.Add(task, p);
}

public interface IProcessor
{
    Task DoWorkAsync();
}

public static Dictionary<Task, IProcessor> NonAwaitedTasks = new Dictionary<Task, IProcessor>();

public class Processor : IProcessor, IDisposable
{
    bool isDisposed = false;
    public void Dispose()
    {
        this.isDisposed = true;
        "I have been disposed of".Dump();
    }

    public async Task DoWorkAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false);

        if (this.isDisposed)
        {
            $"I have been disposed of (isDispose = {this.isDisposed}) but I've not finished work yet...".Dump();
        }

        await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
    }
}

输出:

  

任务已完成,因此将处置任务的处理器...

     

我已经被处置了

     

我已被处决(isDispose = True),但我尚未完成工作   还...

1 个答案:

答案 0 :(得分:2)

您的问题在这一行:

var task = Task.Run(() => p.DoWorkAsync().ConfigureAwait(false));

将鼠标悬停在var上,看看它是什么类型。

Task.Run通过为async和朋友制定特殊的“任务展开”规则来理解Func<Task<Task>>代表。但是,Func<ConfiguredTaskAwaitable>不会有任何特殊的展开。

您可以这样想;上面的代码:

  1. p.DoWorkAsync()返回一个Task
  2. Task.ConfigureAwait(false)返回一个ConfiguredTaskAwaitable
  3. 因此,要求Task.Run运行此函数,以便在线程池线程上创建ConfiguredTaskAwaitable
  4. 因此,Task.Run的返回类型为Task<ConfiguredTaskAwaitable>-创建ConfiguredTaskAwaitable后立即完成的任务。 创建时-不是完成时。

在这种情况下,ConfigureAwait(false)不会做任何事情,因为没有await可以配置。因此,您可以将其删除:

var task = Task.Run(() => p.DoWorkAsync());

此外,正如Servy所述,如果您不需要在线程池线程上运行DoWorkAsync,则也可以跳过Task.Run

var task = p.DoWorkAsync();