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);