【问题标题】:ContinueWith delegate runs before the completion of TaskContinueWith 委托在 Task 完成之前运行
【发布时间】:2014-10-30 20:20:32
【问题描述】:

背景信息:我正在尝试创建一个能够同时处理 5 个并发操作的单例类。每个操作都由SomeAsyncMethod 表示。


此方法存在于单例类中。

consumersConcurrentDictionary<int,Task>

我的问题:由于某种原因,ContinueWith 委托在SomeAsyncMethod 完成之前运行。我知道发生这种情况的原因是因为我有另一种方法可以监视instance.Consumers.Count - 在SomeAsyncMethod 完成之前计数是0

为什么会这样?

    public bool TryAddDequeueRequest()
    {
        if (instance.Consumers.Count < 5)
        {
            Task bogusTask;
            Task newTask = new Task(SomeAsyncMethod);

            //RUNS AFTER THE REQUEST IS COMPLETED
            newTask.ContinueWith(t =>
            {
                instance.Consumers.TryRemove(t.Id, out bogusTask);
            });

            //WE ADD THE TASK TO QUEUE
            instance.Consumers.TryAdd(newTask.Id, newTask);

            //SET IT AND FORGET IT
            newTask.Start();



            return true;
        }
        else
            return false;
    }

【问题讨论】:

  • 你怎么知道它还没有完成?
  • 添加了该详细信息。 (我知道发生这种情况的原因是因为我有另一个方法可以监视 instance.Consumers.Count - 在 SomeAsyncMethod 完成之前计数为 0。)
  • SomeAsyncMethod 调用一些服务并更新数据库。全部通过asyncawait 操作完成。
  • 您应该避免让多个线程分别访问共享内存。 TPL 的整个想法是通过允许任务使用其结果在线程之间传递信息来避免需要这样做。

标签: c# concurrency task-parallel-library async-await concurrentdictionary


【解决方案1】:

SomeAsyncMethod,如果它的名字有任何指示,它是一个异步方法,大概是返回一个Task。您正在创建一个新任务以在另一个线程中开始此异步操作。 Task 将在您完成启动异步操作时返回,而不是在它启动的异步操作完成时返回。

虽然您可以解开任务,但更简单的选择是首先不将其打包。在异步方法返回的Task 上调用延续:

SomeAsyncMethod().ContinueWith(t =>
{
    instance.Consumers.TryRemove(t.Id, out bogusTask);
});
instance.Consumers.TryAdd(newTask.Id, newTask);

当然,如果您希望能够以固定的并行化程度执行一些异步操作,还有更简单的方法。您可以使用SemaphoreSlim 相当简单地创建任何固定并行度的工作队列:

public class FixedParallelismQueue
{
    private SemaphoreSlim semaphore;
    public FixedParallelismQueue(int maxDegreesOfParallelism)
    {
        semaphore = new SemaphoreSlim(maxDegreesOfParallelism);
    }

    public async Task<T> Enqueue<T>(Func<Task<T>> taskGenerator)
    {
        await semaphore.WaitAsync();
        try
        {
            return await taskGenerator();
        }
        finally
        {
            semaphore.Release();
        }
    }
    public async Task Enqueue(Func<Task> taskGenerator)
    {
        await semaphore.WaitAsync();
        try
        {
            await taskGenerator();
        }
        finally
        {
            semaphore.Release();
        }
    }
}

【讨论】:

  • SephamoreSlim 块。 When the count reaches zero, subsequent calls to one of the Wait methods block until other threads release the semaphore.
  • @SimchaKhabinsky 还好我没有打电话给Wait,不是吗。这段代码不会阻塞,它是完全异步的。
  • 即使你这样做WaitAsync - 它会创建一个积压的请求。如果“处理器”很忙,我希望这些请求不会积压并消失。
  • @SimchaKhabinsky 那么你几乎不需要任何东西。只需使用int 来跟踪待处理操作的数量。如果它低于限制,则在继续中递增、运行和递减。如果到了极限,什么也不做。完毕。当然,如果调用者只是继续尝试一遍又一遍地对项目进行排队,那么你最好只在实际队列中将超出限制的项目排队,就像这样。如果他们真的希望将手术放在地板上,那么我想这就是您想要的。
  • @SimchaKhabinsky Interlocked.Increment.
【解决方案2】:

由于SomeAsyncMethod 是异步的,它在完成其自己的 任务之前返回。

如果您可以控制 SomeAsyncMethod 的代码,则将其重构为同步(没有 await/async),或者如果已经有非异步版本,则使用它。

如果你无法控制方法的代码,你可以在周围的任务中等待它完成后再继续:

Task newTask = new Task(()=>{ SomeAsyncMethod().Wait(); });

【讨论】:

  • 解决方法是根本不包装在任务中不调度线程池线程启动任务,同步等待,然后设置任务的结果.这简直是​​愚蠢的。
  • @MarkCidade 您编辑的提案同样不明智。为什么你会放弃使用同步版本的操作并创建一个新线程只是为了坐在那里等待它,而不是使用操作的固有异步实现?它避免了需要有一个线程坐在那里什么都不做。解决方案是根本不将任务包装在另一个任务中,而不是找到使异步代码同步的某种方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多