C# 并行长时间运行任务的时间优化 简介

C# 并行长时间运行任务的时间优化 简介,c#,parallel-processing,C#,Parallel Processing,我使用的是一个复杂的外部库,我试图在一个大的项目列表上执行它的功能。这个库没有公开一个好的异步接口,所以我只能使用一些非常老式的代码 我的目标是优化完成一批处理所需的时间,并演示问题,而不必包括实际的第三方库。我已创建了以下问题的近似值 问题 给定一个非异步操作,您可以提前知道该操作的“大小”(即复杂性): public interface IAction { int Size { get; } void Execute(); } 鉴于此操作有3种变体: public clas

我使用的是一个复杂的外部库,我试图在一个大的项目列表上执行它的功能。这个库没有公开一个好的异步接口,所以我只能使用一些非常老式的代码

我的目标是优化完成一批处理所需的时间,并演示问题,而不必包括实际的第三方库。我已创建了以下问题的近似值

问题 给定一个非异步操作,您可以提前知道该操作的“大小”(即复杂性):

public interface IAction
{
    int Size { get; }
    void Execute();
}
鉴于此操作有3种变体:

public class LongAction : IAction
{
    public int Size => 10000;
    public void Execute()
    {
        Thread.Sleep(10000);
    }
}

public class MediumAction : IAction
{

    public int Size => 1000;
    public void Execute()
    {
        Thread.Sleep(1000);
    }
}

public class ShortAction : IAction
{
    public int Size => 100;
    public void Execute()
    {
        Thread.Sleep(100);
    }
}
如何优化这些操作的长列表,以便在以某种并行方式运行时,整个批处理尽可能快地完成

天真地说,你可以把所有的东西都放在一个并行的.ForEach上,具有相当高的并行性,这当然是可行的,但是必须有一种方法来优化它们,以便首先开始一些最大的

为了进一步说明这个问题,我们举一个超级简化的例子

  • 1个10号任务
  • 5个2号任务
  • 10项大小为1的任务
和2个可用线程。我可以想出2种(多种)方法来安排这些任务(黑条是死区时间-无需安排):

显然,第一个比第二个提前完成

最小完全可验证代码 如果有人喜欢bash,则完整的测试代码(尝试使其比下面我的天真实现更快):

类程序
{
静态void Main(字符串[]参数)
{
MainAsync().GetAwaiter().GetResult();
Console.ReadLine();
}
静态异步任务mainsync()
{
var list=新列表();
对于(var i=0;i<200;i++)list.Add(new LongAction());
对于(var i=0;i<200;i++)list.Add(new MediumAction());
对于(vari=0;i<200;i++)list.Add(newshortaction());
var swSync=Stopwatch.StartNew();
ForEach(列表,新的ParallelOptions{MaxDegreeOfParallelism=20},action=>
{
WriteLine($“{DateTime.Now:HH:mm:ss}:在线程{thread.CurrentThread.ManagedThreadId}上启动操作{action.GetType().Name}”);
var sw=Stopwatch.StartNew();
action.Execute();
sw.Stop();
WriteLine($“{DateTime.Now:HH:mm:ss}:在线程{thread.CurrentThread.ManagedThreadId}上的{sw.elapsedmillesons}ms中完成了动作{action.GetType().Name}”);
});
swSync.Stop();
WriteLine($”在{swSync.elapsedmillesons}ms内完成);
}
}
公共接口
{
整数大小{get;}
void Execute();
}
公共集体诉讼:IAction
{
公共整数大小=>10000;
public void Execute()
{
睡眠(10000);
}
}
公共类医疗:IAction
{
公共整数大小=>1000;
public void Execute()
{
睡眠(1000);
}
}
公共团体诉讼:IAction
{
公共整数大小=>100;
public void Execute()
{
睡眠(100);
}
}

我认为问题如下

您有一个整数列表和数量有限的求和。 您需要一个将整数求和到求和中的算法,以便求和的最大值是可能的最小值

例如:

正如您所见,边界因子是运行时间最长的任务。较短的可以很容易地并行或在更短的时间内送达。它类似于背包,但最终归结为一个非常简单的任务“最长优先”排序

伪代码(使用我发明的类)是:

while (taskManager.HasTasks())
{
    task = taskManager.GetLongestTask();
    thread = threadManager.GetFreeThread(); // blocks if no thread available
    thread.Run(task);
}

这只是伪代码,不是并行/异步和块。我希望你能从中得到一些有用的东西。

好吧,这要看情况而定。在我的硬件上,如果我简单地将循环更改为首先运行所有长任务,则您的人为示例(修改后的睡眠时间为1000100和10ms,因为我没有一整天的睡眠时间)将快约30%(~15s vs~22s):

Parallel.ForEach(list.OrderByDescending(l=>l.Size), action => ...
当然,这完全取决于这些任务的负载。如果两个不同的任务大量使用同一资源(例如共享数据库),那么并行运行这两个任务的收益可能非常有限,因为它们最终会在一定程度上相互锁定一段时间

我建议您需要进行更深入的分析,然后根据任务的实际执行情况,以“并行能力”的方式对任务进行分组,并尝试确保您使用尽可能多的“兼容”任务运行尽可能多的并行线程。。。当然,如果一项特定的任务似乎总是花费所有其他任务的时间,那么请确保首先开始一项任务


很难用这里给出的细节给出更好的建议。

按任务大小降序排序,然后使用TaskFactory在不同的任务中执行每个任务,节省了大量的运行时间。平行度水平保持在20。 结果是:原始样本中的114676ms与193713ms。(改善约40%)

编辑:在您的特定示例中,列表仍然从get go开始排序,但Parallel.ForEach不保留输入顺序

static async Task MainAsync()
{
    var list = new List<IAction>();
    for (var i = 0; i < 200; i++) list.Add(new LongAction());
    for (var i = 0; i < 200; i++) list.Add(new MediumAction());
    for (var i = 0; i < 200; i++) list.Add(new ShortAction());

    Console.WriteLine("Sorting...");
    list.Sort((x, y) => y.Size.CompareTo(x.Size));
    int totalTasks = 0;

    int degreeOfParallelism = 20;
    var swSync = Stopwatch.StartNew();
    using (SemaphoreSlim semaphore = new SemaphoreSlim(degreeOfParallelism))
    {
        foreach (IAction action in list)
        {
            semaphore.Wait();
            Task.Factory.StartNew(() =>
            {
                try
                {
                    Console.WriteLine($"{DateTime.Now:HH:mm:ss}: Starting action {action.GetType().Name} on thread {Thread.CurrentThread.ManagedThreadId}");
                    var sw = Stopwatch.StartNew();
                    action.Execute();
                    sw.Stop();
                    Console.WriteLine($"{DateTime.Now:HH:mm:ss}: Finished action {action.GetType().Name} in {sw.ElapsedMilliseconds}ms on thread {Thread.CurrentThread.ManagedThreadId}");
                }
                finally
                {
                    totalTasks++;
                    semaphore.Release();
                }
            });
        }

        // Wait for remaining tasks....
        while (semaphore.CurrentCount < 20)
        { }

        swSync.Stop();
        Console.WriteLine($"Done in {swSync.ElapsedMilliseconds}ms");
        Console.WriteLine("Performed total tasks: " + totalTasks);
    }
}
static async Task mainaync()
{
var list=新列表();
对于(var i=0;i<200;i++)list.Add(new LongAction());
对于(var i=0;i<200;i++)list.Add(new MediumAction());
对于(vari=0;i<200;i++)list.Add(newshortaction());
控制台。WriteLine(“排序…”);
list.Sort((x,y)=>y.Size.CompareTo(x.Size));
int totalTasks=0;
int degreeOfParallelism=20;
var swSync=Stopwatch.StartNew();
使用(SemaphoreSlim semaphore=新的SemaphoreSlim(degreeOfParallelism))
{
foreach(列表中的IAction操作)
{
semaphore.Wait();
Task.Factory.StartNew(()=>
{
尝试
{
Parallel.ForEach(list.OrderByDescending(l=>l.Size), action => ...
static async Task MainAsync()
{
    var list = new List<IAction>();
    for (var i = 0; i < 200; i++) list.Add(new LongAction());
    for (var i = 0; i < 200; i++) list.Add(new MediumAction());
    for (var i = 0; i < 200; i++) list.Add(new ShortAction());

    Console.WriteLine("Sorting...");
    list.Sort((x, y) => y.Size.CompareTo(x.Size));
    int totalTasks = 0;

    int degreeOfParallelism = 20;
    var swSync = Stopwatch.StartNew();
    using (SemaphoreSlim semaphore = new SemaphoreSlim(degreeOfParallelism))
    {
        foreach (IAction action in list)
        {
            semaphore.Wait();
            Task.Factory.StartNew(() =>
            {
                try
                {
                    Console.WriteLine($"{DateTime.Now:HH:mm:ss}: Starting action {action.GetType().Name} on thread {Thread.CurrentThread.ManagedThreadId}");
                    var sw = Stopwatch.StartNew();
                    action.Execute();
                    sw.Stop();
                    Console.WriteLine($"{DateTime.Now:HH:mm:ss}: Finished action {action.GetType().Name} in {sw.ElapsedMilliseconds}ms on thread {Thread.CurrentThread.ManagedThreadId}");
                }
                finally
                {
                    totalTasks++;
                    semaphore.Release();
                }
            });
        }

        // Wait for remaining tasks....
        while (semaphore.CurrentCount < 20)
        { }

        swSync.Stop();
        Console.WriteLine($"Done in {swSync.ElapsedMilliseconds}ms");
        Console.WriteLine("Performed total tasks: " + totalTasks);
    }
}
var sorted = list.OrderByDescending(a => a.Size).ToArray();
var partitioner=Partitioner.Create(sorted, loadBalance:true);

Parallel.ForEach(partitioner, options, action =>...);
var sorted = list.OrderByDescending(a => a.Size);
var partitioner=Partitioner.Create(sorted,EnumerablePartitionerOptions.NoBuffering);