Eşzamansız BlockingCollection <T> gibi bir şey var mı?


87

Eşzamansız awaitolarak sonucunu istiyorum BlockingCollection<T>.Take(), bu yüzden iş parçacığını engellemiyorum. Bunun gibi bir şey arıyorum:

var item = await blockingCollection.TakeAsync();

Bunu yapabileceğimi biliyorum:

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

ancak bu fikir tüm fikri öldürür, çünkü ThreadPoolbunun yerine başka bir iş parçacığı (of ) engellenir.

Herhangi bir alternatif var mı?


3
Bunu anlamıyorum await Task.Run(() => blockingCollection.Take()), görevi kullanırsanız başka bir iş parçacığı üzerinde gerçekleştirilecek ve UI iş parçacığınız bloke olmayacak. Asıl nokta bu değil mi?
Selman Genç

8
@ Selman22, bu bir UI uygulaması değil. Kütüphane dışa Taskaktarım tabanlı bir API'dir. Örneğin, ASP.NET'ten kullanılabilir. Söz konusu kod orada iyi ölçeklenmeyecektir.
avo

ConfigureAwaitSonra kullanılsaydı yine sorun olur Run()mu? [ed. boşver, şimdi ne dediğini anlıyorum]
MojoFilter

Yanıtlar:


99

Bildiğim dört alternatif var.

İlki , eşzamansız ve işlemleri destekleyen iş parçacığı güvenli bir sıra sağlayan Kanallardır . Kanallar son derece optimize edilmiştir ve isteğe bağlı olarak, bir eşiğe ulaşıldığında bazı öğelerin çıkarılmasını destekler.ReadWrite

Yanında BufferBlock<T>gelen TPL veri akışı . Yalnızca tek bir tüketici varsa, kullanabilir OutputAvailableAsyncveya ReceiveAsync, ya da sadece bir sayfasına bağlantı ActionBlock<T>. Daha fazla bilgi için bloguma bakın .

Son ikisi, oluşturduğum türler, AsyncEx kitaplığımda mevcut .

AsyncCollection<T>bir asyncyakın eşdeğer BlockingCollection<T>gibi eş zamanlı bir üretici / tüketici toplama sarma yeteneğine sahip, ConcurrentQueue<T>ya da ConcurrentBag<T>. TakeAsyncKoleksiyondaki öğeleri zaman uyumsuz olarak tüketmek için kullanabilirsiniz . Daha fazla bilgi için bloguma bakın .

AsyncProducerConsumerQueue<T>daha taşınabilir asyncuyumlu bir üretici / tüketici kuyruğudur. DequeueAsyncKuyruktaki öğeleri zaman uyumsuz olarak tüketmek için kullanabilirsiniz . Daha fazla bilgi için bloguma bakın .

Bu alternatiflerin son üçü eşzamanlı ve eşzamansız koyma ve almaya izin verir.


12
CodePlex'in nihayet kapandığı zaman için Git Hub bağlantısı: github.com/StephenCleary/AsyncEx
Paul

API belgeleri yöntemi içeriyor AsyncCollection.TryTakeAsync, ancak indirilen Nito.AsyncEx.Coordination.dll 5.0.0.0(en son sürüm) içinde bulamıyorum . Başvurulan Nito.AsyncEx.Concurrent.dll içinde yok pakette . Neyi kaçırıyorum?
Theodor Zoulias

@TheodorZoulias: Bu yöntem v5'te kaldırıldı. V5 API belgeleri burada .
Stephen Cleary

Oh teşekkürler. Görünüşe göre koleksiyonu numaralandırmanın en kolay ve en güvenli yolu buydu. while ((result = await collection.TryTakeAsync()).Success) { }. Neden kaldırıldı?
Theodor Zoulias

1
@TheodorZoulias: Çünkü "Dene" farklı insanlar için farklı şeyler ifade ediyor. Bir "Dene" yöntemi eklemeyi düşünüyorum, ancak aslında orijinal yöntemden farklı anlamlara sahip olacaktır. Ayrıca, desteklendiğinde kesinlikle en iyi tüketim yöntemi olacak olan gelecekteki bir sürümde desteklenen eşzamansız akışlara bakıyorum.
Stephen Cleary

21

... veya bunu yapabilirsiniz:

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;
            }
        }
    }
}

Basit, tamamen işlevsel asenkron FIFO kuyruğu.

Not: SemaphoreSlim.WaitAsyncBundan önce .NET 4.5'e eklenmişti, bu kadar basit değildi.


2
Sonsuzun ne faydası var for? semafor serbest bırakılırsa, kuyruktan çıkacak en az bir öğe vardır, hayır?
Blendester

2
@Blendester, birden fazla tüketicinin engellenmesi durumunda bir yarış durumu olabilir. En az iki rakip tüketicinin olmadığından emin olamayız ve her ikisinin de bir ürünü çıkarmadan önce uyanıp uyanmayacağını bilmiyoruz. Bir yarış durumunda, biri kuyruktan çıkmayı başaramazsa, uykuya dalacak ve başka bir sinyal bekleyecektir.
John Leidegren

İki veya daha fazla tüketici WaitAsync () 'i geçebilirse, o zaman kuyrukta eşdeğer sayıda öğe vardır ve bu nedenle her zaman başarılı bir şekilde sıradan çıkarlar. Bir şey mi kaçırıyorum?
mindcruzer

2
Bu bir engelleme koleksiyonudur, are'ın semantiği, TryDequeuebir değerle döndürülür veya hiç döndürülmez. Teknik olarak, 1'den fazla okuyucunuz varsa, aynı okuyucu, diğer herhangi bir okuyucu tamamen uyanmadan önce iki (veya daha fazla) öğeyi tüketebilir. Başarılı WaitAsyncolmak, kuyrukta tüketilecek öğeler olabileceğinin bir işaretidir, bu bir garanti değildir.
John Leidegren

Docs.microsoft.com/en-us/dotnet/api/… ' 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.den @JohnLeidegren Başarılı bir sıradaki öğeler nasıl WaitAsyncolmaz? N sürümü, kırılandan daha fazla tüketici uyandırırsa semaphore. Değil mi?
Ashish Negi

4

İşte bir çok BlockingCollectioneksik özellik ile, beklemeyi destekleyen çok basit bir uygulaması . AsyncEnumerable8.0'dan eski C # sürümleri için zaman uyumsuz numaralandırmayı mümkün kılan kitaplığı kullanır .

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);
            }
        });
    }
}

Kullanım örneği:

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);

Çıktı:

1 2 3 4 5 6 7 8 9 10


Güncelleme: C # 8'in yayımlanmasıyla, eşzamansız numaralandırma yerleşik bir dil özelliği haline geldi. Gerekli sınıflar ( IAsyncEnumerable, IAsyncEnumerator) .NET Core 3.0'da gömülüdür ve .NET Framework 4.6.1+ ( Microsoft.Bcl.AsyncInterfaces ) için bir paket olarak sunulur .

İşte GetConsumingEnumerableyeni C # 8 sözdizimini içeren alternatif bir uygulama:

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;
    }
}

Aynı yöntemin bir arada varlığına awaitve yieldaynı yönteme dikkat edin.

Kullanım örneği (C # 8):

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

Not awaitönce foreach.


1
Sonradan düşünmek gerekirse, şimdi sınıf adının AsyncBlockingCollectionanlamsız olduğunu düşünüyorum . Bir şey eşzamansız ve aynı anda bloke olamaz, çünkü bu iki kavram tam olarak zıt şeylerdir!
Theodor Zoulias

-2

Küçük bir bilgisayar korsanlığına aldırmazsanız, bu uzantıları deneyebilirsiniz.

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;
        }
    }
}

1
Yani eşzamansız hale getirmek için yapay bir gecikme mi getiriyorsun? Hala engelliyor değil mi?
nawfal
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.