Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allow async method to be called only one instance at a time

I have a method which cannot be executed in multiple threads simultaneously (it writes to a file). I cannot use lock, because the method is async. How to avoid calling the method in another thread? Instead of calling it a second time, the program should wait for the previous call to complete (also asynchronously).

For example, create a new C# console application with the following code:

using System;
using System.IO;
using System.Threading.Tasks;
namespace ConsoleApplication1 {
    internal class Program {

        private static void Main () {
            CallSlowStuff();
            CallSlowStuff();
            Console.ReadKey();
        }

        private static async void CallSlowStuff () {
            try {
                await DoSlowStuff();
                Console.WriteLine("Done!");
            }
            catch (Exception e) {
                Console.WriteLine(e.Message);
            }
        }

        private static async Task DoSlowStuff () {
            using (File.Open("_", FileMode.Create, FileAccess.Write, FileShare.None)) {
                for (int i = 0; i < 10; i++) {
                    await Task.Factory.StartNew(Console.WriteLine, i);
                    await Task.Delay(100);
                }
            }
        }
    }
}

In this example, the second call to CallSlowStuff throws an exception, because it cannot access a file which is already opened. Adding lock is not an option, because lock and async don't mix. (Main method should be considered unchangable. In a real application, CallSlowStuff is an interface method which can be called anywhere.)

Question: How to make subsequent calls to CallSlowStuff wait for the currently running call to complete, without blocking the main thread? Can it be done using just async/await and tasks (and Rx maybe)?

like image 459
Athari Avatar asked Jan 21 '13 00:01

Athari


2 Answers

You need some sort of async lock. Stephen Toub has a whole series of articles about building async synchronization primitives (including AsyncLock). A version of AsyncLock is also contained in Stephen Cleary's AsyncEx library.

But probably a simpler solution would be to use the built-in SemaphoreSlim, which does support asynchronous waiting:

private static SemaphoreSlim SlowStuffSemaphore = new SemaphoreSlim(1, 1);

private static async void CallSlowStuff () {
    await SlowStuffSemaphore.WaitAsync();
    try {
        await DoSlowStuff();
        Console.WriteLine("Done!");
    }
    catch (Exception e) {
        Console.WriteLine(e.Message);
    }
    finally {
        SlowStuffSemaphore.Release();
    }
}
like image 184
svick Avatar answered Oct 21 '22 21:10

svick


I would consider altering the method body of CallSlowStuff to post messages to a TPL DataFlow ActionBlock, and configuring it to a single degree of parallelism:

So keep a single ActionBlock somewhere:

ActionBlock actionBlock = 
    new ActionBlock<object>(
         (Func<object,Task>)CallActualSlowStuff,
         new ExecutionDataflowBlockOptions(){MaxDegreeOfParallelism=1});

Now:

public void CallSlowStuff()
{
    actionBlock.Post(null);
}

private async Task CallActualSlowStuff(object _)
{
    using (File.Open("_", FileMode.Create, FileAccess.Write, FileShare.None)) {
        for (int i = 0; i < 10; i++) {
            await Task.Factory.StartNew(Console.WriteLine, i);
            await Task.Delay(100);
        }
    }
}
like image 1
spender Avatar answered Oct 21 '22 21:10

spender