Calling async methods from a Windows Service
Asked Answered
S

2

30

I have a Windows Service written in C# that periodically fires off background jobs. Typically, at any given time, several dozen heavily I/O bound Tasks (downloading large files, etc) are running in parallel. The service runs on a relatively busy web server (necessary for now), and I think it could benefit greatly in terms of thread conservation to use asynchronous APIs as much as possible.

Most of this work is done. All jobs are now fully async (leveraging HttpClient, etc.), as is the main job loop (with heavy doses of Task.Delay). All that's left is to figure out how to correctly and safely fire up the main loop from the service's OnStart. Essentialy, it's the much-warned-about calling-async-from-sync dilemma. Below is what I have so far (grossly simplified).

in Program.cs:

static void Main(string[] args) {
    TaskScheduler.UnobservedTaskException += (sender, e) => {
        // log & alert!
        e.SetObserved();
    };
    ServiceBase.Run(new MyService());
}

in MyService.cs:

protected override void OnStart(string[] args) {
    _scheduler.StartLoopAsync(); // fire and forget! will this get me into trouble?
}

It's that call to StartLoopAsync that concerns me. I can't simply Wait() on the returned Task because OnStart needs to return relatively quickly. (Job loops need to run on a separate thread.) A couple thoughts come to mind:

  • Am I well covered as far as unobserved exceptions by placing that handler in Main?
  • Would there be any benefit to using Task.Run, something like Task.Run(() => _scheduler.StartLoopAsync().Wait()); ?
  • Would there be any benefit to calling _scheduler.StartLoopAsync().ConfigureAwait(false) here? (I'm doubting it since there's no await here.)
  • Would there be any benefit to using Stephen Cleary's AsyncContextThread in this situation? I haven't seen any examples of using this, and since I'm starting an infinite loop I don't know that syncing back up to some context is even relevant here.
Solace answered 14/12, 2013 at 16:22 Comment(9)
I'll write up an AsyncContextThread example next week.Hillari
So I stop pestering you about it? ;) No worries, I look forward to it, and thanks again for your help!Solace
It's no problem. I actually just saw your comment on CodePlex from forever ago, which for some reason I was not notified of...Hillari
Hey @StephenCleary, any chance you could direct us to an example of how to use your AsyncContextThread in a Windows Service? It's not clear to me whether it should be called from OnStart() or from Main().Upcast
@N1njaB0b: Yeah, I never did get around to writing that up... it is still on my todo list... You should call it from OnStart and then join to it in OnStop.Hillari
@StephenCleary you haven't gotten around to writing up an example for this yet have you? Especially one where the background processing can be cancelled.Shop
@CamHart: No, I never did. However, these days I'd recommend looking at .NET Core worker services. I've done a .NET core worker service in a Win32 service and it works fairly nicely, including cancellation.Hillari
@StephenCleary thanks for getting back! I need it for .net framework 4.5+ tho (windows 7).Shop
@CamHart: Ah, well, yeah, I never did write that up...Hillari
H
15

UnobservedTaskException will be called for all unobserved Task exceptions, so it's a good place for logging like this. However, it's not great because, depending on your program logic, you may see spurious messages; e.g., if you Task.WhenAny and then ignore the slower task, then a any exceptions from that slower task should be ignored but they do get sent to UnobservedTaskException. As an alternative, consider placing a ContinueWith on your top-level task (the one returned from StartLoopAsync).

Your call to StartLoopAsync looks fine to me, assuming it's properly asynchronous. You could use TaskRun (e.g., Task.Run(() => _scheduler.StartLoopAsync()) - no Wait is necessary), but the only benefit would be if StartLoopAsync itself could raise an exception (as opposed to faulting its returned task) or if it took too long before the first await.

ConfigureAwait(false) is only useful when doing an await, as you surmised.

My AsyncContextThread is designed for this kind of situation, but it was also designed to be very simple. :) AsyncContextThread provides an independent thread with a main loop similar to your scheduler, complete with a TaskScheduler, TaskFactory, and SynchronizationContext. However, it is simple: it only uses a single thread, and all of the scheduling/context points back to that same thread. I like that because it greatly simplifies thread safety concerns while also allowing concurrent asynchronous operations - but it is not making full use of the thread pool so, e.g., CPU-bound work would block the main loop (similar to a UI thread scenario).

In your situation, it sounds like AsyncContextThread may let you remove/simplify some of the code you've already written. But on the other hand, it is not multithreaded like your solution is.

Hillari answered 14/12, 2013 at 16:47 Comment(0)
S
6

Not an answer per se, but a year after posting this question we're moving this service to an Azure Cloud Service. I've found the Azure SDK's Worker Role template to be a very good example of properly calling async from sync, providing cancellation support, dealing with exceptions, etc. It's not quite apples-to-apples with Windows Services in that the latter doesn't provide an equivalent to the Run method (you need to kick off your work in OnStart and return immediately), but for what it's worth, here it is:

public class WorkerRole : RoleEntryPoint
{
    private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    private readonly ManualResetEvent runCompleteEvent = new ManualResetEvent(false);

    public override void Run() {
        Trace.TraceInformation("WorkerRole1 is running");

        try {
            this.RunAsync(this.cancellationTokenSource.Token).Wait();
        }
        finally {
            this.runCompleteEvent.Set();
        }
    }

    public override bool OnStart() {
        // Set the maximum number of concurrent connections
        ServicePointManager.DefaultConnectionLimit = 12;

        // For information on handling configuration changes
        // see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.

        bool result = base.OnStart();

        Trace.TraceInformation("WorkerRole1 has been started");

        return result;
    }

    public override void OnStop() {
        Trace.TraceInformation("WorkerRole1 is stopping");

        this.cancellationTokenSource.Cancel();
        this.runCompleteEvent.WaitOne();

        base.OnStop();

        Trace.TraceInformation("WorkerRole1 has stopped");
    }

    private async Task RunAsync(CancellationToken cancellationToken) {
        // TODO: Replace the following with your own logic.
        while (!cancellationToken.IsCancellationRequested) {
            Trace.TraceInformation("Working");
            await Task.Delay(1000);
        }
    }
}
Solace answered 9/1, 2015 at 17:0 Comment(1)
I think it will be even better if the cancellationToken is passed to the Task.Delay method so that if cancellation is requested it can happen immediately even if the RunAsync method is currently waiting.Collodion

© 2022 - 2024 — McMap. All rights reserved.