My application has several async methods that can access files on disk. As I learned the ordinary ReaderWriterLockSlim
cannot be used in such case, I went looking for an equivalent. Found the ConcurrentExclusiveSchedulerPair
that looked very promising. I read a few enlightening blog posts about it and I started to think this could be the right choice for me.
So I changed all my reading tasks to use the Concurrent scheduler and writing tasks to use the Exclusive one. However, it turned out I was still getting IOException
s saying the file is in use. Here's a simplified (and still failing) version of my code:
public async Task<IEnumerable<string>> Run()
{
var schedulers = new ConcurrentExclusiveSchedulerPair();
var exclusiveFactory = new TaskFactory(schedulers.ExclusiveScheduler);
var concurrentFactory = new TaskFactory(schedulers.ConcurrentScheduler);
var tasks = new List<Task>();
for (var i = 0; i < 40; ++i)
{
// Create some readers and (less) writers
if (i % 4 == 0)
{
var writeTask = exclusiveFactory.StartNew(WriteToFile).Unwrap();
tasks.Add(writeTask);
}
else
{
var readTask = concurrentFactory.StartNew(ReadFromFile).Unwrap();
tasks.Add(readTask);
}
}
await Task.WhenAll(tasks);
return _contents;
}
private async Task ReadFromFile()
{
using (var fileStream = new FileStream("file.txt", FileMode.OpenOrCreate, FileAccess.Read))
using (var reader = new StreamReader(fileStream))
{
await Task.Delay(500); // some other work
_contents.Add(await reader.ReadToEndAsync());
}
}
private async Task WriteToFile()
{
using (var fileStream = new FileStream("file.txt", FileMode.OpenOrCreate, FileAccess.Write))
using (var writer = new StreamWriter(fileStream))
{
await Task.Delay(500); // some other work
await writer.WriteLineAsync("Lorem ipsum");
}
}
Then I found Stephen Cleary's blog post with a warning in a red box:
When an asynchronous method awaits, it returns back to its context. This means that ExclusiveScheduler is perfectly happy to run one task at a time, not one task until it completes. As soon as an asynchronous method awaits, it’s no longer the “owner” of the ExclusiveScheduler. Stephen Toub’s async-friendly primitives like AsyncLock use a different strategy, allowing an asynchronous method to hold the lock while it awaits.
"One task at a time, not one task until it completes" - now this is a bit confusing. Does this mean the ConcurrentExclusiveSchedulerPair
is not the right choice for this situation or am I using it incorrectly? Perhaps an AsyncLock
should be used here instead (why isn't it a part of the framework)? Or would a plain old Semaphore
be enough (I understand I wouldn't get the reader-writer division then, but maybe it's ok)?
One way of thinking of async
methods[1] is that they are split into tasks at each await
point. To use your example:
private async Task WriteToFile()
{
using (var fileStream = new FileStream("file.txt", FileMode.OpenOrCreate, FileAccess.Write))
using (var writer = new StreamWriter(fileStream))
{
await Task.Delay(500); // some other work
await writer.WriteLineAsync("Lorem ipsum");
}
}
conceptually gets broken into three tasks:
fileStream
and writer
, and starts the Task.Delay
(some other work).Task.Delay
and starts WriteLineAsync
.WriteLineAsync
and disposes writer
and fileStream
.ConcurrentExclusiveSchedulerPair
is a task scheduler, so its semantics only apply when there is a task running code. When WriteToFile
is run with ExclusiveScheduler
, it holds the exclusive scheduler lock while task (1) is running. Once the Task.Delay
code has started, that task (1) is done, and it releases the exclusive scheduler lock. During the 500
millisecond delay, the exclusive scheduler lock is not held. Once that delay has completed, task (2) is ready and queued to the ExclusiveScheduler
. It then takes the exclusive scheduler lock and does its (small) amount of work. When task (2) completes, it also releases the exclusive scheduler lock. Etc.
Task schedulers were designed to work with synchronous tasks. There is some support for them in await
(i.e., TaskScheduler.Current
is automatically captured and used to resume from an await
), but in general they do not have expected semantics when working with asynchronous tasks. Each await
is actually telling the task scheduler "this (sub)task is done".
Does this mean the ConcurrentExclusiveSchedulerPair is not the right choice for this situation?
Yes. ConcurrentExclusiveSchedulerPair
is not the right choice.
Perhaps an AsyncLock should be used here instead (why isn't it a part of the framework)?
SemaphoreSlim
supports asynchronous locking (with a more awkward syntax). But it may not on WP81 - I don't remember (WaitAsync
was added later). To support a platform that old, you'd have to either use AsyncEx v4 or copy/paste Stephen Toub's code.
I understand I wouldn't get the reader-writer division then, but maybe it's ok?
It's not only OK, it's almost certainly preferable. Using a reader/writer lock when you really just need a lightweight lock is an extremely common mistake. In particular, just because some code has read semantics and other code has write semantics is not a good enough reason to use a RWL.
[1] For efficiency reasons, async
methods are broken into chunks of code at await
points, but those individual chunks of code aren't actually wrapped by Task
objects unless a TaskScheduler
is present.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With