web-dev-qa-db-ja.com

非同期タスクの調整

非同期タスクの束を実行したいのですが、いつでも完了を保留できるタスクの数に制限があります。

1000個のURLがあり、一度に50個のリクエストのみをオープンしたいとします。ただし、1つの要求が完了するとすぐに、リスト内の次のURLへの接続を開きます。これにより、URLリストが使い果たされるまで、一度に常に正確に50の接続が開かれます。

また、可能であれば、指定された数のスレッドを利用したいです。

私が望むことをする拡張メソッドThrottleTasksAsyncを思いつきました。もっと簡単な解決策は既にありますか?これは一般的なシナリオだと思います。

使用法:

_class Program
{
    static void Main(string[] args)
    {
        Enumerable.Range(1, 10).ThrottleTasksAsync(5, 2, async i => { Console.WriteLine(i); return i; }).Wait();

        Console.WriteLine("Press a key to exit...");
        Console.ReadKey(true);
    }
}
_

コードは次のとおりです。

_static class IEnumerableExtensions
{
    public static async Task<Result_T[]> ThrottleTasksAsync<Enumerable_T, Result_T>(this IEnumerable<Enumerable_T> enumerable, int maxConcurrentTasks, int maxDegreeOfParallelism, Func<Enumerable_T, Task<Result_T>> taskToRun)
    {
        var blockingQueue = new BlockingCollection<Enumerable_T>(new ConcurrentBag<Enumerable_T>());

        var semaphore = new SemaphoreSlim(maxConcurrentTasks);

        // Run the throttler on a separate thread.
        var t = Task.Run(() =>
        {
            foreach (var item in enumerable)
            {
                // Wait for the semaphore
                semaphore.Wait();
                blockingQueue.Add(item);
            }

            blockingQueue.CompleteAdding();
        });

        var taskList = new List<Task<Result_T>>();

        Parallel.ForEach(IterateUntilTrue(() => blockingQueue.IsCompleted), new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism },
        _ =>
        {
            Enumerable_T item;

            if (blockingQueue.TryTake(out item, 100))
            {
                taskList.Add(
                    // Run the task
                    taskToRun(item)
                    .ContinueWith(tsk =>
                        {
                            // For effect
                            Thread.Sleep(2000);

                            // Release the semaphore
                            semaphore.Release();

                            return tsk.Result;
                        }
                    )
                );
            }
        });

        // Await all the tasks.
        return await Task.WhenAll(taskList);
    }

    static IEnumerable<bool> IterateUntilTrue(Func<bool> condition)
    {
        while (!condition()) yield return true;
    }
}
_

このメソッドは、BlockingCollectionおよびSemaphoreSlimを使用して機能させます。スロットルは1つのスレッドで実行され、すべての非同期タスクは他のスレッドで実行されます。並列処理を実現するために、whileループとして再利用される_Parallel.ForEach_ループに渡されるmaxDegreeOfParallelismパラメーターを追加しました。

古いバージョンは:

_foreach (var master = ...)
{
    var details = ...;
    Parallel.ForEach(details, detail => {
        // Process each detail record here
    }, new ParallelOptions { MaxDegreeOfParallelism = 15 });
    // Perform the final batch updates here
}
_

ただし、スレッドプールはすぐに使い果たされるため、async/awaitを実行することはできません。

ボーナス: Take()が呼び出されたときにCompleteAdding()で例外がスローされるBlockingCollectionの問題を回避するには、TryTakeオーバーロードを使用します。タイムアウト。 TryTakeでタイムアウトを使用しなかった場合、BlockingCollectionはブロックされないため、TryTakeを使用する目的に反します。もっと良い方法はありますか?理想的には、TakeAsyncメソッドがあります。

51
Josh Wyant

提案されているように、TPL Dataflowを使用します。

A TransformBlock<TInput, TOutput> はあなたが探しているものかもしれません。

MaxDegreeOfParallelismを定義して、並行して変換できる文字列の数(つまり、ダウンロードできるURLの数)を制限します。次に、ブロックにURLを投稿します。完了したら、アイテムの追加が完了したことをブロックに伝え、応答を取得します。

var downloader = new TransformBlock<string, HttpResponse>(
        url => Download(url),
        new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 50 }
    );

var buffer = new BufferBlock<HttpResponse>();
downloader.LinkTo(buffer);

foreach(var url in urls)
    downloader.Post(url);
    //or await downloader.SendAsync(url);

downloader.Complete();
await downloader.Completion;

IList<HttpResponse> responses;
if (buffer.TryReceiveAll(out responses))
{
    //process responses
}

注:TransformBlockは、入力と出力の両方をバッファリングします。では、なぜそれをBufferBlockにリンクする必要があるのですか?

TransformBlockはすべてのアイテム(HttpResponse)が消費されるまで完了せず、await downloader.Completionがハングします。代わりに、downloaderがすべての出力を専用のバッファブロックに転送するようにします。その後、downloaderが完了するまで待機し、バッファブロックを検査します。

52
dcastro

1000個のURLがあり、一度に50個のリクエストのみをオープンしたいとします。ただし、1つの要求が完了するとすぐに、リスト内の次のURLへの接続を開きます。これにより、URLリストが使い果たされるまで、一度に常に正確に50の接続が開かれます。

ここでは、次の簡単な解決策が何度も登場しています。ブロックコードを使用せず、スレッドを明示的に作成しないため、非常に優れた拡張性があります。

const int MAX_DOWNLOADS = 50;

static async Task DownloadAsync(string[] urls)
{
    using (var semaphore = new SemaphoreSlim(MAX_DOWNLOADS))
    using (var httpClient = new HttpClient())
    {
        var tasks = urls.Select(async url => 
        {
            await semaphore.WaitAsync();
            try
            {
                var data = await httpClient.GetStringAsync(url);
                Console.WriteLine(data);
            }
            finally
            {
                semaphore.Release();
            }
        });

        await Task.WhenAll(tasks);
    }
}

問題は、ダウンロードされたデータの処理differentパイプライン上で、differentレベルの並列処理で、特にそれがCPUバウンド処理。

たとえば、データ処理(CPUコアの数)を同時に実行する4つのスレッドと、追加のデータ(スレッドをまったく使用しない)に対する最大50の保留中の要求が必要になります。 AFAICT、これはあなたのコードが現在行っていることではありません。

そこで、TPL DataflowまたはRxが優先ソリューションとして役立ちます。それでも、プレーンTPLでこのようなものを実装することは確かに可能です。ここでの唯一のブロックコードは、Task.Run内で実際のデータ処理を実行するコードです。

const int MAX_DOWNLOADS = 50;
const int MAX_PROCESSORS = 4;

// process data
class Processing
{
    SemaphoreSlim _semaphore = new SemaphoreSlim(MAX_PROCESSORS);
    HashSet<Task> _pending = new HashSet<Task>();
    object _lock = new Object();

    async Task ProcessAsync(string data)
    {
        await _semaphore.WaitAsync();
        try
        {
            await Task.Run(() =>
            {
                // simuate work
                Thread.Sleep(1000);
                Console.WriteLine(data);
            });
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public async void QueueItemAsync(string data)
    {
        var task = ProcessAsync(data);
        lock (_lock)
            _pending.Add(task);
        try
        {
            await task;
        }
        catch
        {
            if (!task.IsCanceled && !task.IsFaulted)
                throw; // not the task's exception, rethrow
            // don't remove faulted/cancelled tasks from the list
            return;
        }
        // remove successfully completed tasks from the list 
        lock (_lock)
            _pending.Remove(task);
    }

    public async Task WaitForCompleteAsync()
    {
        Task[] tasks;
        lock (_lock)
            tasks = _pending.ToArray();
        await Task.WhenAll(tasks);
    }
}

// download data
static async Task DownloadAsync(string[] urls)
{
    var processing = new Processing();

    using (var semaphore = new SemaphoreSlim(MAX_DOWNLOADS))
    using (var httpClient = new HttpClient())
    {
        var tasks = urls.Select(async (url) =>
        {
            await semaphore.WaitAsync();
            try
            {
                var data = await httpClient.GetStringAsync(url);
                // put the result on the processing pipeline
                processing.QueueItemAsync(data);
            }
            finally
            {
                semaphore.Release();
            }
        });

        await Task.WhenAll(tasks.ToArray());
        await processing.WaitForCompleteAsync();
    }
}
44
noseratio

要求されたように、ここに私が行くことになったコードがあります。

作業はマスター/ディテール構成で設定され、各マスターはバッチとして処理されます。各作業単位は、この方法でキューに入れられます。

var success = true;

// Start processing all the master records.
Master master;
while (null != (master = await StoredProcedures.ClaimRecordsAsync(...)))
{
    await masterBuffer.SendAsync(master);
}

// Finished sending master records
masterBuffer.Complete();

// Now, wait for all the batches to complete.
await batchAction.Completion;

return success;

マスターは一度に1つずつバッファリングされ、他の外部プロセスの作業を節約します。各マスターの詳細は、masterTransformTransformManyBlockを介して作業のために送信されます。 BatchedJoinBlockも作成され、詳細を1つのバッチで収集します。

実際の作業は、detailTransformTransformBlockで一度に150ずつ非同期的に行われます。 BoundedCapacityは300に設定され、チェーンの開始時にバッファリングされないマスターが多すぎないようにします。また、一度に150レコードを処理できるように十分な詳細レコードをキューに入れる余地を残します。ブロックは、objectであるかDetailであるかに応じてリンク全体でフィルタリングされるため、Exceptionをターゲットに出力します。

batchActionActionBlockは、すべてのバッチからの出力を収集し、各バッチの一括データベース更新、エラーロギングなどを実行します。

マスターごとに1つずつ、複数のBatchedJoinBlocksがあります。各ISourceBlockは順次出力され、各バッチは1つのマスターに関連付けられた詳細レコードの数のみを受け入れるため、バッチは順番に処理されます。各ブロックは1つのグループのみを出力し、完了時にリンク解除されます。最後のバッチブロックのみが、その完了を最後のActionBlockに伝播します。

データフローネットワーク:

// The dataflow network
BufferBlock<Master> masterBuffer = null;
TransformManyBlock<Master, Detail> masterTransform = null;
TransformBlock<Detail, object> detailTransform = null;
ActionBlock<Tuple<IList<object>, IList<object>>> batchAction = null;

// Buffer master records to enable efficient throttling.
masterBuffer = new BufferBlock<Master>(new DataflowBlockOptions { BoundedCapacity = 1 });

// Sequentially transform master records into a stream of detail records.
masterTransform = new TransformManyBlock<Master, Detail>(async masterRecord =>
{
    var records = await StoredProcedures.GetObjectsAsync(masterRecord);

    // Filter the master records based on some criteria here
    var filteredRecords = records;

    // Only propagate completion to the last batch
    var propagateCompletion = masterBuffer.Completion.IsCompleted && masterTransform.InputCount == 0;

    // Create a batch join block to encapsulate the results of the master record.
    var batchjoinblock = new BatchedJoinBlock<object, object>(records.Count(), new GroupingDataflowBlockOptions { MaxNumberOfGroups = 1 });

    // Add the batch block to the detail transform pipeline's link queue, and link the batch block to the the batch action block.
    var detailLink1 = detailTransform.LinkTo(batchjoinblock.Target1, detailResult => detailResult is Detail);
    var detailLink2 = detailTransform.LinkTo(batchjoinblock.Target2, detailResult => detailResult is Exception);
    var batchLink = batchjoinblock.LinkTo(batchAction, new DataflowLinkOptions { PropagateCompletion = propagateCompletion });

    // Unlink batchjoinblock upon completion.
    // (the returned task does not need to be awaited, despite the warning.)
    batchjoinblock.Completion.ContinueWith(task =>
    {
        detailLink1.Dispose();
        detailLink2.Dispose();
        batchLink.Dispose();
    });

    return filteredRecords;
}, new ExecutionDataflowBlockOptions { BoundedCapacity = 1 });

// Process each detail record asynchronously, 150 at a time.
detailTransform = new TransformBlock<Detail, object>(async detail => {
    try
    {
        // Perform the action for each detail here asynchronously
        await DoSomethingAsync();

        return detail;
    }
    catch (Exception e)
    {
        success = false;
        return e;
    }

}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 150, BoundedCapacity = 300 });

// Perform the proper action for each batch
batchAction = new ActionBlock<Tuple<IList<object>, IList<object>>>(async batch =>
{
    var details = batch.Item1.Cast<Detail>();
    var errors = batch.Item2.Cast<Exception>();

    // Do something with the batch here
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4 });

masterBuffer.LinkTo(masterTransform, new DataflowLinkOptions { PropagateCompletion = true });
masterTransform.LinkTo(detailTransform, new DataflowLinkOptions { PropagateCompletion = true });
3
Josh Wyant