手动控制执行任务的顺序

时间:2018-05-25 10:49:29

标签: c# .net asynchronous async-await task

我有一个RabbitMQ队列,我批量读取asyncrounusly,但我必须保留这些消息的顺序。我有一个名为ServiceNumber的字段,它定义了一个唯一的邮件编号,我必须保留这个订单。

例如

   SN1 SN2 SN1 SN1 SN1 SN2
   1   2   3   4   5   6 

在这种情况下,我们可以同时处理消息1和2(它们具有不同的SN),然后我们可以处理3和6,然后是4,然后是5。

我尝试通过ContinueWith链以下列方式实现它:

private readonly Dictionary<string, Task> _currentTasks = new Dictionary<string, Task>();
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);

private async Task WrapMessageInQueue(string serviceNumber, Func<Task> taskFunc)
{
    Task taskToAwait;
    await _semaphore.WaitAsync();
    try
    {
        _currentTasks.TryGetValue(serviceNumber, out var task);
        if (task == null)
            task = Task.CompletedTask;

        taskToAwait = _currentTasks[serviceNumber] = task.ContinueWith(_ => taskFunc());
    }
    finally
    {
        _semaphore.Release();
    }

    await taskToAwait.ConfigureAwait(false);
}

void Main()
{
    Task.Run(async () => {
        var task1 = Task.Run(() =>
        {
            return WrapMessageInQueue("10", async () =>
            {
                await Task.Delay(5000);
                Console.WriteLine("first task finished");
            });
        });

        while (task1.Status == TaskStatus.WaitingForActivation) 
        {
            Console.WriteLine("waiting task to be picked by a scheduler. Status = {0}", task1.Status);
            await Task.Delay(100);
        }

        var task2 = Task.Run(() =>
        {
            return WrapMessageInQueue("10", async () =>
            {
                Console.WriteLine("second task finished");
            });
        });

        await Task.WhenAll(new[] {task1, task2});
    }).Wait();
}

这里的主要思想是第一个RUNNED任务应该在所有其他任务开始之前完成。所以我实现了一个字典,我存储了一个任务,每个后续的一个都被添加到ContinueWith链中。因此,它会在普遍的执行后严格执行。当第3个任务到达时,它将在队列中占据一席之地,依此类推。

但由于某种原因,它不起作用,输出是

  

第二项任务完成

     

第一项任务完成

这段代码有什么问题?有没有更好的方法?

3 个答案:

答案 0 :(得分:2)

您正在使用Task.Run将测试任务添加到队列中并且在此处具有竞争条件 - 无法保证在task1之前线程池将选择task2

不确定是否真的如此。

您可能想要检查TPL Dataflow库,我认为它非常适合所描述的场景。

甚至可以通过SN使用Reactive扩展进行分组,然后进行处理。

答案 1 :(得分:1)

问题在于此代码片段

task.ContinueWith(_ => taskFunc());

没有做你期望的事。它创建了一个任务,它只是启动延续,但不是等待。结果,您的两个延续都会立即被调用。

总而言之,那里有太多不必要的任务,部分未经正常等待。我清理了它并实现了使其工作所需的延续功能。

public static
class TaskExtensions
{
    public static async
    Task ContinueWith(this Task task, Func<Task> continuation)
    {
        await task;
        await continuation();
    }
}

class Program
{
    static readonly Dictionary<string, Task> _currentTasks = new Dictionary<string, Task>();

    private static
    Task WrapMessageInQueue(string serviceNumber, Func<Task> taskFunc)
    {
        lock (_currentTasks)
        {
            if (!_currentTasks.TryGetValue(serviceNumber, out var task))
                task = Task.CompletedTask;

            return _currentTasks[serviceNumber] = task.ContinueWith(() => taskFunc());
        }
    }

    public static
    void Main(string[] args)
    {
        Task.Run(async () =>
        {
            var task1 = WrapMessageInQueue("10", async () =>
            {
                await Task.Delay(500);
                Console.WriteLine("first task finished");
            });

            var task2 = WrapMessageInQueue("10", async () =>
            {
                Console.WriteLine("second task finished");
            });

            await Task.WhenAll(new[] { task1, task2 });
        }).Wait();
    }
}

答案 2 :(得分:0)

有趣的方法。但是由于消息的处理(通过SN)无论如何都必须是顺序的,为什么每条消息一个任务呢?它只会让事情变得更复杂,因为你需要控制任务的执行顺序。

为什么没有收集器任务将传入的消息排序到队列中(通过SN)并且每个SN启动一个任务来处理队列?