簡體   English   中英

使用 BlockingCollection<T> :OperationCanceledException,有沒有更好的辦法?

[英]Using BlockingCollection<T>: OperationCanceledException, is there a better way?

我正在將(坦率地說很棒) BlockingCollection<T>類型用於高度多線程的高性能應用程序。

整個集合有很多吞吐量,並且在微觀層面上它是高性能的。 但是,對於每個“批次”,它總是通過標記取消標記來結束。 這會導致在任何等待的Take調用上拋出異常。 這很好,但我會選擇一個返回值或輸出參數來表示它,因為 a) 異常有明顯的開銷 b) 在調試時,我不想手動關閉那個特定的異常中斷例外。

實現似乎很激烈,理論上我想我可以反匯編並重新創建我自己的不使用異常的版本,但也許有更簡單的方法?

我可以向集合中添加一個null (或者如果不是,一個占位符)對象來表示進程應該結束,但是也需要有一種方法可以很好地中止,即喚醒等待線程並以某種方式告訴他們某些事情已經發生。

那么 - 替代集合類型? 重新創建我自己的? 某種方式來濫用這個?

(一些上下文:我選擇了BlockingCollection<T>因為它比手動鎖定Queue有優勢。盡我所能說線程原語的使用是極好的,在我的情況下,這里和那里幾毫秒並且是最佳的核心是使用至關重要。)

編輯:我剛剛為這個開了一個賞金。 我不相信Anastasiosyal 的回答涵蓋了我在評論中提出的問題。 我知道這是一個棘手的問題。 有人可以提供幫助嗎?

我猜你已經完成了自己,看看BlockingCollection的反映來源,不幸的是,當一個CancellationToken被傳遞到BlockingCollection並取消時,你會得到OperationCancelledException,如下圖所示(有幾個圖像后的變通方法)

GetConsumingEnumerable在BlockingCollection上調用TryTakeWithNoTimeValidation ,后者又引發此異常。

在此輸入圖像描述

解決方法#1

一種可能的策略是,假設您對生產者和消費者有更多控制權,而不是將取消令牌傳遞給BlockingCollection(這將引發此異常),您將取消令牌傳遞給您的生產者和消費者。

如果您的生產者沒有生產並且您的消費者沒有消費,那么您已經有效地取消了操作,而沒有引發此異常並且在您的BlockingCollection中傳遞CancellationToken.None。

特殊情況當BlockingCollection處於BoundedCapacity或Empty時取消

生產者被阻止 :當達到BlockingCollection上的BoundedCapacity時,生產者線程將被阻止。 因此,當嘗試取消並且BlockingCollection處於BoundedCapacity時(這意味着您的消費者未被阻止但生產者因為無法向隊列中添加任何其他項而被阻止),那么您將需要允許使用其他項目(一個對於每個生成器線程)將解除生成器的阻塞(因為它們在添加到blockingCollection時被阻止),反過來允許你的取消邏輯在生產者端啟動。

消費者被阻止 :當您的消費者因隊列為空而被阻止時,您可以在阻止集合中插入一個空的工作單元(每個消費者線程一個),以便取消阻止消費者線程並允許您的取消邏輯啟動消費者方面。

如果隊列中有項目且未達到BoundedCapacity或Empty等限制,則不應阻止生產者和消費者線程。

解決方法#2

使用取消工作單位。

當您的應用程序需要取消時,那么您的生產者(可能只有1個生產者就足夠而其他人只是取消生產)將產生一個取消工作單元(可能是null,因為您還提到或某些實現標記接口的類)。 當消費者使用這個工作單元並檢測到它實際上是一個取消工作單元時,他們的取消邏輯就會起作用。要生產的取消工作單元的數量需要等於消費者線程的數量。

當我們接近BoundedCapacity時,需要謹慎,因為這可能是一些生產者被阻止的跡象。 根據生產者/消費者的數量,在所有生產者(但是1)關閉之前,消費者可以消費。 這確保了周圍沒有揮之不去的生產者。 當只有一個生產者離開時,您的最后一個消費者可以關閉,生產者可以停止生產取消工作單位。

我之前做過的BlockingQueue怎么樣?

http://apichange.codeplex.com/SourceControl/changeset/view/76c98b8c7311#ApiChange.Api%2fsrc%2fInfrastructure%2fBlockingQueue.cs

沒有任何例外,它應該沒問題。 當前隊列只是關閉dispose上的事件,這可能不是你想要的。 您可能希望將null設置為null並等待所有項目都被處理。 除此之外,它應該適合您的需求。

using System.Collections.Generic;
using System.Collections;
using System.Threading;
using System;

namespace ApiChange.Infrastructure
{

    /// <summary>
    /// A blocking queue which supports end markers to signal that no more work is left by inserting
    /// a null reference. This constrains the queue to reference types only. 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class BlockingQueue<T> : IEnumerable<T>, IEnumerable, IDisposable where T : class
    {
        /// <summary>
        /// The queue used to store the elements
        /// </summary>
        private Queue<T> myQueue = new Queue<T>();
        bool myAllItemsProcessed = false;
        ManualResetEvent myEmptyEvent = new ManualResetEvent(false);

        /// <summary>
        /// Deques an element from the queue and returns it.
        /// If the queue is empty the thread will block. If the queue is stopped it will immedieately
        /// return with null.
        /// </summary>
        /// <returns>An object of type T</returns>      
        public T Dequeue()
        {
            if (myAllItemsProcessed)
                return null;

            lock (myQueue)
            {
                while (myQueue.Count == 0) 
                {
                    if(!Monitor.Wait(myQueue, 45))
                    {
                        // dispatch any work which is not done yet
                        if( myQueue.Count > 0 )
                            continue;
                    }

                    // finito 
                    if (myAllItemsProcessed)
                    {
                        return null;
                    }
                }

                T result = myQueue.Dequeue();
                if (result == null)
                {
                    myAllItemsProcessed = true;
                    myEmptyEvent.Set();
                }
                return result;
            }
        }

        /// <summary>
        /// Releases the waiters by enqueuing a null reference which causes all waiters to be released. 
        /// The will then get a null reference as queued element to signal that they should terminate.
        /// </summary>
        public void ReleaseWaiters()
        {
            Enqueue(null);
        }

        /// <summary>
        /// Waits the until empty. This does not mean that all items are already process. Only that
        /// the queue contains no more pending work. 
        /// </summary>
        public void WaitUntilEmpty()
        {
            myEmptyEvent.WaitOne();
        }

        /// <summary>
        /// Adds an element of type T to the queue. 
        /// The consumer thread is notified (if waiting)
        /// </summary>
        /// <param name="data_in">An object of type T</param>      
        public void Enqueue(T data_in)
        {
            lock (myQueue)
            {
                myQueue.Enqueue(data_in);
                Monitor.PulseAll(myQueue);
            }
        }

        /// <summary>
        /// Returns an IEnumerator of Type T for this queue
        /// </summary>
        /// <returns></returns>    
        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            while (true)
            {
                T item = Dequeue();
                if (item == null)
                    break;
                else
                    yield return item;
            }
        }

        /// <summary>
        /// Returns a untyped IEnumerator for this queue
        /// </summary>
        /// <returns></returns>     
        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<T>)this).GetEnumerator();
        }


        #region IDisposable Members

        /// <summary>
        /// Closes the EmptyEvent WaitHandle.
        /// </summary>
        public void Dispose()
        {
            myEmptyEvent.Close();
        }

        #endregion
    }
}

您可以通過在最后一項上設置一個標志來向批處理結束發出信號(向其添加一個IsLastItem bool屬性或將其包裝)。 或者你可以發送一個null作為最后一項(不確定null是否正確地通過了阻塞集合)。

如果您可以刪除對“批處理”概念的需要,您可以創建一個額外的線程來連續Take()並從您的blockingcollection處理新數據,而不執行任何其他操作。

基倫,

根據我的檢查,我個人不知道ProducerConsumer模式的任何線程安全類型,它完全符合您的要求。 我並不認為這是競爭解決方案,但建議您使用少量extension method裝飾BlockingCollection<T> ,這將使您可以自由地提供任何內置或自定義類型而不是默認的CancellationToken

階段1:

以下是使用基礎TryAddWithNoTimeValidation方法添加到隊列的默認方法列表。

public void Add(T item){
      this.TryAddWithNoTimeValidation(item, -1, new CancellationToken());
}

public void Add(T item, CancellationToken cancellationToken){
      this.TryAddWithNoTimeValidation(item, -1, cancellationToken);
    }

public bool TryAdd(T item){
      return this.TryAddWithNoTimeValidation(item, 0, new CancellationToken());
    }

public bool TryAdd(T item, TimeSpan timeout){
      BlockingCollection<T>.ValidateTimeout(timeout);
      return this.TryAddWithNoTimeValidation(item, (int) timeout.TotalMilliseconds, new CancellationToken());
    }

public bool TryAdd(T item, int millisecondsTimeout){
      BlockingCollection<T>.ValidateMillisecondsTimeout(millisecondsTimeout);
      return this.TryAddWithNoTimeValidation(item, millisecondsTimeout, new           CancellationToken());
}

public bool TryAdd(T item, int millisecondsTimeout, CancellationToken cancellationToken){
 BlockingCollection<T>.ValidateMillisecondsTimeout(millisecondsTimeout);
 return this.TryAddWithNoTimeValidation(item, millisecondsTimeout, cancellationToken);
}

現在,您可以為您感興趣的任何/所有方法提供擴展。

第二階段:

您現在引用TryAddWithNoTimeValidation的實現而不是默認值。

我可以給你一個替代版本的TryAddWithNoTimeValidation ,它可以安全地繼續而不會拋出OperationCancellation異常。

我的建議是通過封裝一個異步隊列來實現這個功能,比如來自TPL Dataflow庫的BufferBlock<T>類。 此類是用於生產者-消費者場景的線程安全容器,並像BlockingCollection<T>類一樣支持背壓 ( BoundedCapacity )。 異步意味着相應的Add / Take方法( SendAsync / ReceiveAsync )返回任務。 這些任務將取消事件存儲為內部狀態,可以使用IsCanceled屬性進行查詢,因此可以避免在內部拋出異常。 通過使用抑制所有異常的專用等待器等待任務,也可以避免傳播這些異常。 這是一個實現:

/// <summary>
/// A thread-safe collection that provides blocking and bounding capabilities.
/// The cancellation is propagated as a false result, and not as an exception.
/// </summary>
public class CancellationFriendlyBlockingCollection<T>
{
    private readonly BufferBlock<T> _bufferBlock;

    public CancellationFriendlyBlockingCollection()
    {
        _bufferBlock = new BufferBlock<T>();
    }

    public CancellationFriendlyBlockingCollection(int boundedCapacity)
    {
        _bufferBlock = new BufferBlock<T>(new() { BoundedCapacity = boundedCapacity });
    }

    public bool TryAdd(T item, CancellationToken cancellationToken = default)
    {
        if (_bufferBlock.Post(item)) return true;
        Task<bool> task = _bufferBlock.SendAsync(item, cancellationToken);
        SuppressException.Await(task).GetResult();
        Debug.Assert(task.IsCompleted);
        if (task.IsCanceled) return false;
        return task.GetAwaiter().GetResult();
    }

    public bool TryTake(out T item, CancellationToken cancellationToken = default)
    {
        if (_bufferBlock.TryReceive(out item)) return true;
        Task<T> task = _bufferBlock.ReceiveAsync(cancellationToken);
        SuppressException.Await(task).GetResult();
        Debug.Assert(task.IsCompleted);
        if (task.IsCanceled) return false;
        item = task.GetAwaiter().GetResult();
        return true;
    }

    public IEnumerable<T> GetConsumingEnumerable(
        CancellationToken cancellationToken = default)
    {
        while (TryTake(out var item, cancellationToken))
            yield return item;
    }

    public void CompleteAdding() => _bufferBlock.Complete();
    public Task Completion => _bufferBlock.Completion;

    private struct SuppressException : ICriticalNotifyCompletion
    {
        private Task _task;
        public static SuppressException Await(Task task)
            => new SuppressException { _task = task };
        public SuppressException GetAwaiter() => this;
        public bool IsCompleted => _task.IsCompleted;
        public void OnCompleted(Action action)
            => _task.GetAwaiter().OnCompleted(action);
        public void UnsafeOnCompleted(Action action)
            => _task.GetAwaiter().UnsafeOnCompleted(action);
        public void GetResult() { }
    }
}

內部SuppressException結構是復制粘貼GitHub的問題。 它由頂級 Microsoft 工程師 (Stephen Toub) 編寫。

CancellationFriendlyBlockingCollection.TryTake方法可以在我的 PC 中(在單個線程上)以每秒約 1,500,000 次的頻率在循環中使用取消的CancellationToken調用。 為了比較, BlockingCollection<T>.Take在相同條件下的頻率約為每秒 10,000 次。

您可能想用更現代的異步隊列(如Channel<T>替換BufferBlock<T> Channel<T> 在這種情況下,請務必先閱讀此問題,以便了解此類在特定條件下的泄漏行為。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM