Why the SemaphoreSlim.WaitAsync is not working? It jumps to "return currentToken.AccessToken" before GetAccesTokenAsync call it's finished

Viewed 2737

SemaphoreSlim.WaitAsync is not working. It jumps to return currentToken.AccessToken before the GetAccesTokenAsync call it's finished and throws NullException. I tried to use also AsyncLock, AsyncSemaphore and some other methods I read online but it seems like nothing it's working in my case.

public static class HttpClientHelper
{
    #region members
    private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
    private static Token currentToken;
    #endregion

    public static string GetAuthorizeToken(ref HttpClient client, string username, string password)
    {
        GetToken(client, username, password);

        return currentToken.AccessToken;
    }

    private static async void GetToken(HttpClient client, string username, string password)
    {
        await semaphore.WaitAsync();

        try
        {
            if (currentToken == null)
            {
                await GetAccesTokenAsync(client, username, password);
            }
            else if (currentToken.IsExpired)
            {
                await GetAccessTokenByRefreshToken(client);
            }
        }
        finally
        {
            semaphore.Release();
        }
    }

    private static async Task<Token> GetAccesTokenAsync(HttpClient client, string username, string password)
    {
        List<KeyValuePair<string, string>> requestBody = new List<KeyValuePair<string, string>>();
        requestBody.Add(new KeyValuePair<string, string>("Username", username));
        requestBody.Add(new KeyValuePair<string, string>("Password", password));
        requestBody.Add(new KeyValuePair<string, string>("grant_type", "password"));

        try
        {
            using (var urlEncodedContent = new FormUrlEncodedContent(requestBody))
            {
                var httpResponse = await client.PostAsync(new Uri(client.BaseAddress + "/api/authentication/token"), urlEncodedContent);
                currentToken = await httpResponse.Content.ReadAsAsync<Token>(new[] { new JsonMediaTypeFormatter() });
            }

            return currentToken;
        }
        catch (Exception e)
        {
            Logers.Log.Error($"Error while getting the access token {e}");
            return null;
        }
    }
}
1 Answers

The first thing you need to do is to change your private static async void GetToken() method declaration to return a Task: private static async Task GetToken(). If it doesn't return a Task, you won't be able to wait for it to complete. ("async void" is "fire-and-forget" as mentioned by GSerg.)

The most basic way to call an async method from a sync method is to use Task.Run(...).Wait(), as shown below.

Pay attention to the call to Task.Run(waitForSem).Wait(); which is what actually turns the async call into a sync call.

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            Parallel.Invoke(() => timeWaitForSem(1), () => timeWaitForSem(2));
            Console.ReadLine();
        }

        static void timeWaitForSem(int id)
        {
            var sw = Stopwatch.StartNew();
            Console.WriteLine($"Thread {id} is waiting for semaphore.");
            Task.Run(waitForSem).Wait(); // <=== HERE is the important bit.
            Console.WriteLine($"Thread {id} finished waiting for semaphore after {sw.Elapsed}.");
        }

        static async Task waitForSem()
        {
            await _semaphore.WaitAsync().ConfigureAwait(false);
            // Keep hold of the semaphore for a while.
            await Task.Delay(2000).ConfigureAwait(false); 
            _semaphore.Release();
        }

        static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    }
}

The output of this program will be something like:

Thread 1 is waiting for semaphore.
Thread 2 is waiting for semaphore.
Thread 1 finished waiting for semaphore after 00:00:02.0133882.
Thread 2 finished waiting for semaphore after 00:00:04.0316629.

What you must NOT do is to simply put waitForSem().Wait(); rather than Task.Run(waitForSem).Wait();, because you are likely to get a deadlock that way (particularly if it's called from an application with a message pump, such as WinForms).

For more information, see Calling async methods from non-async code

An alternative and slightly more efficient approach is to use JoinableTaskFactory from Microsoft.VisualStudio.Threading.dll. To use that, you'd need to reference Microsoft.VisualStudio.Threading.dll or add it via NuGet.

This has the advantage of not starting a new thread if it does not need to. If you use JoinableTaskFactory, the code would look like this:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            Parallel.Invoke(() => timeWaitForSem(1), () => timeWaitForSem(2));
            Console.ReadLine();
        }

        static void timeWaitForSem(int id)
        {
            var sw = Stopwatch.StartNew();
            Console.WriteLine($"Thread {id} is waiting for semaphore.");

            _jtf.Run(async () => await waitForSem().ConfigureAwait(false));

            Console.WriteLine($"Thread {id} finished waiting for semaphore after {sw.Elapsed}.");
        }

        static async Task waitForSem()
        {
            await _semaphore.WaitAsync().ConfigureAwait(false);
            // Keep hold of the semaphore for a while.
            await Task.Delay(2000).ConfigureAwait(false);
            _semaphore.Release();
        }

        static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
        static readonly JoinableTaskFactory _jtf = new JoinableTaskFactory(new JoinableTaskContext());
    }
}
Related