Có bất cứ điều gì giống như BlockingCollection không đồng bộ <T> không?


85

Tôi muốn awaittrên kết quả của BlockingCollection<T>.Take()không đồng bộ, vì vậy tôi không chặn luồng. Tìm kiếm bất cứ điều gì như thế này:

var item = await blockingCollection.TakeAsync();

Tôi biết tôi có thể làm điều này:

var item = await Task.Run(() => blockingCollection.Take());

nhưng điều đó giết chết toàn bộ ý tưởng, vì một chuỗi khác (trong số ThreadPool) bị chặn.

Có cách nào thay thế không?


2
Tôi không hiểu điều này, nếu bạn sử dụng await Task.Run(() => blockingCollection.Take()), tác vụ sẽ được thực hiện trên chuỗi khác và chuỗi giao diện người dùng của bạn sẽ không bị chặn.
Selman Genç

8
@ Selman22, đây không phải là một ứng dụng giao diện người dùng. Nó là một thư viện Taskdựa trên API xuất khẩu . Nó có thể được sử dụng từ ASP.NET, chẳng hạn. Mã được đề cập sẽ không mở rộng ở đó.
ava

Nó sẽ vẫn là một vấn đề nếu ConfigureAwaitđược sử dụng sau Run()? [biên tập. không bao giờ tâm trí, tôi nhìn thấy những gì bạn đang nói bây giờ]
MojoFilter

Câu trả lời:


95

Có bốn lựa chọn thay thế mà tôi biết.

Đầu tiên là Kênh , cung cấp hàng đợi an toàn luồng hỗ trợ hoạt động Readvà không đồng bộ Write. Các kênh được tối ưu hóa cao và tùy chọn hỗ trợ bỏ một số mục nếu đạt đến ngưỡng.

Tiếp theo là BufferBlock<T>từ TPL Dataflow . Nếu bạn chỉ có một người tiêu dùng, bạn có thể sử dụng OutputAvailableAsynchoặc ReceiveAsynchoặc chỉ liên kết nó với một ActionBlock<T>. Để biết thêm thông tin, hãy xem blog của tôi .

Hai loại cuối cùng là các loại mà tôi đã tạo, có sẵn trong thư viện AsyncEx của tôi .

AsyncCollection<T>asyncgần như tương đương BlockingCollection<T>, có khả năng gói một nhà sản xuất đồng thời / bộ sưu tập của người tiêu dùng như ConcurrentQueue<T>hay ConcurrentBag<T>. Bạn có thể sử dụng TakeAsyncđể tiêu thụ không đồng bộ các mục từ bộ sưu tập. Để biết thêm thông tin, hãy xem blog của tôi .

AsyncProducerConsumerQueue<T>asynchàng đợi nhà sản xuất / người tiêu dùng tương thích di động hơn . Bạn có thể sử dụng DequeueAsyncđể tiêu thụ không đồng bộ các mục từ hàng đợi. Để biết thêm thông tin, hãy xem blog của tôi .

Ba lựa chọn thay thế cuối cùng cho phép đặt và nhận đồng bộ và không đồng bộ.


12
Liên kết Git Hub về thời điểm CodePlex cuối cùng đóng cửa: github.com/StephenCleary/AsyncEx
Paul

Tài liệu API có chứa phương pháp AsyncCollection.TryTakeAsync, nhưng tôi không thể tìm thấy phương thức này trong bản tải xuống Nito.AsyncEx.Coordination.dll 5.0.0.0(phiên bản mới nhất). Nito.AsyncEx.Concurrent.dll được tham chiếu không tồn tại trong gói . Tôi đang thiếu gì?
Theodor Zoulias

@TheodorZoulias: Phương thức đó đã bị loại bỏ trong v5. Tài liệu v5 API có ở đây .
Stephen Cleary

Ồ cảm ơn. Có vẻ như đây là cách dễ nhất và an toàn nhất để liệt kê bộ sưu tập. while ((result = await collection.TryTakeAsync()).Success) { }. Tại sao nó bị xóa?
Theodor Zoulias

1
@TheodorZoulias: Bởi vì "Thử" có nghĩa là những điều khác nhau đối với những người khác nhau. Tôi đang nghĩ đến việc thêm lại phương thức "Thử" nhưng nó thực sự sẽ có ngữ nghĩa khác với phương thức ban đầu. Đồng thời xem xét hỗ trợ các luồng không đồng bộ trong phiên bản tương lai, đây chắc chắn sẽ là phương pháp tiêu thụ tốt nhất khi được hỗ trợ.
Stephen Cleary

21

... hoặc bạn có thể làm điều này:

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public class AsyncQueue<T>
{
    private readonly SemaphoreSlim _sem;
    private readonly ConcurrentQueue<T> _que;

    public AsyncQueue()
    {
        _sem = new SemaphoreSlim(0);
        _que = new ConcurrentQueue<T>();
    }

    public void Enqueue(T item)
    {
        _que.Enqueue(item);
        _sem.Release();
    }

    public void EnqueueRange(IEnumerable<T> source)
    {
        var n = 0;
        foreach (var item in source)
        {
            _que.Enqueue(item);
            n++;
        }
        _sem.Release(n);
    }

    public async Task<T> DequeueAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        for (; ; )
        {
            await _sem.WaitAsync(cancellationToken);

            T item;
            if (_que.TryDequeue(out item))
            {
                return item;
            }
        }
    }
}

Hàng đợi FIFO không đồng bộ đầy đủ chức năng đơn giản.

Lưu ý: SemaphoreSlim.WaitAsyncđã được thêm vào .NET 4.5 trước đó, điều này không đơn giản như vậy.


2
Công dụng của vô hạn là forgì? nếu semaphore được phát hành, hàng đợi có ít nhất một mục để xếp hàng, không?
Blendester

2
@Blendester có thể có một điều kiện chạy đua nếu nhiều người tiêu dùng bị chặn. Chúng tôi không thể biết chắc chắn rằng không có ít nhất hai người tiêu dùng cạnh tranh và chúng tôi không biết liệu cả hai người đều có thể thức dậy trước khi họ mua một món hàng hay không. Trong trường hợp của một cuộc đua, nếu một con không xoay sở được, nó sẽ quay trở lại trạng thái ngủ và chờ một tín hiệu khác.
John Leidegren

Nếu hai hoặc nhiều người tiêu dùng vượt qua WaitAsync (), thì sẽ có một số lượng mặt hàng tương đương trong hàng đợi và do đó họ sẽ luôn xếp hàng thành công. Tui bỏ lỡ điều gì vậy?
mindcruzer

2
Đây là một tập hợp chặn, ngữ nghĩa của TryDequeuelà, trả về với một giá trị hoặc hoàn toàn không trả về. Về mặt kỹ thuật, nếu bạn có nhiều hơn 1 người đọc, thì cùng một người đọc có thể sử dụng hai (hoặc nhiều hơn) mục trước khi bất kỳ người đọc nào khác hoàn toàn tỉnh táo. Thành công WaitAsyncchỉ là một tín hiệu cho thấy có thể có mặt hàng trong hàng đợi để tiêu thụ, nó không phải là một sự đảm bảo.
John Leidegren

@JohnLeidegren If the value of the CurrentCount property is zero before this method is called, the method also allows releaseCount threads or tasks blocked by a call to the Wait or WaitAsync method to enter the semaphore.from docs.microsoft.com/en-us/dotnet/api/… Làm thế nào để thành công khi WaitAsynckhông có các mục trong hàng đợi? Nếu N phát hành đánh thức nhiều hơn N người tiêu dùng semaphorebị hỏng. Phải không?
Ashish Negi

4

Đây là cách triển khai rất cơ bản của một BlockingCollectionhỗ trợ đang chờ, với rất nhiều tính năng còn thiếu. Nó sử dụng AsyncEnumerablethư viện, giúp liệt kê không đồng bộ có thể cho các phiên bản C # cũ hơn 8.0.

public class AsyncBlockingCollection<T>
{ // Missing features: cancellation, boundedCapacity, TakeAsync
    private Queue<T> _queue = new Queue<T>();
    private SemaphoreSlim _semaphore = new SemaphoreSlim(0);
    private int _consumersCount = 0;
    private bool _isAddingCompleted;

    public void Add(T item)
    {
        lock (_queue)
        {
            if (_isAddingCompleted) throw new InvalidOperationException();
            _queue.Enqueue(item);
        }
        _semaphore.Release();
    }

    public void CompleteAdding()
    {
        lock (_queue)
        {
            if (_isAddingCompleted) return;
            _isAddingCompleted = true;
            if (_consumersCount > 0) _semaphore.Release(_consumersCount);
        }
    }

    public IAsyncEnumerable<T> GetConsumingEnumerable()
    {
        lock (_queue) _consumersCount++;
        return new AsyncEnumerable<T>(async yield =>
        {
            while (true)
            {
                lock (_queue)
                {
                    if (_queue.Count == 0 && _isAddingCompleted) break;
                }
                await _semaphore.WaitAsync();
                bool hasItem;
                T item = default;
                lock (_queue)
                {
                    hasItem = _queue.Count > 0;
                    if (hasItem) item = _queue.Dequeue();
                }
                if (hasItem) await yield.ReturnAsync(item);
            }
        });
    }
}

Ví dụ sử dụng:

var abc = new AsyncBlockingCollection<int>();
var producer = Task.Run(async () =>
{
    for (int i = 1; i <= 10; i++)
    {
        await Task.Delay(100);
        abc.Add(i);
    }
    abc.CompleteAdding();
});
var consumer = Task.Run(async () =>
{
    await abc.GetConsumingEnumerable().ForEachAsync(async item =>
    {
        await Task.Delay(200);
        await Console.Out.WriteAsync(item + " ");
    });
});
await Task.WhenAll(producer, consumer);

Đầu ra:

1 2 3 4 5 6 7 8 9 10


Cập nhật: Với việc phát hành C # 8, liệt kê không đồng bộ đã trở thành một tính năng ngôn ngữ tích hợp. Các lớp bắt buộc ( IAsyncEnumerable, IAsyncEnumerator) được nhúng trong .NET Core 3.0 và được cung cấp dưới dạng gói cho .NET Framework 4.6.1+ ( Microsoft.Bcl.AsyncInterfaces ).

Đây là một GetConsumingEnumerabletriển khai thay thế , có cú pháp C # 8 mới:

public async IAsyncEnumerable<T> GetConsumingEnumerable()
{
    lock (_queue) _consumersCount++;
    while (true)
    {
        lock (_queue)
        {
            if (_queue.Count == 0 && _isAddingCompleted) break;
        }
        await _semaphore.WaitAsync();
        bool hasItem;
        T item = default;
        lock (_queue)
        {
            hasItem = _queue.Count > 0;
            if (hasItem) item = _queue.Dequeue();
        }
        if (hasItem) yield return item;
    }
}

Lưu ý sự cùng tồn tại của awaityield trong cùng một phương pháp.

Ví dụ sử dụng (C # 8):

var consumer = Task.Run(async () =>
{
    await foreach (var item in abc.GetConsumingEnumerable())
    {
        await Task.Delay(200);
        await Console.Out.WriteAsync(item + " ");
    }
});

Lưu ý awaittrước khi foreach.


1
Suy nghĩ lại, bây giờ tôi nghĩ rằng tên lớp AsyncBlockingCollectionlà vô nghĩa. Một cái gì đó không thể không đồng bộ và chặn cùng một lúc, vì hai khái niệm này hoàn toàn đối lập nhau!
Theodor Zoulias

0

Nếu bạn không ngại một chút hack, bạn có thể thử các tiện ích mở rộng này.

public static async Task AddAsync<TEntity>(
    this BlockingCollection<TEntity> Bc, TEntity item, CancellationToken abortCt)
{
    while (true)
    {
        try
        {
            if (Bc.TryAdd(item, 0, abortCt))
                return;
            else
                await Task.Delay(100, abortCt);
        }
        catch (Exception)
        {
            throw;
        }
    }
}

public static async Task<TEntity> TakeAsync<TEntity>(
    this BlockingCollection<TEntity> Bc, CancellationToken abortCt)
{
    while (true)
    {
        try
        {
            TEntity item;

            if (Bc.TryTake(out item, 0, abortCt))
                return item;
            else
                await Task.Delay(100, abortCt);
        }
        catch (Exception)
        {
            throw;
        }
    }
}

Vì vậy, bạn mang đến một sự chậm trễ giả tạo để làm cho nó không đồng bộ? Nó vẫn chặn đúng không?
nawfal
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.