控制台应用程序完成工作后需要很长时间才能退出

Console app takes a long time to exit after finishing work

我有一个查询数据库的控制台应用程序,然后 post 循环将一些记录发送到 REST API(api 不支持批处理 posting,所以我必须循环遍历每条记录和 post,如果相关的话)。数据库访问速度很快,没有问题,根据我设置的计时器,api post 循环也是如此,但是应用程序本身在工作完成后需要很长时间才能退出。

这是在我引入 Parallel.Foreach 以加快 posting 之后开始发生的。在使用非并行循环之前,post 处理 1000 条记录平均需要大约 10 分钟,但应用程序会 return 并在完成后立即退出(如预期)。有了并行循环,根据我正在使用的 Stopwatch 计时器,这减少到平均 ~44 秒,但是应用程序直到大约 2 分钟后才会退出 - 毕竟是 ~1min15sec工作已完成。

该应用没有执行任何操作'extra'。它进入 mainmain 调用一个方法从数据库中检索一些记录(1-2 秒),将这些记录中的 1000 条转发给另一个循环遍历它们的方法,每个 posts到 api,然后退出。除了,出于某种原因,在这种情况下它不会立即退出。

我在调用 posting 方法之前在 main 中放置了一个 stopwatch 计时器,并在方法 returns 之后立即记录时间,并且计时器与方法内的计时器对齐,平均约 46 秒。所以延迟发生在 posting 方法 returned 之后但在 main 函数退出之前,但是此时没有定义它要做什么。调试没有显示任何异常。这是与并行循环生成的所有对象相关的取消分配问题吗 'hanging around'?

无论我是 运行 附加的调试器还是在为发布构建时直接执行二进制文件(因此不是分离延迟问题),都会发生这种情况。我看过像这样的其他 SO 问题,但他们的方法没有什么不同。如有任何意见,我们将不胜感激。

posting 函数的代码:

public ProcessingState PostClockingRecordBatchParallel(List<ClockingEvent> batch, int tokenExpiryTolerance)
{
    log.Info($"Attempting to post batch of {batch.Count.ToString()} clocking records to API with an auth token expiry tolerance of {tokenExpiryTolerance} seconds");
    try
    {
        ProcessingState state = new ProcessingState() { PendingRecords = batch };
        List<ClockingEvent> successfulRecords = new List<ClockingEvent>();
        Stopwatch timer = new Stopwatch();

        ServicePointManager.UseNagleAlgorithm = false; //Performance optimization related to RestSharp lib
        authToken = Authenticate();

        timer.Start();
        Parallel.ForEach(state.PendingRecords, pr =>
        {
             successfulRecords.Add(PostClockingRecord(pr, tokenExpiryTolerance));
        });
        //Prior non-parallel version
        //state.PendingRecords.ForEach(pr => 
        //{
        //    successfulRecords.Add(PostClockingRecord(pr, tokenExpiryTolerance));
        //});


        state.PendingRecords        = state.PendingRecords.Except(successfulRecords).ToList();
        state.LastSuccessfulRecord  = successfulRecords.OrderBy(r => r.EventID).Last().EventID;

         log.Info($"PostClockingRecordBatchParallel - Time elapsed: {new TimeSpan(timer.ElapsedTicks).ToString()}");
         return state;
    }
    catch (Exception ex)
    {
            log.Fatal($"Failed to post records to API (exception encountered: {ex}).");
         throw;
    }
}

是的,它会释放内存。您的线程将耗尽内存,您可以使用 ParallelOptions.MaxDegreeOfParallelism Property 来限制它,这当然会减慢查询速度,并且您需要管理内存释放 - 如果您想减少退出所需的时间应用程序。

您可以 dispose of your tasks, if scalability is an issue and you use up too much memory, or wish to clean up resources as you go. As the Parallel class 扩展任务 class。

不过,调用垃圾收集器对你来说可能是一个更简单的设计。

How can I free-up memory used by a Parallel.Task?

要减少运行末尾的垃圾回收,可以实现自己的垃圾回收,如图this answer

Action allCollect = () =>
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        };

在那里你可以定期手动调用垃圾收集。

也有帮助:
Possible memoryleak in ConcurrentBag?

This answer 举例说明如何使用 MaxDegreeOfParallelism

ParallelOptions.MaximumDegreeOfParallelism = 1: use one full CPU (which will be a percentage of your OS CPU)

如果您希望扩展应用程序以避免内存泄漏和 OutOfMemoryException.

,管理它很重要