为什么 SemaphoreSlim.WaitAsync 不起作用?在 GetAccesTokenAsync 调用完成之前它会跳转到“return currentToken.AccessToken”

Cat*_*een 5 c# synchronization semaphore async-await

SemaphoreSlim.WaitAsync不管用。它跳转到调用完成return currentToken.AccessToken之前并抛出 NullException。GetAccesTokenAsync我也尝试使用 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 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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Mat*_*son 3

您需要做的第一件事是更改private static async void GetToken()方法声明以返回 Task: private static async Task GetToken()。如果它不返回任务,您将无法等待它完成。(正如 GSerg 所提到的,“async void”是“即发即忘”。)

从同步方法调用异步方法的最基本方法是使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

该程序的输出将类似于:

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.
Run Code Online (Sandbox Code Playgroud)

你绝对不能做的是简单地 putwaitForSem().Wait();而不是Task.Run(waitForSem).Wait();,因为这样你可能会陷入死锁(特别是如果它是从带有消息泵的应用程序调用的,例如 WinForms)。

有关更多信息,请参阅从非异步代码调用异步方法

另一种更有效的方法是使用Microsoft.VisualStudio.Threading.dll. 要使用它,您需要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());
    }
}
Run Code Online (Sandbox Code Playgroud)