C# 使用task.WhenAll和max degree of parallelism实现并行任务调用时,如何管理锁?

C# 使用task.WhenAll和max degree of parallelism实现并行任务调用时,如何管理锁?,c#,asynchronous,parallel-processing,locking,task,C#,Asynchronous,Parallel Processing,Locking,Task,我提出了以下代码,该代码以5的页面大小重复调用数据库分页函数,并且对于页面中的每个项,以4的最大并发度并行执行一个函数。到目前为止,它看起来很有效,但我不确定是否需要使用锁定来封装parallelInvocationTasks.Remove(completedTask)行和任务.WhenAll(parallelInvocationTasks.ToArray())那么我需要在这里使用锁定吗?您是否看到任何其他改进 这是密码 Program.cs using System; using System

我提出了以下代码,该代码以5的页面大小重复调用数据库分页函数,并且对于页面中的每个项,以4的最大并发度并行执行一个函数。到目前为止,它看起来很有效,但我不确定是否需要使用锁定来封装
parallelInvocationTasks.Remove(completedTask)行和
任务.WhenAll(parallelInvocationTasks.ToArray())那么我需要在这里使用锁定吗?您是否看到任何其他改进

这是密码

Program.cs

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

namespace ConsoleApp1
{
    class Program
    {
        private static async Task Main(string[] args)
        {
            Console.WriteLine("Starting");
            Func<int, int, CancellationToken, Task<IList<string>>> getNextPageFunction = GetNextPageFromDatabase;
            await getNextPageFunction.ForEachParallel(4, 5, new CancellationToken(), async (item) =>
            {
                Console.WriteLine($"{item} started");
                //simulate processing
                await Task.Delay(1000);
                Console.WriteLine($"{item} ended");
            });
            
            Console.WriteLine("Done");
        }

        private static async Task<IList<string>> GetNextPageFromDatabase(
            int offset,
            int pageSize,
            CancellationToken cancellationToken)
        {
            //simulate i/o and database paging
            await Task.Delay(2000, cancellationToken);
            var pageData = new List<string>();
            
            //simulate just 4 pages
            if (offset >= pageSize * 3)
            {
                return pageData;
            }

            for (var i = 1; i <= pageSize; i++)
            {
                string nextItem = $"Item {i + offset}";
                pageData.Add(nextItem);
            }

            return pageData;
        } 
    }
}
使用系统;
使用System.Collections.Generic;
使用系统线程;
使用System.Threading.Tasks;
名称空间控制台EAPP1
{
班级计划
{
专用静态异步任务主(字符串[]args)
{
控制台写入线(“启动”);
Func getNextPageFunction=GetNextPageFromDatabase;
等待getNextPageFunction.ForEachParallel(4,5,新的CancellationToken(),异步(项)=>
{
WriteLine($“{item}已启动”);
//模拟加工
等待任务。延迟(1000);
WriteLine($“{item}end”);
});
控制台。写入线(“完成”);
}
私有静态异步任务GetNextPageFromDatabase(
整数偏移,
int pageSize,
取消令牌(取消令牌)
{
//模拟i/o和数据库分页
等待任务。延迟(2000,取消令牌);
var pageData=新列表();
//只模拟4页
如果(偏移量>=页面大小*3)
{
返回页面数据;
}
对于(var i=1;i
{
尝试
{
等待功能(项目);
}
最后
{
//ReSharper禁用一次性访问DisposedClosure
//只要在使用信号量之前调用Task.WhenAll,这是安全的
//外壳端部
semaphore.Release();
}
},取消令牌)
.Unwrap();
parallelInvocationTasks.Add(forEachFunctionTask);
#pragma警告禁用4014
forEachFunctionTask.ContinueWith((completedTask)=>
#pragma警告恢复4014
{
if(completedTask.Exception==null)
{
//目的是在完成任务时在枚举过程中释放已完成的任务
//这是为了确保“parallelInvocationTasks”列表不会
//以非托管方式增长,导致列表包含多个已完成的任务
//每次添加调用任务都会不必要地消耗更多内存
//因此,下面的最终Task.wheall调用只需要等待出现故障的任务
//导致它仅抛出异常和/或不完整任务的最小列表
parallelInvocationTasks.Remove(completedTask);
}
},取消令牌);
enumeratedCount+=1;
}
偏移量+=页面大小;
}
而(items.Count>=页面大小);
wait Task.WhenAll(parallelInvocationTasks.ToArray());
}
返回枚举计数;
}
}
}

好的,根据上面的评论和更多的研究,我得出了这个答案,它完成了工作,而无需编写自定义代码来管理并发。它使用来自TPL数据流的ActionBlock

PagingExtensions.cs

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

namespace ConsoleApp1
{
    public static class PagingExtensions
    {
        public static async Task<int> ForEachParallel<TItem>(
            this Func<int, int, CancellationToken, Task<IList<TItem>>> getNextPageFunction,
            int concurrency,
            int pageSize,
            CancellationToken cancellationToken,
            Func<TItem, Task> forEachFunction)
        {
            var enumeratedCount = 0;
            if (getNextPageFunction == null || forEachFunction == null)
            {
                return enumeratedCount;
            }

            var offset = 0;
            using (var semaphore = new SemaphoreSlim(concurrency))
            {
                IList<Task> parallelInvocationTasks = new List<Task>();
                IList<TItem> items;
                
                do
                {
                    items = await getNextPageFunction(offset, pageSize, cancellationToken) ?? new List<TItem>();
                    foreach (TItem item in items)
                    {
                        await semaphore.WaitAsync(cancellationToken);
                        Task forEachFunctionTask = Task.Factory.StartNew(async () =>
                            {
                                try
                                {
                                    await forEachFunction(item);
                                }
                                finally
                                {
                                    // ReSharper disable once AccessToDisposedClosure
                                    // This is safe as long as Task.WhenAll is called before the using semaphore
                                    // enclosure ends
                                    semaphore.Release();
                                }
                            }, cancellationToken)
                            .Unwrap();

                        parallelInvocationTasks.Add(forEachFunctionTask);
                        
#pragma warning disable 4014
                        forEachFunctionTask.ContinueWith((completedTask) =>
#pragma warning restore 4014
                        {
                            if (completedTask.Exception == null)
                            {
                                //Intention is to release completed tasks during enumeration as they complete
                                //so they can be GCed. This is to ensure the 'parallelInvocationTasks' list does not
                                //grow in an unmanaged manner resulting in a list holding multiple completed tasks
                                //unnecessarily consuming more memory with each added invocation task
                                //Thus the final Task.WhenAll call below will only need to await only faulted tasks
                                //causing it to throw an exception and/or a minimal list of incomplete tasks only
                                parallelInvocationTasks.Remove(completedTask);
                            }
                        }, cancellationToken);

                        enumeratedCount += 1;
                    }
                    
                    offset += pageSize;
                }
                while (items.Count >= pageSize);
                
                await Task.WhenAll(parallelInvocationTasks.ToArray());
            }

            return enumeratedCount;
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

namespace ConsoleApp1
{
    public static class PagingExtensions
    {
        public delegate Task<IList<TItem>> GetNextPageDelegate<TItem>(
            int offset,
            int pageSize,
            CancellationToken cancellationToken);

        public static async Task<int> EnumerateParallel<TItem>(
            this GetNextPageDelegate<TItem> getNextPageFunction,
            int maxDegreeOfParallelism,
            int pageSize,
            CancellationToken cancellationToken,
            Func<TItem, Task> forEachFunction)
        {
            var enumeratedCount = 0;
            if (getNextPageFunction == null || forEachFunction == null)
            {
                return enumeratedCount;
            }

            var offset = 0;
            var forEachFunctionBlock = new ActionBlock<TItem>(forEachFunction, new ExecutionDataflowBlockOptions
            {
                BoundedCapacity = pageSize > maxDegreeOfParallelism ? pageSize : maxDegreeOfParallelism,
                EnsureOrdered = false,
                MaxDegreeOfParallelism = maxDegreeOfParallelism,
                CancellationToken = cancellationToken
            });

            IList<TItem> items;

            do
            {
                items = await getNextPageFunction(offset, pageSize, cancellationToken) ?? new List<TItem>();
                foreach (TItem item in items)
                {
                    await forEachFunctionBlock.SendAsync(item, cancellationToken);
                    enumeratedCount += 1;
                }

                offset += pageSize;
            }
            while (items.Count >= pageSize);

            forEachFunctionBlock.Complete();
            await forEachFunctionBlock.Completion;

            return enumeratedCount;
        }
    }
}
使用系统;
使用System.Collections.Generic;
使用System.Linq;
使用系统线程;
使用System.Threading.Tasks;
使用System.Threading.Tasks.Dataflow;
名称空间控制台EAPP1
{
公共静态类分页扩展
{
公共委托任务GetNextPageDelegate(
整数偏移,
int pageSize,
取消令牌取消令牌);
公共静态异步任务枚举并行(
此GetNextPageDelegate getNextPageFunction,
int maxDegreeOfParallelism,
int pageSize,
CancellationToken CancellationToken,
函数(函数)
{
var enumeratedCount=0;
if(getNextPageFunction==null | | forEachFunction==null)
{
返回枚举计数;
}
var偏移=0;
var forEachFunctionBlock=新操作块(forEachFunction,新ExecutionDataflowBlockOptions
{
BoundedCapacity=pageSize>maxDegreeOfParallelism?pageSize:maxDegreeOfParallelism,
重新排序=错误,
MaxDegreeOfParallelism=MaxDegreeOfParallelism,
CancellationToken=CancellationToken
});
IList项目;
做
{
items=等待getNextPageFunction(偏移量、页面大小、取消令牌)??新建列表();
foreach(项目中的项目)
{
等待forEachFunctionBlock.SendAsync(项,取消令牌);
enumeratedCount+=1;
}
关闭