-2

If I start the following example in .NET Core BackgroundService on debug mode:

protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        Task.Run(async () => await Task.Delay(30000, stoppingToken))
            .Wait(stoppingToken);
    }
}

the Ctrl + C cancellation event does not call the StopAsync() Method which is calling the Cancel() from the CancellationTokenSource.

I think my problem is similar to this post.

How can I catch those cancellations when I'm using blocking methods inside the ExecuteAsync?

p.s.: In the real world my ExecuteAsync is watching the filesystem until a new file is created in my destination. To achieve this behavior I'm using the FileSystemWatcher.WaitForChanged() method.

  • 1
    `when I'm using blocking methods inside the ExecuteAsync` don't use blocking methods. Definitely don't use `Thread.Sleep` for any reason. That just wastes a threadpool thread. Use `await Task.Delay(stoppingToken)` instead – Panagiotis Kanavos Feb 18 '21 at 15:46
  • 2
    Do not mix threads and tasks – Pavel Anikhouski Feb 18 '21 at 15:46
  • 1
    Why did you use that line anyway? You start a task that gets blocked immediatelly, then you block the original task with `Wait()`. Was that an attempt to "cancel" the `Thread.Sleep` operation? – Panagiotis Kanavos Feb 18 '21 at 15:48
  • You **also** shouldn't use `Task.Factory.StartNew`, use `Task.Run()`. – Liam Feb 18 '21 at 16:16
  • @PanagiotisKanavos In the real world I'm trying to use the FileSystemWatcher's `WaitForChanged` method to "fall asleep" until a change is incoming in the filesystem. @PavelAnikhouski & @Liam Thanks for your advices. This is just an example code. I'm going to update the post. – Christopher Feb 19 '21 at 07:49
  • @Christopher put that in your question then. You don't need a thread with FSW either, you only need to handle events as fast as possible, preferably by quickly posting them to a queue. You can use [CancellationToken.Register](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.register?view=net-5.0) to stop the FSW when the cancellation token is triggered – Panagiotis Kanavos Feb 19 '21 at 07:52
  • @Christopher please edit the question and add the real code. This has little to do with threads, `Thread.Sleep` or `Task.Delay`. Post the FSW processing code – Panagiotis Kanavos Feb 19 '21 at 08:38
  • @PanagiotisKanavos My FSW blocking method has nothing to do with the cancellation of a blocking method. You can use any method in there which is blocking. Maybe `Task.Delay()` is a bad example? – Christopher Feb 19 '21 at 09:17
  • @Christopher your question is about FWS, not `Task.Delay`, so any example that doesn't include that class isn't helping. You don't need to block calling `WaitForChanged`, you can use events. Once you do that, you don't need `Task.Run` or `Task.Delay`. – Panagiotis Kanavos Feb 19 '21 at 09:23

2 Answers2

1

From the comments, it looks like the problem has little to do with threads. The real problem is how to stop a FileSystemWatcher.

You don't need an extra thread with a FileSystemWatcher, you need to handle its change events as quickly as possible. You can use an asynchronous event handler for this, or even better, quickly post events to a queue or Channel for processing.

To stop the FSW you can use the CancellationToken.Register method to set EnableRaisingEvents to false :

stoppingToken.Register(()=>watcher.EnableRaisingEvents=false);

Event processing

To quickly handle events, one could post the FileSystemEventArgs values directly to a queue or a Channel and process them with another tasks. This has two benefits:

  • File events are handled as fast as possible, so none is lost
  • The code can either wait for all events to finish, or cancel them.
var channel=Channel.CreateUnbounded<FileSystemEventArgs>();

stoppingToken.Register(()=>{
    watcher.EnableRaisingEvents=false;
    channel.Writer.TryComplete();
});

watcher.Changed+=(o,e)=>channel.Writer.WriteAsync(e,stoppingToken);

await foreach(var e in channel.Reader.ReadAllAsync(stoppingToken))
{
    //do something
}

A Channel can be treated as a queue with asynchronous read and write operation. The ReadAllAsync method dequeues messages until stopped and returns them as an IAsyncEnumerable which allows the use of await foreach to easily handle items asynchronously.

Pipelines and Channels

The code can be refactored into this:

await watcher.AsChannel(stoppingToken)
             .ProcessEvents(stoppingToken);

The consumer

It's easy to extract the subscriber code into a separate method. This could even be an extension method:

public static async Task ProcessEvents(this ChannelReader<FileSystemEventArgs> reader,CancellationToken stoppingToken)
{
    await foreach(var e in channel.Reader.ReadAllAsync(stoppingToken))
    {
        //do something
    }
}

And call it :

var channel=Channel.CreateUnbounded<FileSystemEventArgs>();

stoppingToken.Register(()=>{
    watcher.EnableRaisingEvents=false;
    channel.Writer.TryComplete();
});

watcher.Changed+=(o,e)=>channel.Writer.WriteAsync(e,stoppingToken);

await ProcessEvents(channel,stoppingToken);

This works because a Channel has implicit cast operators to ChannelReader and ChannelWriter.

A ChannelReader supports multiple consumers, so one could use multiple tasks to process events, eg :

public static async Task ProcessEvents(this ChannelReader<FileSystemEventArgs> reader,int dop,CancellationToken stoppingToken)
{
    var tasks=Enumerable.Range(0,dop).Select(()=>{
        await foreach(var e in channel.Reader.ReadAllAsync(stoppingToken))
        {
            //do something
        }
    });
    await Task.WhenAll(tasks);
}

The producer

It's also possible to extract the channel creation and posting into a separate method. After all, we only need the ChannelReader for processing:

public static ChannelReader AsChannel(this FileSystemWatcher watcher, CancellationToken stoppingToken)
{
    var channel=Channel.CreateUnbounded<FileSystemEventArgs>();

    stoppingToken.Register(()=>{
        watcher.EnableRaisingEvents=false;
        channel.Writer.TryComplete();
    });

    watcher.Changed+=(o,e)=>channel.Writer.WriteAsync(e,stoppingToken);
    return channel;
}

And combine everything in a simple pipeline:

await watcher.AsChannel(stoppingToken)
             .ProcessEvents(stoppingToken);
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Thanks, that's something I can work with. I have written a library method with FSW `WaitForChanged` method. This method is blocking until a change is registered. I've tested to cancel my method with a CancellationToken and this is working fine. – Christopher Feb 19 '21 at 08:24
  • @Christopher I just posted how to handle the events without having to block, using channels – Panagiotis Kanavos Feb 19 '21 at 08:29
0

My first workaround at the moment is this:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        var blockingTask = Task.Run(async () => await Task.Delay(30000, stoppingToken));
        await Task.WhenAny(blockingTask);
    }
}

@Panagiotis Kanavos I appreciate your efforts, I'm coming back to your detailed post If I'm trying to change my "blocking" FSW to an event driven FSW.

In productive I'm using something like this:

private void DoServiceWork() 
{ 
    // Some Work if new PDF or docx file is available 
    // ...
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    int myTimeout = 1000 * 60 * 60; // 1 hour

    while (!stoppingToken.IsCancellationRequested)
    {
        pdfWatchingTask = Task.Run(() => MyFSWLibrary.Watch(directory, "*.pdf", myTimeout, stoppingToken));
        docWatchingTask = Task.Run(() => MyFSWLibrary.Watch(directory, "*.docx", myTimeout, stoppingToken));

        var finishedTask = await Task.WhenAny(new Task<MyFSWResult>[] { waitPdfTask, waitXmpTask });
        if(finishedTask.Result.Success) DoServiceWork();
    }
}