0

Problem The problem can arise in multiple common scenarios:

  • You press a button and want to be sure if the button handler logic is only executed once while the button handler is running. As soon as the button handler has finished, the button can be pressed again and will start the button handler logic again. The button handler logic must never be executed concurrently.
  • Imagine you have to send a bitmap image to a very slow display (e.g. a waveshare eink display which has a refresh rate of 2s per full refresh cycle) and you have hardware push buttons which cause the display to show a new bitmap. You can press buttons in a mich faster rate than the display will be able to refresh. For this purpose, I need a locking around the refresh handler of the display. I want to avoid concurrent calls to the display refresh routine if the user presses the pus buttons in faster rate than the display can process the images.

Attempt 1: Using SemaphoreSlim My first attempt looks like follows: I'm using a SemaphoreSlim for which I created an extension method "ExecuteOnceAsync" which has a parameter "action" (which is essentially a task creator factory for the task we only want to run once).

public static class SemaphoreSlimExtensions
{
    /// <summary>
    /// Executes a task within the context of a a SemaphoreSlim.
    /// The task is started only if no <paramref name="action"/> is currently running.
    /// </summary>
    /// <param name="semaphoreSlim">The semaphore instance.</param>
    /// <param name="action">The function to execute as a task.</param>
    public static async Task ExecuteOnceAsync(this SemaphoreSlim semaphoreSlim, Func<Task> action)
    {
        if (semaphoreSlim.CurrentCount == 0)
        {
            return;
        }

        try
        {
            await semaphoreSlim.WaitAsync();
            await action();
        }
        finally
        {
            try
            {
                semaphoreSlim.Release();
            }
            catch (SemaphoreFullException)
            {
                // Ignored
            }
        }
    }
}

Following unit test should show a sample usage where a call to a task with a shared resource is done 100 times in parallel but we want to make sure it is only executed once:

[Fact]
public async Task ShouldExecuteOnceAsync()
{
    // Arrange
    var counter = 0;
    var parallelTasks = 100;
    var semaphoreSlim = new SemaphoreSlim(1, 1);
    Func<Task> action = () => Task.Run(() =>
    {
        counter++;
        this.testOutputHelper.WriteLine($"Run: counter={counter}");
        return counter;
    });

    // Act
    var tasks = Enumerable.Range(1, parallelTasks).Select(i => semaphoreSlim.ExecuteOnceAsync(action));
    await Task.WhenAll(tasks);

    // Assert
    counter.Should().Be(1);
}

This solution is not thread-safe since CurrentCount is not implemented thread-safe. So, forget about this attempt!

Attempt 2: Using AsyncLazy AsyncLazy allows to run initialization code once and in a thread-safe manner.

The problem with AsyncLazy is that it will only run once per instance of AsyncLazy. My button clicks will occur multiple times. If one handler finishes, another button handler must be run. They must just not run simultaneously.

Attempt 3: Using Interlocked.CompareExchange With the help of other Stackoverflow questions and some github searches, I was able to put following code together. This code atomically sets a flag (currentState) to either 0 (NotRunning) or 1 (Running). CompareExchange checks and updates the currentState. The finally section ensures that the flag is reset from Running to NotRunning after the task is finished.

internal class SyncHelper
{
    private const int NotRunning = 0;
    private const int Running = 1;
    private int currentState;

    public async Task RunOnceAsync(Func<Task> task)
    {
        if (Interlocked.CompareExchange(ref this.currentState, Running, NotRunning) == NotRunning)
        {
            // The given task is only executed if we pass this atomic CompareExchange call,
            // which switches the current state flag from 'not running' to 'running'.

            var id = $"{Guid.NewGuid():N}".Substring(0, 5).ToUpperInvariant();
            Debug.WriteLine($"RunOnceAsync: Task {id} started");

            try
            {
                await task();
            }
            finally
            {
                Debug.WriteLine($"RunOnceAsync: Task {id} finished");
                Interlocked.Exchange(ref this.currentState, NotRunning);
            }
        }

        // All other method calls which can't make it into the critical section
        // are just returned immediately.
    }
}
thomasgalliker
  • 1,279
  • 13
  • 19
  • 1
    This is already provided by either `ActionBlock` or a Channel that's processed by a single task. – Panagiotis Kanavos Sep 28 '22 at 10:28
  • @PanagiotisKanavos, I read the documentation for ActionBlock and tried some examples. It either doesn't solve the described problem or I simply didn't understand how to use ActionBlock to solve the problem. var actionBlock = new ActionBlock(async url => { await Task.Delay(1000); this.testOutputHelper.WriteLine($"url={url}"); }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1 }); var result1 = actionBlock.Post("http://website.com/path/to/images"); var result2 = actionBlock.Post("http://another-website.com/path/to/images"); – thomasgalliker Sep 28 '22 at 18:09
  • An ActionBlock allows you to process items in order. That's the first step in solving your problem. A Semaphore doesn't help at all with that. Besides `only-once` isn't a *threading* problem, it's a *message* problem. You you want to ensure a message is processed only once you need to store somewhere that this message was processed already. A `ConcurrentDictionary` helps in this case, storing the ID or the entire message you process each time. Of course, if you use an ActionBlock with DOP=1 even a `Dictionary` would do – Panagiotis Kanavos Sep 29 '22 at 08:50
  • If you wanted to debounce button clicks, Reactive Extensions' `Throttle` method could work. A bounded Channel configured to *drop* new messages when full could help too. – Panagiotis Kanavos Sep 29 '22 at 08:56
  • Another way is to just disable the button while processing the first click. You'd have to do that anyway, to give a visual cue to the user. – Panagiotis Kanavos Sep 29 '22 at 08:57
  • "*If one handler finishes, another button handler must be run.*" -- This contradicts the title of the question. You don't want to run the async method only once. You want to run it multiple times, just not concurrently. It's unclear what is the desirable behavior in case the async operation is currently running. My guess is that you want the behavior described in [this question](https://stackoverflow.com/questions/73438513) that I've voted as duplicate. If that's not what you want, you could edit the question and describe exactly the desirable behavior. – Theodor Zoulias Oct 07 '22 at 08:45
  • I adjusted the title as well as the description this morning when I saw the duplicate message. The mentioned article describes a different problem (run once only). This was pretty clear, if you read the description and not only the title. I was asking for opinions on how to solve the described problem. Some people thankfully contributed with constructive answers and hints. Obviously the question was clear to them. – thomasgalliker Oct 07 '22 at 14:09
  • I have changed the question that I voted as duplicate. Originally (after reading your question superficially) it was [this](https://stackoverflow.com/questions/28340177/enforce-an-async-method-to-be-called-once), and later (after reading it more carefully) I changed it to [this](https://stackoverflow.com/questions/73438513/how-to-make-multiple-concurrent-calls-to-async-function-collapse-into-1-task). If neither of them is actually a duplicate, please tell me to cast a reopen vote. – Theodor Zoulias Oct 07 '22 at 14:38
  • Also please consider expanding the first part of the question where you are describing the problem. Could you include an example of the async event handler that has the undesirable behavior of running in overlapping manner? Please explain what should happen if the user clicks the button while the handler is still running from a previous click. What is the desirable user experience? What is the feedback that the user should get? Please try your best to describe the problem, and if you want to share your solution then post it as a [self-answer](https://stackoverflow.com/help/self-answer). – Theodor Zoulias Oct 07 '22 at 14:46
  • I added the second scenario where I use the proposed code. It should be very clear now. Let me know if its not. You can renove the duplicate flag since the mentioned articles are describing a different problem. – thomasgalliker Oct 07 '22 at 19:04
  • ​The first problem in the question, the one with the button that can be pressed again when the handler completes, can be solved simply be disabling the button when the handler starts, and enabling the button again when the handler ends. The Attempt 3 in the question shows a method `RunOnceAsync` that returns a `Task`. What does this `Task` represent? If it doesn't represent anything, why is the return type `Task` and not `void`? – Theodor Zoulias Oct 07 '22 at 20:37
  • So you would not use CompareExchange or any other locking? Simply check if(!IsRunning) { IsRunning = true; try{ await task();}finally{ IsRunning = false; }} – thomasgalliker Oct 08 '22 at 08:06
  • Disabling the button is even simpler: `button.Enabled = false; try { await MyAsyncMethod(); } finally { button.Enabled = true; }`. – Theodor Zoulias Oct 08 '22 at 14:08
  • We used exactly such code and we experienced multiple button handlers running simultaneously. That's why I was looking for a thread-safe way to handle the situation. – thomasgalliker Oct 08 '22 at 14:55
  • I wonder how is it possible to run the handler again concurrently, if you disable the button. Do you think that the `button.Enabled = false;` command has not instantaneous effect? If that's the case, I would consider it a bug that needs to be reported to Microsoft. – Theodor Zoulias Oct 08 '22 at 18:22
  • Microsoft would not care much I guess since these visual disable effects are all 'best effort' and not designed for thread safety. And I tried to solve this problem generically - not only for this UI case. – thomasgalliker Oct 10 '22 at 06:22

1 Answers1

0

The code above has a race condition, between checking the CurrentCount of the semaphore and calling WaitAsync.

A better way would be to use semaphoreSlim.WaitAsync(0):

public static async Task ExecuteOnceAsync(this SemaphoreSlim semaphoreSlim, 
  Func<Task> action)
{
    try
    {
        if (! await semaphoreSlim.WaitAsync(0)) return;
        await action();
    }
    finally
    {
        try
        {
            semaphoreSlim.Release();
        }
        catch (SemaphoreFullException)
        {
            // Ignored
        }
    }
}

Your test may not help. There is no reliable way to test concurrent code, because anything can happen, and therefore the test is not idempotent.

Nick
  • 4,787
  • 2
  • 18
  • 24
  • The documentation of WaitAsync(0) says: "The number of milliseconds to wait (...), or zero to test the state of the wait handle and return immediately." Whenever I run this code, be it in a unit test or in a real-world scenario, WaitAsync(0) immediately returns true and runs the action(). Am I doing something wrong? Did you run this code? – thomasgalliker Sep 28 '22 at 17:52
  • Yes, I did. My code is correct. Your test is not, however, just saw the problem with it. If you add `counter--` before `return counter`, you will see that all the output is 1, and the final value of counter is 0. – Nick Sep 29 '22 at 07:19