簡體   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