我有一个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个任务到达时,它将在队列中占据一席之地,依此类推。
但由于某种原因,它不起作用,输出是
第二项任务完成
第一项任务完成
这段代码有什么问题?有没有更好的方法?
答案 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启动一个任务来处理队列?