Synchronous I/O within an async/await-based Windows Service

后端 未结 3 772
梦毁少年i
梦毁少年i 2021-01-04 13:26

Let\'s say I have a Windows Service which is doing some bit of work, then sleeping for a short amount of time, over and over forever (until the service is shut down). So in

相关标签:
3条回答
  • 2021-01-04 13:39

    A thread can only do one thing at a time. While it is working on your DoSomething it can't do anything else.

    In an interview Eric Lippert described async-await in a restaurant metaphor. He suggests to use async-await only for functionality where your thread can do other things instead of waiting for a process to complete, like respond to operator input.

    Alas, your thread is not waiting, it is doing hard work in DoSomething. And as long as DoSomething is not awaiting, your thread will not return from DoSomething to do the next thing.

    So if your thread has something meaningful to do while procedure DoSomething is executing, it's wise to let another thread do the DoSomething, while your original thread is doing the meaningful stuff. Task.Run( () => DoSomething()) could do this for you. As long as the thread that called Task.Run doesn't await for this task, it is free to do other things.

    You also want to cancel your process. DoSomething can't be cancelled. So even if cancellation is requested you'll have to wait until DoSomething is completed.

    Below is your DoSomething in a form with a Start button and a Cancel button. While your thread is DoingSomething, one of the meaningful things your GUI thread may want to do is respond to pressing the cancel button:

    void CancellableDoSomething(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            DoSomething()
        }
    }
    
    async Task DoSomethingAsync(CancellationToken token)
    {
        var task = Task.Run(CancellableDoSomething(token), token);
        // if you have something meaningful to do, do it now, otherwise:
        return Task;
    }
    
    CancellationTokenSource cancellationTokenSource = null;
    
    private async void OnButtonStartSomething_Clicked(object sender, ...)
    {
        if (cancellationTokenSource != null)
            // already doing something
            return
    
        // else: not doing something: start doing something
        cancellationTokenSource = new CancellationtokenSource()
        var task = AwaitDoSomethingAsync(cancellationTokenSource.Token);
        // if you have something meaningful to do, do it now, otherwise:
        await task;
        cancellationTokenSource.Dispose();
        cancellationTokenSource = null;
    }
    
    private void OnButtonCancelSomething(object sender, ...)
    {
        if (cancellationTokenSource == null)
            // not doing something, nothing to cancel
            return;
    
        // else: cancel doing something
        cancellationTokenSource.Cancel();
    }
    
    0 讨论(0)
  • 2021-01-04 13:53

    Actually there might be several such threads, and other threads too, all started in OnStart and shut down/joined in OnStop.

    On a side note, it's usually simpler to have a single "master" thread that will start/join all the others. Then OnStart/OnStop just deals with the master thread.

    If I want to instead do this sort of thing in an async/await based Windows Service, it seems like I could have OnStart create cancelable tasks but not await (or wait) on them, and have OnStop cancel those tasks and then Task.WhenAll().Wait() on them.

    That's a perfectly acceptable approach.

    If I understand correctly, the equivalent of the "WorkerThreadFunc" shown above might be something like:

    Probably want to pass the CancellationToken down; cancellation can be used by synchronous code, too:

    private async Task WorkAsync(CancellationToken cancel)
    {
      while (true)
      {
        DoSomething(cancel);
        await Task.Delay(10, cancel).ConfigureAwait(false);
      }
    }
    

    Question #1: Uh... right? I am new to async/await and still trying to get my head around it.

    It's not wrong, but it only saves you one thread on a Win32 service, which doesn't do much for you.

    Question #2: That is bad? I shouldn't be doing synchronous I/O within a Task in an async/await-based program? Because it ties up a thread from the thread pool while the I/O is happening, and threads from the thread pool are a highly limited resource? Please note that I might have dozens of such Workers going simultaneously to different pieces of hardware.

    Dozens of threads are not a lot. Generally, asynchronous I/O is better because it doesn't use any threads at all, but in this case you're on the desktop, so threads are not a highly limited resource. async is most beneficial on UI apps (where the UI thread is special and needs to be freed), and ASP.NET apps that need to scale (where the thread pool limits scalability).

    Bottom line: calling a blocking method from an asynchronous method is not bad but it's not the best, either. If there is an asynchronous method, call that instead. But if there isn't, then just keep the blocking call and document it in the XML comments for that method (because an asynchronous method blocking is rather surprising behavior).

    I am getting the idea that it's bad from articles like Stephen Cleary's "Task.Run Etiquette Examples: Don't Use Task.Run for the Wrong Thing", but that's specifically about it being bad to do blocking work within Task.Run.

    Yes, that is specifically about using Task.Run to wrap synchronous methods and pretend they're asynchronous. It's a common mistake; all it does is trade one thread pool thread for another.

    Assuming that's bad too, then if I understand correctly I should instead utilize the nonblocking version of DoSomething (creating a nonblocking version of it if it doesn't already exist)

    Asynchronous is better (in terms of resource utilization - that is, fewer threads used), so if you want/need to reduce the number of threads, you should use async.

    Question #3: But... what if DoSomething is from a third party library, which I must use and cannot alter, and that library doesn't expose a nonblocking version of DoSomething? It's just a black box set in stone that at some point does a blocking write to a piece of hardware.

    Then just call it directly.

    Maybe I wrap it and use TaskCompletionSource?

    No, that doesn't do anything useful. That just calls it synchronously and then returns an already-completed task.

    But that seems like it's just pushing the issue down a bit further rather than resolving it. WorkAsync() will still block when it calls WrappedDoSomething(), and only get to the "await" for that after WrappedDoSomething() has already completed the blocking work. Right?

    Yup.

    Given that (if I understand correctly) in the general case async/await should be allowed to "spread" all the way up and down in a program, would this mean that if I need to use such a library, I essentially should not make the program async/await-based? I should go back to the Thread/WorkerThreadFunc/Thread.Sleep world?

    Assuming you already have a blocking Win32 service, it's probably fine to just keep it as it is. If you are writing a new one, personally I would make it async to reduce threads and allow asynchronous APIs, but you don't have to do it either way. I prefer Tasks over Threads in general, since it's much easier to get results from Tasks (including exceptions).

    The "async all the way" rule only goes one way. That is, once you call an async method, then its caller should be async, and its caller should be async, etc. It does not mean that every method called by an async method must be async.

    So, one good reason to have an async Win32 service would be if there's an async-only API you need to consume. That would cause your DoSomething method to become async DoSomethingAsync.

    What if an async/await-based program already exists, doing other things, but now additional functionality that uses such a library needs to be added to it? Does that mean that the async/await-based program should be rewritten as a Thread/etc.-based program?

    No. You can always just block from an async method. With proper documentation so when you are reusing/maintaining this code a year from now, you don't swear at your past self. :)

    0 讨论(0)
  • 2021-01-04 13:54

    If you still spawn your threads, well, yes, it's bad. Because it will not give you any benefit as the thread is still allocated and consuming resources for the specific purpose of running your worker function. Running a few threads to be able to do work in parallel within a service has a minimal impact on your application.

    If DoSomething() is synchronous, you could switch to the Timer class instead. It allows multiple timers to use a smaller amount of threads.

    If it's important that the jobs can complete, you can modify your worker classes like this:

    SemaphoreSlim _shutdownEvent = new SemaphoreSlim(0,1);
    
    public async Task Stop()
    {
        return await _shutdownEvent.WaitAsync();
    }
    
    private void WorkerThreadFunc()
    {
        while (!shuttingDown)
        {
            DoSomething();
            Thread.Sleep(10);
        }
        _shutdownEvent.Release();
    }
    

    .. which means that during shutdown you can do this:

    var tasks = myServices.Select(x=> x.Stop());
    Task.WaitAll(tasks);
    
    0 讨论(0)
提交回复
热议问题