如何解决挂C#的任务?

时间:2019-01-21 21:07:43

标签: c# .net task-parallel-library

在控制台应用程序中浏览一些代码时,我在SecondInitialize函数中看到了嵌套的Task.WhenAll。我决定使用较大的“位置列表”测试此功能,然后查看其反应。

我看到的是,大约有100个位置,100 * 100 = 10,000个Calculate调用,t.Wait()内部的Start大约需要60秒才能返回,有时甚至完全挂起。如果我尝试单击Break All,则控制台应用程序甚至无法响应我的单击,Visual Studio崩溃。

SecondInitialize中使用“易于阅读的版本”时,还需要一段时间才能返回。行为一致。

现在很奇怪的是,每当我使用调试器并将断点放在SecondInitialize内,然后单击继续时,它将在5到7秒钟内完成。

所以我的问题是,当我在该函数中进行调试时看到它更快时,为什么通常要花很长时间挂起?另一个问题是Tasks的使用是否正确

public void Start()
{
    var t = CacheInitialize(locations, CancellationToken.None);
    t.Wait();
}

public Task CacheInitialize(IList<Location> locations, CancellationToken token)
{
    return SecondInitialize(locations, token);
}

public async Task SecondInitialize(IList<Location> locations, CancellationToken token)
{
    await Task.WhenAll(locations.Select(first =>
    {
        return Task.WhenAll(locations.Where(second => !second.Equals(first)).Select(second =>
        {
            return Calculate(first, second, token);
        }));
    }));

    //Easier to read version of ^
    //var tasks = locations.SelectMany(first => locations.Where(second => !second.Equals(first)).Select(second =>
    //{
    //  return Calculate(first, second, token);
    //}));
    //await Task.WhenAll(tasks);


    //No Tasks.
    //for (int x = 0; x < locations.Length; x++)
    //{
    //    for (int y = 0; y < locations.Length; y++)
    //    {
    //        if (x == y)
    //            continue;
    //        await Calculate(locations[x], locations[y], token).ConfigureAwait(false);
    //    }
    //}
}

public async Task<TripLength> Calculate(Location start, Location finish, CancellationToken token)
{
    if (start == finish)
        return TripLength.Zero;

    var parameters = new RouteParameters
    {
        Coordinates = new []
        {
            new Coordinate(start.Latitude, start.Longitude),
            new Coordinate(finish.Latitude, finish.Longitude)
        }
    };

    var route = await RunRoute(parameters, token);

    return ToTripLength(route);
}


protected Task<RouteResult> RunRoute(RouteParameters routeParams, CancellationToken token)
{
    return Task.Run(async () =>
    {
        var routingTask = Task.Run(() =>
        {
            RouteResult routeResults;
            var status = _routeService.Route(routeParams, out routeResults);
            return routeResults;
        }, token);
    return await routingTask.ConfigureAwait(false);

    }, token);
}

2 个答案:

答案 0 :(得分:1)

问题似乎在于如何从连接一组位置(起点和目的地?)的所有行程中计算路线,并计算每条路线的长度(费用?)。费用昂贵的工作似乎是对_routeService.RouteToTripLength的呼吁。

从100个位置计算10K组合很简单,不需要并行化。一个简单的LINQ查询将起作用:

var combinations=( from start in locations
                   from finish in locations
                   where start!=finish
                   select (start,finish))
                 .ToArray();

此后会发生什么取决于_routeService.Route的行为。如果它是本地库,这是一个数据并行性问题,试图以最有效的方式计算10K数据点。可以用PLINQ

处理

如果是对外部服务的调用,则是并发性问题,不应浪费CPU时间等待10K远程请求进行响应。

假设_routeService.Route是本地库,则可以使用PLINQ。几个辅助方法将使编写查询更容易:

RouteParameters locationsToParams((Location start,Location finish) combination)
{
    return new RouteParameters {
        Coordinates = new[]
        {
            new Coordinate( start.Latitude, start.Longitude ),
            new Coordinate( finish.Latitude, finish.Longitude )
        }
    };
}

RouteResult  callRoute(RouteParameters routeParams)
{
    _routeService.Route(routeParams, out var routeResults);
    return routeResults;
}

var tripLengths = from cmb in combinations.AsParallel()
                  let routeParams=locationsToParams(cmb)
                  let result=callRoute(routeParams)
                  select ToTripLength(result);
var finalResults = tripLengths.ToArray();

AsParallel()将采用输入IEnumerable,在这种情况下是组合,将其划分为与内核一样多的分区,然后对每个分区使用 one 工作任务。每个分区的数据都馈送到其辅助任务,从而最大程度地降低了同步成本。

由于每次对Route的调用都将在其中一个工作任务上运行,因此它可以用作快速且相当肮脏的方式来发出10K远程请求。这很浪费,因为它仅阻塞任务以等待响应。 WithDegreeOfParallelism可以用来比内核使用更多的工作任务,但这仍然浪费CPU等待响应的时间。阻塞调用在线程挂起之前从SpinWait开始,这意味着对远程服务的阻塞调用可以使用CPU内核,而无需执行任何操作。这会严重损害服务器环境上的可伸缩性。

var tripLengths = from cmb in combinations.AsParallel()
                                          .WithDegreeOfParalellism(10)
                  let routeParams=locationsToParams(cmb)
                  let result=callRoute(routeParams)
                  select ToTripLength(result);
var finalResults = tripLengths.ToArray();

答案 1 :(得分:0)

由于您的示例不完整且无法编译,因此很难看到您到底想做什么。

但据我所知有几个问题:

  1. 在任务can lead to deadlocks上调用Wait(或Result)。 使用ConfigureAwait( false )将有助于避免此类问题,但不能消除所有问题。 因此,当您要访问任务的结果时,最好始终等待它。

  2. 我看不到您在Task.WhenAll内嵌套Task.WhenAll所要完成的工作。 WhenAll返回一个任务,您可以在没有Task.WhenAll的情况下等待。 您创建的每个任务都会增加一些性能开销,因此您应该尝试创建尽可能少的任务。

  3. 使用Task.Run和一个异步委托来等待另一个任务(由Task.Run创建)没有意义,您正在创建的任务超出了您的需要。 您可以等待一个任务。运行

我试图根据您的代码创建一个有效的示例(它不会做任何工作),以显示您应该更改的内容。 请注意,异步Main方法仅在C#7.1或更高版本中可用。

public class Program
{
    public static async Task Main( String[] args )
    {
        var foo = new Foo();

        var sw = Stopwatch.StartNew();

        await foo.Start();

        sw.Stop();

        Console.WriteLine($"Elapsed {sw.Elapsed} {sw.ElapsedMilliseconds}ms");
        Console.ReadLine();
    }
}

public class Foo
{
    public async Task CacheInitialize( IList<Location> locations, CancellationToken token ) =>
        await SecondInitialize( locations, token )
            .ConfigureAwait( false );

    public async Task<TripLength> Calculate( Location start, Location finish, CancellationToken token )
    {
        if ( start == finish )
            return TripLength.Zero;

        var parameters = new RouteParameters
        {
            Coordinates = new[]
            {
                new Coordinate( start.Latitude, start.Longitude ),
                new Coordinate( finish.Latitude, finish.Longitude )
            }
        };

        var route = await RunRoute( parameters, token );

        return new TripLength();
    }

    public async Task SecondInitialize( IList<Location> locations, CancellationToken token )
    {
        var tasks = new List<Task>( locations.Count );

        foreach ( var outer in locations )
        foreach ( var inner in locations )
        {
            if ( inner.Equals( outer ) )
                continue;

            tasks.Add( Calculate( outer, inner, token ) );
        }

        await Task.WhenAll( tasks );
    }

    public async Task Start()
    {
        var locations = new List<Location>();
        await CacheInitialize( locations, CancellationToken.None )
            .ConfigureAwait( false );
    }

    protected async Task<RouteResult> RunRoute( RouteParameters routeParams, CancellationToken token )
    {
        return await Task
                     .Run( () =>
                           {
                               //RouteResult routeResults;
                               //var status = _routeService.Route( routeParams, out routeResults );
                               //return routeResults;
                               return new RouteResult();
                           },
                           token )
                     .ConfigureAwait( false );
    }
}

public class Coordinate
{
    public Double Latitude { get; }
    public Double Longitude { get; }
    public Coordinate( Double latitude, Double longitude )
    {
        Latitude = latitude;
        Longitude = longitude;
    }
}
public class RouteParameters
{
    public Coordinate[] Coordinates { get; set; }
}
public class TripLength
{
    public static TripLength Zero = new TripLength();
}
public class RouteResult
{
}
public class Location
{
    public Double Latitude { get; }
    public Double Longitude { get; }
}