Reputation: 273
I have a net framework server. Part of this server's boot process is to set up a bunch of static variables. At the end of the server's life we cancel tasks, dispose of various disposables, and generally tear things down. One aspect of my code was taking a very long time to start up so I changed it to a lazy implementation + a background task to 'touch' all the lazy things so that N minutes after startup they'd all be ready.
public class MyClass
{
private ConcurrentDictionary<string, Lazy<Thing>> dictionary = new ConcurrentDictionary<string, Lazy<Thing>>();
private CancellationTokenSource cts = new CancellationTokenSource();
private Task bootingTask;
public void Boot()
{
// Add to the dictionary of all the lazy things
// For example: dictionary.TryAdd(thingKey, new Lazy<Thing>(CreateThing));
// Note: Replace CreateThing with your actual method to create a Thing object
bootingTask = Task.Factory.StartNew(() =>
{
foreach(var item in dictionary.Values)
{
// (lazy initter)
var _ = item.Value;
}
}, cts.Token, TaskCreationOptions.LongRunning)
.ContinueWith(t =>
{
// Log how long the task took
// For example: Console.WriteLine($"Task took: {t.Result} ms");
});
}
public void Shutdown()
{
// Cancel from the Task Cancellation source
cts.Cancel();
// Checks if the bootingTask is not null and waits for it
if(bootingTask != null)
{
bootingTask.Wait();
}
// Dispose of the task cancellation source and the task
cts.Dispose();
bootingTask.Dispose();
dictionary.Values.foreach(disposeThing);
// Other stuff
// ... We do reach here
}
}
The issue I'm experiencing is that after this change my server process is not ending, which is bad because all processes must end before my application will restart itself with NSSM.
Some things to know:
My ContinueWith
logic is being hit, I am seeing a time being logged to the Console.
I am not seeing any logging or exceptions being thrown in the booting task, I thought I would see these as I .wait()
if any existed.
I am arriving at the .wait()
logic and continuing past it - I am not getting blocked forever.
I'm using Task.Factory.StartNew
because I didn't want to just chuck this multi-minute task onto the thread pool without the LongRunning
task option. I think perhaps I haven't configured it properly. I'd be happy to split this up into dictionary.Count number of tasks.
At runtime everything seems to work, I can use all members of my dictionary of things. It doesn't seem like there's any exceptions thrown when accessing any of the things.
What could be causing my process to never get killed?
Upvotes: 0
Views: 32
Reputation: 43845
You are probably beaten by the common misunderstanding regarding the cancellationToken
parameter in the Task.Factory.StartNew
method. This token cancels the Task
only if the action
has not been invoked yet (by the TaskScheduler
). After that, is has practically no effect (it might only affect the final status of the task, IsCanceled
or IsFaulted
).
What you have to do is to inspect the token inside the action
. Ideally you would pass the token to the Lazy<T>
as well, for more responsive cancellation of the valueFactory
delegate:
bootingTask = Task.Factory.StartNew(() =>
{
foreach(var item in dictionary.Values)
{
// (lazy initter)
var _ = item.Value;
cts.Token.ThrowIfCancellationRequested();
}
}, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default)
As a side note, you are advised to specify explicitly the scheduler
argument of the Task.Factory.StartNew
, as shown above, to comply with the guideline CA2008.
Upvotes: 0