问题
I have a console app that queries a database and then posts some records to a REST API in a loop (the api does not support batch posting, so I have to loop through each record and post individually, if its relevant). The database access is fast and no issue and so is the api post loop according to the timer I've put in place, however the app itself takes a long time to exit after the work is done.
This started happening after I introduced Parallel.Foreach
to speed up the posting. Before using a non-parallel loop, posting 1000 records took on average ~10mins, but the app would return and exit immediately when it was done (as expected). With the parallel loop in place, this, according to the Stopwatch
timer I'm using, is reduced to an average of ~44secs, however the app doesn't exit until around 2 minutes have passed - ~1min15sec after all work has completed.
The app isn't doing anything 'extra'. It enters main
, main
calls a method to retrieve some records from a database (1-2 secs), forwards 1000 of those records to another method that loops through them and posts each to the api, then exits. Except, it doesn't exit immediately in this case, for some reason.
I put a stopwatch
timer in main
immediately before the call to the posting method and log the time immediately after the method returns, and the timer aligns with the timer inside the method, averaging ~46 seconds. So the delay is happening after the posting method has returned but before the main
function exits, but there is nothing defined for it to do at this point. Debugging didn't show anything out of the ordinary. Is this a de-allocation issue related to all the objects spawned by the parallel loop that are 'hanging around'?
This happens regardless of whether I am running with a debugger attached or executing the binary directly when built for release (so not a detaching delay issue). I've looked at other SO questions like this but their approaches have not made a difference. Any input would be appreciated.
Code of the posting function:
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;
}
}
回答1:
Yes it would be freeing up the memory. Your thread will be using up memory and you can limit this by using ParallelOptions.MaxDegreeOfParallelism Property, which will then slow down the query, of course, and you need to manage the memory deallocation - if you want to reduce the time taken to exit the application.
You can 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 extends the Task class.
Although, calling the garbage collector may be a more foolproof design for you.
How can I free-up memory used by a Parallel.Task?
To reduce the garbage collection at the end of the run, you can implement your own garbage collection, as shown in this answer
Action allCollect = () =>
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
};
Where you can periodically manually call for garbage collection.
Also helpful:
Possible memoryleak in ConcurrentBag?
This answer gives examples of how to use MaxDegreeOfParallelism
ParallelOptions.MaximumDegreeOfParallelism = 1: use one full CPU (which will be a percentage of your OS CPU)
Managing this is important if you wish to scale your application, so as to avoid memory leaks and OutOfMemoryException.
来源:https://stackoverflow.com/questions/45298554/console-app-takes-a-long-time-to-exit-after-finishing-work