Is it ok to use "async" with a ThreadStart method?
Asked Answered
P

1

21

I have a Windows Service that uses Thread and SemaphoreSlim to perform some "work" every 60 seconds.

class Daemon
{
    private SemaphoreSlim _semaphore;
    private Thread _thread;

    public void Stop()
    {
        _semaphore.Release();
        _thread.Join();
    }

    public void Start()
    {
        _semaphore = new SemaphoreSlim(0);
        _thread = new Thread(DoWork);
        _thread.Start();
    }

    private void DoWork()
    {
        while (true)
        {
            // Do some work here

            // Wait for 60 seconds, or exit if the Semaphore is released
            if (_semaphore.Wait(60 * 1000))                
            {
                return;
            }
        }
    }
}

I'd like to call an asynchronous method from DoWork. In order to use the await keyword I must add async to DoWork:

private async void DoWork()
  1. Is there any reason not to do this?
  2. Is DoWork actually able to run asynchronously, if it's already running inside a dedicated thread?
Pantry answered 5/6, 2017 at 7:44 Comment(8)
It might make more sense to make DoWork async and not start new thread for it, but that depends on what exactly DoWork is doing and why you need to make it async.Carbonate
async anonymous methods in Task.Run() is a common enough pattern, and it's functionally equivalent to what you're asking. Your question is primarily opinion based, and yes there are reasons not to do it, but probably not any really good ones. Do note that, just as in the Task.Run() scenario, your thread will exit as soon as the first await is reached.Lynea
See possible duplicate #34808535 for discussion regarding these types of scenarios and the wisdom or lack thereof. Your question lacks detail, making it also broad, but if it were me, I'd refactor both the thread and the semaphore to be TPL-based. There's nothing in the bit of info you posted that suggests a dedicated thread or semaphore is warranted anyway.Lynea
Part of the problem, at least in my understanding, is the weird way that services run. You are usually performing some sort of sleep/work/sleep/.. cycle, but also need to be able to respond to a "Stop" command, to allow the service to exit. If anyone has any links to a TPL style service, please post them!Pantry
In the context of a service, "stop" translates to CancellationTokenSource, which works very well with the TPL API.Lynea
To expand on what I'm trying to find out here... If I add async to DoWork() the code still works. But I don't know async/await well enough to understand what's actually happening with that dedicated thread. The use of await means that the method exits, and will continue later, correct? But if that's happening, what prevents the Thread from exiting?Pantry
Just found this, which gives a nice explanation: #27384496Pantry
@PeterDuniho the first not synchronously completed await; pedantic point, perhaps, but ... sometimes it mattersGoeger
G
18

You can do this, but it wouldn't be a good idea. As soon as the first await hits that is not synchronously completed, the rest of the work will be done on a continuation, not the thread you started (_thread); the thread you start will terminate at the first such await. There is no guarantee that a continuation will go back to the originating thread, and in your scenario, it cannot - that thread is now toast. That means that:

  1. _thread is meaningless and does not represent the state of the operation; as such, your Stop() method with a _thread.Join(); doesn't do what you expect it to do
  2. you've created a thread (allocating threads is expensive, in particular because of the size of the stack) only to have it exit almost immediately

Both of these issues can avoided by using Task.Run to start such operations.

Goeger answered 5/6, 2017 at 8:48 Comment(6)
Thank you. I think I will reimplement the service using the TPL pattern I found here: #27384496Pantry
Not that it would be a good idea for the OP, but is there a best practice for starting a thread using an async method? Neither the Thread constructor, nor Task.Factory.StartNew have an overload that accepts a Func<Task> and Task.Run creates a thread-pool thread. Should I just do void Foo() => FooAsync().Wait(); async Task FooAsync() { ... } and use Foo as the ThreadStart?Idolatrize
The downside of the Task.Run is it immediately starts running and the it might be needed to start running threads at a later time.Primogeniture
@Primogeniture you can always just create the delegate now, and call Task.Run later...Goeger
"the thread you start will terminate at the first such await" Where can I read more about this?Laughton
@Laughton that's simply a direct consequence of how a) manual threads, and b) async operations - work; a thread started via Thread (rather than a pool thread) exists to invoke the delegate passed to it, and then terminate; an async void method only run synchronously as far as the first incomplete async await (this is also true of async Task etc, but with async Task, the incomplete state is at least visible to the caller). So: the moment an async void method hits an incomplete await: the thread you spawned for it: unrolls and dies. When the await completes: it'll happen elsewhere.Goeger

© 2022 - 2024 — McMap. All rights reserved.