【问题标题】:A way to Lock await calls one at a time when there are multiple calls calling the method? [duplicate]当有多个调用调用该方法时,一种锁定等待调用的方法? [复制]
【发布时间】:2025-12-26 15:20:08
【问题描述】:

我有一个供人们调用服务的 api 端点, 如果缓存已过期,我只希望 GetCacheToken 一次运行一个。

显然我通过getAccessToken同时有3个或更多调用getCacheToken,并且它同步处理(不知道为什么),当它等待_client.SendAsync(requestData);它将刷新刷新令牌,旧的刷新令牌将不再有效。

所以第一次调用通过,第二次和第三次失败,因为他们都使用旧令牌在发送请求中传递它。

有没有办法在缓存过期时锁定进程? 因为 Lock 不能用于 await 调用。 谢谢

认为它会被异步等待处理但没有运气

    [System.Web.Http.HttpGet]
    [System.Web.Http.Route("getAccessToken")]
    public async Task<string> getAccessToken()
    {
        return await cacheToken.GetCacheToken();
    }       


    private static readonly SemaphoreSlim _mutex = new SemaphoreSlim(1,1); 

     public async Task<string> GetCacheToken()
    {
        ObjectCache cache = MemoryCache.Default;
        var cacheToken = cache.Get("refreshToken", null);
        string refreshToken = cacheToken == null ? GetToken() : cacheToken.ToString();

        if (!cache.Contains("apiToken"))
        {
            //it prevent multiple threads call to update refreshtoken at the same time
            await _mutex.WaitAsync();
            try
            {
                //Log.Information("Access token " + cache.Get("apiToken", null));
                var isNull = cache.Get("refreshToken", null) == null ? "Yes " : "No ";
                var logtime = DateTime.Now.ToString("O");
                Log.Information("refresh token is null : " + isNull + DateTime.Now.ToString("O") + " " + logtime);
                Log.Information(
                    "refresh token : " + refreshToken + " " + DateTime.Now.ToString("O") + " " + logtime);

                var httpContent = new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded");
                var dict = new Dictionary<string, string>();
                dict.Add("grant_type", "refresh_token");
                dict.Add("refresh_token", refreshToken);
                var requestData = new HttpRequestMessage
                {
                    Method = HttpMethod.Post,
                    RequestUri = new Uri("https://oauth2.sky.blackbaud.com/token"),
                    Content = new FormUrlEncodedContent(dict)
                };

                requestData.Headers.Authorization = new AuthenticationHeaderValue("Basic", Settings.BasicAuth);
                var results = await _client.SendAsync(requestData);
                Log.Information("run time " + DateTime.Now);
                var resultResponse = results.Content.ReadAsStringAsync().Result;

                try
                {
                    results.EnsureSuccessStatusCode();
                    var result = _js.Deserialize<TokenModel>(resultResponse);
                    //token expires in one hour from blackbaud
                    var expiration = DateTimeOffset.Now.AddMinutes(55);
                    cache.Set("apiToken", result.access_token, expiration);
                    cache.Set("refreshToken", result.refresh_token, expiration);
                    await UpdateToken(result.access_token, result.refresh_token);
                    Log.Information("refresh token after update : " + cache.Get("refreshToken", null) +
                                    DateTime.Now.ToString("O"));
                }
                catch (Exception e)
                {
                    var exceptionMessage = $"ResultMessage : {resultResponse} Exception: {e}.";
                    Log.Exception(e, exceptionMessage);
                    throw;
                }
            }
            finally
            {
                _mutex.Release();
            }
        }

        return cache.Get("apiToken", null).ToString();
    }

这是日志

12 Jul 2019 12:00:10.847
 ResultMessage : {"error":"invalid_grant"} Exception: System.Net.Http.HttpRequestException: Response status code does not indicate success: 400 (Bad Request). at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode() at RaisersEdge.Infrastructure.Cache.<GetCacheToken>d__3.MoveNext().Exception: HttpRequestException {"Message":"Response status code does not indicate success: 400 (Bad Request).","Data":[],…Stack: at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode() at RaisersEdge.Infrastructure.Cache.<GetCacheToken>d__3.MoveNext()Category: Exception
12 Jul 2019 12:00:10.841
 run time 12/07/2019 12:00:10 PM
12 Jul 2019 12:00:09.981
 refresh token : 03124379add64a90850961e6a2021d6e 2019-07-12T12:00:09.9813412+12:00 2019-07-12T12:00:09.9813412+12:00
12 Jul 2019 12:00:09.981
 refresh token is null : No 2019-07-12T12:00:09.9813412+12:00 2019-07-12T12:00:09.9813412+12:00
12 Jul 2019 12:00:09.980
 refresh token after update : f0b569cfa2254bbfbd78e9d84ddd66ae2019-07-12T12:00:09.9803523+12:00
12 Jul 2019 12:00:09.951
 run time 12/07/2019 12:00:09 PM
12 Jul 2019 12:00:08.970
 refresh token : 03124379add64a90850961e6a2021d6e 2019-07-12T12:00:08.9212759+12:00 2019-07-12T12:00:08.9192713+12:00
12 Jul 2019 12:00:08.942
 refresh token is null : Yes 2019-07-12T12:00:08.9192713+12:00 2019-07-12T12:00:08.9192713+12:00

【问题讨论】:

标签: c# async-await


【解决方案1】:

lock 的异步等效项是 SemaphoreSlim

如果您想要的代码如下所示:

private readonly object _mutex = new object();

...
lock (_mutex)
{
  ...
  var results = await _client.SendAsync(requestData);
  ...
}

然后您可以使用SemaphoreSlim 来获得所需的行为:

private readonly SemaphoreSlim _mutex = new SemaphoreSlim();

...
await _mutex.WaitAsync();
try
{
  ...
  var results = await _client.SendAsync(requestData);
  ...
}
finally
{
  _mutex.Release();
}

【讨论】:

  • 信号量不应该是静态的吗?
  • @xxbbcc:在这种情况下,是的。
  • 感谢您的回复,只是想知道以这种方式这样做是一种好习惯吗?
  • 如果你真的必须阻止其他调用,那么可以。
  • 抱歉,它仍然无法正常工作,我已设置为 new SemaphoreSlim(1,1);一次一个,并尝试使用 1,3,但不起作用。2 个呼叫在日志中同时通过,一个在 2019 年 12:00:09 另一个在 2019 年 12:00:08