简体   繁体   中英

How Do I Create a Looping Service inside an C# Async/Await application?

I have written a class with a method that runs as a long-running Task in the thread pool. The method is a monitoring service to periodically make a REST request to check on the status of another system. It's just a while() loop with a try()catch() inside so that it can handle its own exceptions and and gracefully continuing if something unexpected happens.

Here's an example:

public void LaunchMonitorThread()
{
    Task.Run(() =>
    {
        while (true)
        {
            try
            {
                //Check system status
                Thread.Sleep(5000);
            }
            catch (Exception e)
            {
                Console.WriteLine("An error occurred. Resuming on next loop...");
            }
        }
    });
}

It works fine, but I want to know if there's another pattern I could use that would allow the Monitor method to run as regular part of a standard Async/Await application, instead of launching it with Task.Run() -- basically I'm trying to avoid fire-and-forget pattern.

So I tried refactoring the code to this:

   public async Task LaunchMonitorThread()
    {

        while (true)
        {
            try
            {
                //Check system status

                //Use task.delay instead of thread.sleep:
                await Task.Delay(5000);
            }
            catch (Exception e)
            {
                Console.WriteLine("An error occurred. Resuming on next loop...");
            }
        }

    }

But when I try to call the method in another async method, I get the fun compiler warning:

"Because this call is not awaited, execution of the current method continues before the call is completed."

Now I think this is correct and what I want. But I have doubts because I'm new to async/await. Is this code going to run the way I expect or is it going to DEADLOCK or do something else fatal?

What you are really looking for is the use of a Timer . Use the one in the System.Threading namespace. There is no need to use Task or any other variation thereof (for the code sample you have shown).

private System.Threading.Timer timer;
void StartTimer()
{
    timer = new System.Threading.Timer(TimerExecution, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
}

void TimerExecution(object state)
{
    try
    {
        //Check system status
    }
    catch (Exception e)
    {
        Console.WriteLine("An error occurred. Resuming on next loop...");
    }
}

From the documentation

Provides a mechanism for executing a method on a thread pool thread at specified intervals


You could also use System.Timers.Timer but you might not need it. For a comparison between the 2 Timers see also System.Timers.Timer vs System.Threading.Timer .

If you need fire-and-forget operation, it is fine. I'd suggest to improve it with CancellationToken

public async Task LaunchMonitorThread(CancellationToken token)
{
    while (!token.IsCancellationRequested)
    {
        try
        {
            //Check system status

            //Use task.delay instead of thread.sleep:
            await Task.Delay(5000, token);
        }
        catch (Exception e)
        {
            Console.WriteLine("An error occurred. Resuming on next loop...");
        }
    }

}

besides that, you can use it like

var cancellationToken = new CancellationToken();
var monitorTask = LaunchMonitorThread(cancellationToken);

and save task and/or cancellationToken to interrupt monitor wherever you want

The method Task.Run that you use to fire is perfect to start long-running async functions from a non-async method.

You are right: the forget part is not correct. If for instance your process is going to close, it would be neater if you kindly asked the started thread to finish its task.

The proper way to do this would be to use a CancellationTokenSource . If you order the CancellationTokenSource to Cancel , then all procedures that were started using Tokens from this CancellationTokenSource will stop neatly within reasonable time.

So let's create a class LongRunningTask , that will create a long running Task upon construction and Cancel this task using the CancellationTokenSource upon Dispose().

As both the CancellationTokenSource as the Task implement IDisposable the neat way would be to Dispose these two when the LongRunningTask object is disposed

class LongRunningTask : IDisposable
{
    public LongRunningTask(Action<CancellationToken> action)
    {   // Starts a Task that will perform the action
        this.cancellationTokenSource = new CancellationTokenSource();
        this.longRunningTask = Task.Run( () => action (this.cancellationTokenSource.Token));
    }

    private readonly CancellationTokenSource cancellationTokenSource;
    private readonly Task longRunningTask;
    private bool isDisposed = false;

    public async Task CancelAsync()
    {   // cancel the task and wait until the task is completed:
        if (this.isDisposed) throw new ObjectDisposedException();

        this.cancellationTokenSource.Cancel();
        await this.longRunningTask;
    }

    // for completeness a non-async version:
    public void Cancel()
    {   // cancel the task and wait until the task is completed:
        if (this.isDisposed) throw new ObjectDisposedException();

        this.cancellationTokenSource.Cancel();
        this.longRunningTask.Wait;
    }
}

Add a standard Dispose Pattern

public void Dispose()
{
     this.Dispose(true);
     GC.SuppressFinalize(this);
}

protected void Dispose(bool disposing)
{
    if (disposing && !this.isDisposed)
    {   // cancel the task, and wait until task completed:
        this.Cancel();
        this.IsDisposed = true;                 
    }
}

Usage:

var longRunningTask = new LongRunningTask( (token) => MyFunction(token)
...
// when application closes:
await longRunningTask.CancelAsync(); // not necessary but the neat way to do
longRunningTask.Dispose();

The Action {...} has a CancellationToken as input parameter, your function should regularly check it

async Task MyFunction(CancellationToken token)
{
     while (!token.IsCancellationrequested)
     {
          // do what you have to do, make sure to regularly (every second?) check the token
          // when calling other tasks: pass the token
          await Task.Delay(TimeSpan.FromSeconds(5), token);
     }
}

Instead of checking for Token, you could call token.ThrowIfCancellationRequested . This will throw an exception that you'll have to catch

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM