มีอะไรเช่น Asynchronous BlockingCollection <T> หรือไม่?


87

ฉันต้องการawaitผลลัพธ์ของBlockingCollection<T>.Take()แบบอะซิงโครนัสดังนั้นฉันจึงไม่บล็อกเธรด กำลังมองหาอะไรเช่นนี้:

var item = await blockingCollection.TakeAsync();

ฉันรู้ว่าฉันทำได้:

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

แต่นั่นเป็นการฆ่าความคิดทั้งหมดเพราะเธรดอื่นThreadPoolถูกบล็อกแทน

มีทางเลือกอื่นไหม


3
ฉันไม่เข้าใจสิ่งนี้หากคุณใช้await Task.Run(() => blockingCollection.Take())งานจะดำเนินการกับเธรดอื่นและเธรด UI ของคุณจะไม่ถูกบล็อกนั่นไม่ใช่ประเด็นหรือ?
Selman Genç

8
@ Selman22 นี่ไม่ใช่แอป UI เป็นไลบรารีที่ส่งออกTask- ตาม API สามารถใช้ได้จาก ASP.NET เช่น รหัสที่เป็นปัญหาจะปรับขนาดได้ไม่ดี
AVO

จะยังคงเป็นปัญหาหรือไม่ถ้าConfigureAwaitใช้หลังจากRun()? [ed. ไม่เป็นไรฉันเห็นว่าคุณกำลังพูดอะไรอยู่ตอนนี้]
MojoFilter

คำตอบ:


99

มีสี่ทางเลือกที่ฉันรู้จัก

ประการแรกคือChannelsซึ่งจัดเตรียมคิวเธรดที่ปลอดภัยที่รองรับอะซิงโครนัสReadและWriteการดำเนินการ ช่องได้รับการปรับให้เหมาะสมอย่างมากและสามารถเลือกที่จะรองรับการทิ้งบางรายการได้หากถึงเกณฑ์

ถัดไปคือBufferBlock<T>จากTPL Dataflow หากคุณมีผู้บริโภคเพียงรายเดียวคุณสามารถใช้OutputAvailableAsyncหรือReceiveAsyncหรือเชื่อมโยงกับActionBlock<T>ไฟล์. สำหรับข้อมูลเพิ่มเติมโปรดดูที่บล็อกของฉัน

สุดท้ายทั้งสองประเภทที่ฉันสร้างที่มีอยู่ในของฉันห้องสมุด AsyncEx

AsyncCollection<T>เป็นasyncที่อยู่ใกล้เทียบเท่าของBlockingCollection<T>ความสามารถในการตัดผู้ผลิตพร้อมกันคอลเลกชันของผู้บริโภค / เช่นหรือConcurrentQueue<T> ConcurrentBag<T>คุณสามารถใช้TakeAsyncเพื่อใช้ไอเท็มจากคอลเลกชันแบบอะซิงโครนัสได้ สำหรับข้อมูลเพิ่มเติมโปรดดูที่บล็อกของฉัน

AsyncProducerConsumerQueue<T>เป็นasyncคิวผู้ผลิต / ผู้บริโภคแบบพกพาที่เข้ากันได้มากกว่า คุณสามารถใช้DequeueAsyncเพื่อใช้รายการจากคิวแบบอะซิงโครนัส สำหรับข้อมูลเพิ่มเติมโปรดดูที่บล็อกของฉัน

สามทางเลือกสุดท้ายเหล่านี้อนุญาตให้วางและใช้แบบซิงโครนัสและอะซิงโครนัส


12
ลิงก์ Git Hub เมื่อ CodePlex ปิดตัวลงในที่สุด: github.com/StephenCleary/AsyncEx
Paul

เอกสาร API มีวิธีการAsyncCollection.TryTakeAsyncแต่ไม่พบในดาวน์โหลดNito.AsyncEx.Coordination.dll 5.0.0.0(เวอร์ชันล่าสุด) อ้างอิงNito.AsyncEx.Concurrent.dllไม่ได้อยู่ในแพคเกจ ฉันขาดอะไรไป?
Theodor Zoulias

@TheodorZoulias: วิธีนั้นถูกลบออกใน v5 เอกสาร v5 API อยู่ที่นี่
Stephen Cleary

โอ้ขอบคุณ. ดูเหมือนว่าจะเป็นวิธีที่ง่ายและปลอดภัยที่สุดในการแจกแจงคอลเล็กชัน while ((result = await collection.TryTakeAsync()).Success) { }. ทำไมจึงถูกลบออก?
Theodor Zoulias

1
@TheodorZoulias: เพราะ "ลอง" หมายถึงสิ่งที่แตกต่างกับคนอื่น ฉันกำลังคิดที่จะเพิ่มเมธอด "ลอง" กลับเข้าไป แต่จริงๆแล้วมันจะมีความหมายที่แตกต่างจากวิธีเดิม นอกจากนี้ยังมองไปที่การรองรับสตรีม async ในเวอร์ชันอนาคตซึ่งจะเป็นวิธีการบริโภคที่ดีที่สุดเมื่อรองรับ
Stephen Cleary

21

... หรือคุณสามารถทำได้:

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

คิว FIFO แบบอะซิงโครนัสที่ใช้งานง่ายและสมบูรณ์

หมายเหตุ: SemaphoreSlim.WaitAsyncถูกเพิ่มใน. NET 4.5 ก่อนหน้านั้นนี่ไม่ใช่ทั้งหมดที่ตรงไปตรงมา


2
การใช้อนันต์forคืออะไร? ถ้าเซมาฟอร์ถูกปล่อยออกคิวจะมีอย่างน้อยหนึ่งรายการที่จะยกเลิกคิวไม่?
Blendester

2
@Blendester อาจมีเงื่อนไขการแข่งขันหากผู้บริโภคหลายรายถูกบล็อก เราไม่สามารถทราบได้อย่างแน่นอนว่าไม่มีผู้บริโภคที่แข่งขันกันอย่างน้อยสองรายและเราไม่รู้ว่าทั้งสองคนตื่นขึ้นมาก่อนที่พวกเขาจะออกจากรายการหรือไม่ ในกรณีที่มีการแข่งขันหากไม่สามารถจัดการได้มันจะกลับไปนอนและรอสัญญาณอีกครั้ง
John Leidegren

หากผู้บริโภคสองรายขึ้นไปผ่าน WaitAsync () จะมีจำนวนรายการที่เท่ากันในคิวดังนั้นพวกเขาจะยกเลิกคิวได้สำเร็จเสมอ ฉันพลาดอะไรไปรึเปล่า?
mindcruzer

2
นี่คือคอลเล็กชันที่ปิดกั้นความหมายของTryDequeueare ส่งคืนด้วยค่าหรือไม่ส่งคืนเลย ในทางเทคนิคหากคุณมีผู้อ่านมากกว่า 1 คนผู้อ่านคนเดียวกันสามารถใช้สองรายการ (หรือมากกว่า) ก่อนที่ผู้อ่านคนอื่นจะตื่นเต็มที่ ความสำเร็จWaitAsyncเป็นเพียงสัญญาณว่าอาจมีรายการอยู่ในคิวที่จะบริโภคไม่ใช่การรับประกัน
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.จากdocs.microsoft.com/en-us/dotnet/api/… ความสำเร็จWaitAsyncไม่มีรายการในคิวได้อย่างไร? หาก N release ปลุกผู้บริโภคมากกว่า N คนsemaphoreเสียมากกว่า ไม่ใช่เหรอ?
Ashish Negi

4

นี่คือการใช้งานขั้นพื้นฐานBlockingCollectionที่รองรับการรอคอยพร้อมคุณสมบัติที่ขาดหายไปมากมาย ใช้AsyncEnumerableไลบรารีซึ่งทำให้การแจงนับแบบอะซิงโครนัสเป็นไปได้สำหรับเวอร์ชัน C # ที่เก่ากว่า 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);
            }
        });
    }
}

ตัวอย่างการใช้งาน:

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

เอาท์พุต:

1 2 3 4 5 6 7 8 9 10


อัปเดต:ด้วยการเปิดตัว C # 8 การแจงนับแบบอะซิงโครนัสได้กลายเป็นคุณลักษณะของภาษาในตัว คลาสที่จำเป็น ( IAsyncEnumerable, IAsyncEnumerator) ถูกฝังอยู่ใน. NET Core 3.0 และมีให้เป็นแพ็คเกจสำหรับ. NET Framework 4.6.1+ ( Microsoft.BclAsyncInterfaces )

นี่คือการGetConsumingEnumerableใช้งานทางเลือกที่มีไวยากรณ์ C # 8 ใหม่:

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

สังเกตการอยู่ร่วมกันawaitและyieldในวิธีการเดียวกัน

ตัวอย่างการใช้งาน (C # 8):

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

หมายเหตุawaitก่อนหน้าไฟล์foreach.


1
ในตอนหลังตอนนี้ฉันคิดว่าชื่อชั้นAsyncBlockingCollectionเป็นเรื่องไร้สาระ บางสิ่งบางอย่างไม่สามารถเป็นแบบอะซิงโครนัสและบล็อกได้ในเวลาเดียวกันเนื่องจากทั้งสองแนวคิดนี้ตรงกันข้ามกัน!
Theodor Zoulias

-2

หากคุณไม่สนใจการแฮ็กเล็กน้อยคุณสามารถลองใช้ส่วนขยายเหล่านี้ได้

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
คุณจึงนำการหน่วงเวลาเทียมมาทำให้เป็น async? มันยังคงปิดกั้นใช่ไหม
nawfal
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.