繁体   English   中英

"TPL DataFlow,具有优先级的链接块?"

[英]TPL DataFlow, link blocks with priority?

使用 TPL.DataFlow 块,是否可以将两个或多个源链接到单个 ITargetBlock(例如 ActionBlock)并确定源的优先级?

例如

BufferBlock<string> b1 = new ...
BufferBlock<string> b2 = new ...
ActionBlock<string> a = new ...

//somehow force messages in b1 to be processed before any message of b2, always
b1.LinkTo (a);
b2.LinkTo (a);

TPL Dataflow本身没有类似的东西。

我能想象的最简单的方法就是创建一个封装三个块的结构:高优先级输入,低优先级输入和输出。 这些块将是简单的BufferBlock ,以及在后台运行的基于优先级将消息从两个输入转发到输出的方法。

代码可能如下所示:

public class PriorityBlock<T>
{
    private readonly BufferBlock<T> highPriorityTarget;

    public ITargetBlock<T> HighPriorityTarget
    {
        get { return highPriorityTarget; }
    }

    private readonly BufferBlock<T> lowPriorityTarget;

    public ITargetBlock<T> LowPriorityTarget
    {
        get { return lowPriorityTarget; }
    }

    private readonly BufferBlock<T> source;

    public ISourceBlock<T> Source
    {
        get { return source; }
    }

    public PriorityBlock()
    {
        var options = new DataflowBlockOptions { BoundedCapacity = 1 };

        highPriorityTarget = new BufferBlock<T>(options);
        lowPriorityTarget = new BufferBlock<T>(options);
        source = new BufferBlock<T>(options);

        Task.Run(() => ForwardMessages());
    }

    private async Task ForwardMessages()
    {
        while (true)
        {
            await Task.WhenAny(
                highPriorityTarget.OutputAvailableAsync(),
                lowPriorityTarget.OutputAvailableAsync());

            T item;

            if (highPriorityTarget.TryReceive(out item))
            {
                await source.SendAsync(item);
            }
            else if (lowPriorityTarget.TryReceive(out item))
            {
                await source.SendAsync(item);
            }
            else
            {
                // both input blocks must be completed
                source.Complete();
                return;
            }
        }
    }
}

用法如下所示:

b1.LinkTo(priorityBlock.HighPriorityTarget);
b2.LinkTo(priorityBlock.LowPriorityTarget);
priorityBlock.Source.LinkTo(a);

对于这项工作, a也必须有BoundingCapacity设置为一个(或至少是一个非常低的数字)。

这段代码的警告是它可以引入两条消息的延迟(一条在输出块中等待,一条在SendAsync()等待)。 因此,如果您有一长串低优先级消息,并且突然出现高优先级消息,则只有在那两个已经等待的低优先级消息之后才会处理它。

如果这对您来说是个问题,那就可以解决了。 但我相信它需要更复杂的代码,它处理TPL Dataflow中较少公开的部分,比如OfferMessage()

这是PriorityBufferBlock<T>类的实现,它比低优先级项目更频繁地传播高优先级项目。 此类的构造函数有一个priorityPrecedence参数,该参数定义了为每个低优先级项传播多少高优先级项。 如果此参数的值为1.0 (最小的有效值),则没有真正的优先级可言。 如果此参数的值为Double.PositiveInfinity ,则只要队列中有高优先级项目,就不会传播低优先级项目。 如果此参数具有更正常的值,例如5.0 ,则每 5 个高优先级项目将传播一个低优先级项目。

此类在内部维护两个队列,一个用于高优先级项目,一个用于低优先级项目。 不考虑存储在每个队列中的项目数,除非两个列表之一为空,在这种情况下,另一个队列的所有项目都可以按需自由传播。 priorityPrecedence参数仅在两个内部队列均非空时才影响类的行为。 否则,如果只有一个队列有项目,则PriorityBufferBlock<T>的行为就像普通的BufferBlock<T>

public class PriorityBufferBlock<T> : IPropagatorBlock<T, T>,
    IReceivableSourceBlock<T>
{
    private readonly IPropagatorBlock<T, int> _block;
    private readonly Queue<T> _highQueue = new();
    private readonly Queue<T> _lowQueue = new();
    private readonly Predicate<T> _hasPriorityPredicate;
    private readonly double _priorityPrecedence;
    private double _priorityCounter = 0;

    private object Locker => _highQueue;

    public PriorityBufferBlock(Predicate<T> hasPriorityPredicate,
        double priorityPrecedence,
        DataflowBlockOptions dataflowBlockOptions = null)
    {
        _hasPriorityPredicate = hasPriorityPredicate
            ?? throw new ArgumentNullException(nameof(hasPriorityPredicate));
        if (priorityPrecedence < 1.0)
            throw new ArgumentOutOfRangeException(nameof(priorityPrecedence));
        _priorityPrecedence = priorityPrecedence;
        dataflowBlockOptions ??= new();
        _block = new TransformBlock<T, int>(item =>
        {
            bool hasPriority = _hasPriorityPredicate(item);
            var selectedQueue = hasPriority ? _highQueue : _lowQueue;
            lock (Locker) selectedQueue.Enqueue(item);
            return 0;
        }, new()
        {
            BoundedCapacity = dataflowBlockOptions.BoundedCapacity,
            CancellationToken = dataflowBlockOptions.CancellationToken,
            MaxMessagesPerTask = dataflowBlockOptions.MaxMessagesPerTask
        });
        this.Completion = _block.Completion.ContinueWith(completion =>
        {
            Debug.Assert(this.Count == 0 || !completion.IsCompletedSuccessfully);
            lock (Locker) { _highQueue.Clear(); _lowQueue.Clear(); }
            return completion;
        }, default, TaskContinuationOptions.ExecuteSynchronously |
            TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
    }

    public Task Completion { get; private init; }
    public void Complete() => _block.Complete();
    void IDataflowBlock.Fault(Exception exception) => _block.Fault(exception);
    public int Count
    {
        get { lock (Locker) return _highQueue.Count + _lowQueue.Count; }
    }

    private Queue<T> GetSelectedQueue(bool forDequeue)
    {
        Debug.Assert(Monitor.IsEntered(Locker));
        Queue<T> selectedQueue;
        if (_highQueue.Count == 0)
            selectedQueue = _lowQueue;
        else if (_lowQueue.Count == 0)
            selectedQueue = _highQueue;
        else if (_priorityCounter + 1 > _priorityPrecedence)
            selectedQueue = _lowQueue;
        else
            selectedQueue = _highQueue;
        if (forDequeue)
        {
            if (_highQueue.Count == 0 || _lowQueue.Count == 0)
                _priorityCounter = 0;
            else if (++_priorityCounter > _priorityPrecedence)
                _priorityCounter -= _priorityPrecedence + 1;
        }
        return selectedQueue;
    }

    private T Peek()
    {
        Debug.Assert(Monitor.IsEntered(Locker));
        Debug.Assert(_highQueue.Count > 0 || _lowQueue.Count > 0);
        return GetSelectedQueue(false).Peek();
    }

    private T Dequeue()
    {
        Debug.Assert(Monitor.IsEntered(Locker));
        Debug.Assert(_highQueue.Count > 0 || _lowQueue.Count > 0);
        return GetSelectedQueue(true).Dequeue();
    }

    private class TargetProxy : ITargetBlock<int>
    {
        private readonly PriorityBufferBlock<T> _parent;
        private readonly ITargetBlock<T> _realTarget;

        public TargetProxy(PriorityBufferBlock<T> parent, ITargetBlock<T> target)
        {
            Debug.Assert(parent != null);
            _parent = parent;
            _realTarget = target ?? throw new ArgumentNullException(nameof(target));
        }

        public Task Completion => throw new NotSupportedException();
        public void Complete() => _realTarget.Complete();
        void IDataflowBlock.Fault(Exception error) => _realTarget.Fault(error);

        DataflowMessageStatus ITargetBlock<int>.OfferMessage(
            DataflowMessageHeader messageHeader, int messageValue,
            ISourceBlock<int> source, bool consumeToAccept)
        {
            Debug.Assert(messageValue == 0);
            if (consumeToAccept) throw new NotSupportedException();
            lock (_parent.Locker)
            {
                var realValue = _parent.Peek();
                var response = _realTarget.OfferMessage(messageHeader, realValue,
                    _parent, consumeToAccept);
                if (response == DataflowMessageStatus.Accepted) _parent.Dequeue();
                return response;
            }
        }
    }

    public IDisposable LinkTo(ITargetBlock<T> target,
        DataflowLinkOptions linkOptions)
            => _block.LinkTo(new TargetProxy(this, target), linkOptions);

    DataflowMessageStatus ITargetBlock<T>.OfferMessage(
        DataflowMessageHeader messageHeader, T messageValue,
        ISourceBlock<T> source, bool consumeToAccept)
            => _block.OfferMessage(messageHeader,
                messageValue, source, consumeToAccept);

    T ISourceBlock<T>.ConsumeMessage(DataflowMessageHeader messageHeader,
        ITargetBlock<T> target, out bool messageConsumed)
    {
        _ = _block.ConsumeMessage(messageHeader, new TargetProxy(this, target),
            out messageConsumed);
        if (messageConsumed) lock (Locker) return Dequeue();
        return default;
    }

    bool ISourceBlock<T>.ReserveMessage(DataflowMessageHeader messageHeader,
        ITargetBlock<T> target)
            => _block.ReserveMessage(messageHeader, new TargetProxy(this, target));

    void ISourceBlock<T>.ReleaseReservation(DataflowMessageHeader messageHeader,
        ITargetBlock<T> target)
            => _block.ReleaseReservation(messageHeader, new TargetProxy(this, target));

    public bool TryReceive(Predicate<T> filter, out T item)
    {
        if (((IReceivableSourceBlock<T>)_block).TryReceive(filter, out _))
        {
            lock (Locker) item = Dequeue(); return true;
        }
        item = default; return false;
    }

    public bool TryReceiveAll(out IList<T> items)
    {
        if (((IReceivableSourceBlock<T>)_block).TryReceiveAll(out var items2))
        {
            var array = new T[items2.Count];
            lock (Locker)
                for (int i = 0; i < array.Length; i++)
                    array[i] = Dequeue();
            items = array; return true;
        }
        items = default; return false;
    }
}

使用示例:

var bufferBlock = new PriorityBufferBlock<SaleOrder>(x => x.HasPriority, 2.5);

上述实现支持内置BufferBlock<T>的所有功能。 块的核心功能委托给内部TransformBlock<T, int> ,其中包含存储在其中一个队列中的每个项目的虚拟零值。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM