为什么SemaphoreSlim.WaitAsync无法正常工作?在GetAccesTokenAsync调用完成之前,它跳转到“返回currentToken.AccessToken”

问题描述

SemaphoreSlim.WaitAsync不起作用。在return currentToken.Accesstoken调用完成之前,它跳到GetAccesTokenAsync并抛出NullException。我还尝试使用AsyncLock,AsyncSemaphore和其他一些在线阅读的方法,但对于我来说似乎没有任何作用。

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 password)
    {
        await semaphore.WaitAsync();

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

    private static async Task<Token> GetAccesTokenAsync(HttpClient client,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;
        }
    }
}

解决方法

您需要做的第一件事是更改您的private static async void GetToken()方法声明以返回任务:private static async Task GetToken()。如果没有返回任务,您将无法等待它完成。 (如GSerg所述,“异步无效”是“一劳永逸”。)

从同步方法中调用异步方法的最基本方法是使用Task.Run(...).Wait(),如下所示。

请注意对Task.Run(waitForSem).Wait();的调用,实际上是将异步调用转换为同步调用。

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

该程序的输出将类似于:

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.

您绝对不能简单地放置waitForSem().Wait();而不是Task.Run(waitForSem).Wait();,因为这样很可能会导致死锁(特别是如果它是从带有消息泵的应用程序中调用的,例如WinForms)。

有关更多信息,请参见Calling async methods from non-async code

另一种更有效的方法是使用Microsoft.VisualStudio.Threading.dll中的JoinableTaskFactory。要使用它,您需要引用Microsoft.VisualStudio.Threading.dll或通过NuGet添加它。

这样做的好处是不需要启动新线程。如果您使用JoinableTaskFactory,代码将如下所示:

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