有没有办法在TPL Dataflow中每个链接块一次只处理一个任务?

时间:2018-05-14 18:16:36

标签: c# tpl-dataflow

我有两个类,serviceclient和一个服务。 serviceclient将生成将由服务处理的消息。 ServiceClient消息应以严格的FIFO顺序处理,下一条消息仅在前一个消息完成处理时才可用。

为了解决这个问题,我在每个serviceclient中放置了一个动作块,它直接调用服务来处理客户端消息,我觉得这个消息工作得很好但需要额外的依赖注入。我想知道是否有一种方法来设置它,以便服务可以直接链接到serviceclient消息块,以便它可以同时处理来自多个serviceclient消息块的消息,但是一次只能从任何特定的serviceclient处理一条消息?

具有所需功能的一些代码::

    static void Main(string[] args)
    {
        var service = new Service();

        //I would like messages from these clients to be processed concurrently by service, but only one at a time per client. 
        //So if Client A has two messages in queue(a1, a2) and B has 3(b1,b2,b3), it will immediately take a1&b1. If a1 finishes, it will then take a2. if b1 finishes it will take b2, same with b2 and b3. It would never process a1 concurrently with a2, or b1 concurrently with b2 or b3. 
        service.AddClient(new ServiceClient());
        service.AddClient(new ServiceClient());
    }

    interface IServiceMessage
    {
        string Message { get; }
    }

    class ServiceClient
    {
        public BufferBlock<IServiceMessage> clientServiceMsgs = new BufferBlock<IServiceMessage>();

        public ServiceClient() {
            //run task to populate bufferblock 
        }
    }

    class Service
    {
        ActionBlock<IServiceMessage> processServiceMsgsBlock;

        public Service() {
            processServiceMsgsBlock = new ActionBlock<IServiceMessage>(ProcessServiceMessage);
        }  

        public async Task ProcessServiceMessage(IServiceMessage msg) {
            //process stuff
            return;
        }

        public void AddClient(ServiceClient client)
        {
            client.clientServiceMsgs.LinkTo(processServiceMsgsBlock);
        }
    }

1 个答案:

答案 0 :(得分:0)

你真的需要TPL DataFlow吗?我会给你两个选项,最后一个是TPL Dataflow,就像你问的那样。

每个选项分享以下内容。

public class ServiceClient
{
    public async Task<IServiceMessage> GetAsync(string filter)
    {
        await Task.Yield();
        return new ServiceMessage() { Message = Guid.NewGuid().ToString() };
    }
}

public class Service
{
    public async Task ProcessAsync(IServiceMessage message)
    {
        // do something with it
        await Task.Delay(10);
    }
}

public interface IServiceMessage
{
    string Message { get; }
}

public class ServiceMessage : IServiceMessage
{
    public string Message { get; set; }
}

选项1 - 服务启动任务以读取和写入数据。只需将Service和ServiceClient连接在一起,即可完成第三项服务。

class Program
{
    static void Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        var service = new Service();
        var serviceClient = new ServiceClient();
        var processor = new ProducerConsumerService(serviceClient, service);
        processor.Process("A", cts.Token);
        processor.Process("B", cts.Token);
        processor.Process("C", cts.Token);
        processor.Process("D", cts.Token);

        Console.WriteLine("Press any key to shutdown");
        Console.Read();

        cts.Cancel();
        processor.WaitForCompletion();
    }
}

public class ProducerConsumerService
{
    private List<Task> _processTasks;
    private ServiceClient _serviceClient;
    private Service _service;

    public ProducerConsumerService(ServiceClient serviceClient, Service service)
    {
        _serviceClient = serviceClient;
        _service = service;

        _processTasks = new List<Task>();
    }

    public void Process(string filter, CancellationToken token)
    {
        _processTasks.Add(Task.Run(() =>
        {
            while (!token.IsCancellationRequested)
            {
                var message = _serviceClient.Get(filter);
                _service.Process(message);
            }
        }));
    }

    public void WaitForCompletion()
    {
        Task.WaitAll(_processTasks.ToArray(), TimeSpan.FromSeconds(10));
    }
}

选项2 - 与选项1相同,但有两个任务和一个BlockingCollection,它在生产者(ServiceClient)和消费者(服务)之间提供有界缓冲区。

public class ProducerBufferConsumerService
{
    private List<Task> _producerTasks;
    private List<Task> _consumerTasks;
    private ServiceClient _serviceClient;
    private Service _service;

    public ProducerBufferConsumerService(ServiceClient serviceClient, Service service)
    {
        _serviceClient = serviceClient;
        _service = service;

        _producerTasks = new List<Task>();
        _consumerTasks = new List<Task>();
    }

    public void Process(CancellationToken token)
    {
        var buffer = new BlockingCollection<IServiceMessage>(1000);
        _producerTasks.Add(Task.Run(async () =>
        {
            while (!token.IsCancellationRequested)
            {
                var message = await _serviceClient.GetAsync();
                buffer.Add(message, token);
            }

            buffer.CompleteAdding();
        }));

        _consumerTasks.Add(Task.Run(async () =>
        {
            while (!token.IsCancellationRequested && !buffer.IsAddingCompleted)
            {
                var message = buffer.Take(token);
                await _service.ProcessAsync(message);
            }
        }));
    }

    public void WaitForCompletion()
    {
        Task.WaitAll(_producerTasks.ToArray(), 10000);
        Task.WaitAll(_consumerTasks.ToArray(), 10000);
    }
}

选项3 - 在维持给定ServiceClient的顺序的同时并行化处理

不完全是你要求的,但是关闭。您可以并行化传入的ServiceClient数据流,同时保持正确的排序。在此示例中,所有ServiceClients都发布到单个块,然后将其提供给9个分区的操作块..

为指定服务客户端的每条消息提供Guid ID。然后使用GetHashCode()将其转换为数字。将该数字减少到1-9的范围。创建9个ActionBlock并在LinkTo方法中使用lambda将链接限制为该范围内的单个数字。这样我们就创建了9个分区,每个分区处理一个子范围的数据。您可以安全地以并行方式处理,同时保持给定id的FIFO。

class Program
{
    static void Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        var filters = new List<string>() { "A", "B", "C", "D" };
        var service = new Service();
        var serviceClient = new ServiceClient();
        var partitioningService = new PartitioningService(serviceClient, service);
        var processingTask = Task.Run(() => partitioningService.Process(filters, cts.Token));


        Console.WriteLine("Press any key to shutdown");
        Console.ReadKey();

        cts.Cancel();
        processingTask.Wait(10000);
    }
}

public interface IServiceMessage
{
    string Message { get; }
    Guid Id { get; set; }
}

public class ServiceMessage : IServiceMessage
{
    public string Message { get; set; }
    public Guid Id { get; set;  }
}

public class RoutedMessage
{
    public IServiceMessage Message { get; set; }
    public int PartitionId { get; set; }
}

public class PartitioningService
{
    private ServiceClient _serviceClient;
    private Service _service;

    public PartitioningService(ServiceClient serviceClient, Service service)
    {
        _serviceClient = serviceClient;
        _service = service;
    }

    public void Process(List<string> filters, CancellationToken token)
    {
        var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
        Func<IServiceMessage, RoutedMessage> partitioner = x => new RoutedMessage
            { 
                Message = x,
                PartitionId = x.Id.GetHashCode() / 1000000000
        };

        var partitionerBlock = new TransformBlock<IServiceMessage, RoutedMessage>(partitioner);
        var actionBlock1 = new ActionBlock<RoutedMessage>(async (RoutedMessage msg) => await _service.ProcessAsync(msg));
        var actionBlock2 = new ActionBlock<RoutedMessage>(async (RoutedMessage msg) => await _service.ProcessAsync(msg));
        var actionBlock3 = new ActionBlock<RoutedMessage>(async (RoutedMessage msg) => await _service.ProcessAsync(msg));
        var actionBlock4 = new ActionBlock<RoutedMessage>(async (RoutedMessage msg) => await _service.ProcessAsync(msg));
        var actionBlock5 = new ActionBlock<RoutedMessage>(async (RoutedMessage msg) => await _service.ProcessAsync(msg));
        var actionBlock6 = new ActionBlock<RoutedMessage>(async (RoutedMessage msg) => await _service.ProcessAsync(msg));
        var actionBlock7 = new ActionBlock<RoutedMessage>(async (RoutedMessage msg) => await _service.ProcessAsync(msg));
        var actionBlock8 = new ActionBlock<RoutedMessage>(async (RoutedMessage msg) => await _service.ProcessAsync(msg));
        var actionBlock9 = new ActionBlock<RoutedMessage>(async (RoutedMessage msg) => await _service.ProcessAsync(msg));

        partitionerBlock.LinkTo(actionBlock1, linkOptions, msg => msg.PartitionId == -4);
        partitionerBlock.LinkTo(actionBlock1, linkOptions, msg => msg.PartitionId == -3);
        partitionerBlock.LinkTo(actionBlock1, linkOptions, msg => msg.PartitionId == -2);
        partitionerBlock.LinkTo(actionBlock1, linkOptions, msg => msg.PartitionId == -1);
        partitionerBlock.LinkTo(actionBlock1, linkOptions, msg => msg.PartitionId == 0);
        partitionerBlock.LinkTo(actionBlock1, linkOptions, msg => msg.PartitionId == 1);
        partitionerBlock.LinkTo(actionBlock1, linkOptions, msg => msg.PartitionId == 2);
        partitionerBlock.LinkTo(actionBlock1, linkOptions, msg => msg.PartitionId == 3);
        partitionerBlock.LinkTo(actionBlock1, linkOptions, msg => msg.PartitionId == 4);

        var tasks = new List<Task>();
        foreach (var filter in filters)
        {
            tasks.Add(Task.Run(async () =>
                {
                    Guid filterId = Guid.NewGuid();

                    while (!token.IsCancellationRequested)
                    {
                        var message = await _serviceClient.GetAsync(filter);
                        message.Id = filterId;
                        await partitionerBlock.SendAsync(message);
                    }
                }));
        }

        while (!token.IsCancellationRequested)
            Thread.Sleep(100);

        partitionerBlock.Complete();
        actionBlock1.Completion.Wait();
        actionBlock2.Completion.Wait();
        actionBlock3.Completion.Wait();
        actionBlock4.Completion.Wait();
        actionBlock5.Completion.Wait();
        actionBlock6.Completion.Wait();
        actionBlock7.Completion.Wait();
        actionBlock8.Completion.Wait();
        actionBlock9.Completion.Wait();

        Task.WaitAll(tasks.ToArray(), 10000);
    }
}