如何在异步方法中管理互斥锁

Nei*_*ski 6 c# synchronization mutex asynchronous linq-to-twitter

我已将旧的HttpHandler(.ashx)TwitterFeed代码移植到WebAPI应用程序.代码的核心使用了出色的Linq2Twitter软件包(https://linqtotwitter.codeplex.com/).部分端口涉及将此组件从版本2升级到版本3,现在提供了许多异步方法调用 - 这对我来说是新的.这是基本的控制器:

public async Task<IEnumerable<Status>> 
GetTweets(int count, bool includeRetweets, bool excludeReplies)
{
   var auth = new SingleUserAuthorizer
   {
      CredentialStore = new SingleUserInMemoryCredentialStore
      {
         ConsumerKey       = ConfigurationManager.AppSettings["twitterConsumerKey"],
         ConsumerSecret    = ConfigurationManager.AppSettings["twitterConsumerKeySecret"],
         AccessToken       = ConfigurationManager.AppSettings["twitterAccessToken"],
         AccessTokenSecret = ConfigurationManager.AppSettings["twitterAccessTokenSecret"]
      }
   };

   var ctx = new TwitterContext(auth);

   var tweets =
      await
      (from tweet in ctx.Status
         where (
            (tweet.Type == StatusType.Home)
            && (tweet.ExcludeReplies == excludeReplies)
            && (tweet.IncludeMyRetweet == includeRetweets)
            && (tweet.Count == count)
         )
      select tweet)
      .ToListAsync();

   return tweets;
}
Run Code Online (Sandbox Code Playgroud)

这工作正常,但之前,我已经缓存了结果,以避免"过度调用"Twitter API.在这里,我遇到了一个问题(更多的是与我对异步协议的理解不比我怀疑的其他问题).

概括地说,我想要做的是首先检查缓存,如果数据不存在,则重新水化缓存并将数据返回给调用者(网页).这是我对代码的尝试

public class TwitterController : ApiController {

   private const string CacheKey = "TwitterFeed";

   public async Task<IEnumerable<Status>>
   GetTweets(int count, bool includeRetweets, bool excludeReplies)
   {
      var context = System.Web.HttpContext.Current;
      var tweets = await GetTweetData(context, count, includeRetweets, excludeReplies);
      return tweets;
   }

   private async Task<IEnumerable<Status>>
   GetTweetData(HttpContext context, int count, bool includeRetweets, bool excludeReplies)
   {
      var cache = context.Cache;
      Mutex mutex = null;
      bool iOwnMutex = false;
      IEnumerable<Status> data = (IEnumerable<Status>)cache[CacheKey];

      // Start check to see if available on cache
      if (data == null)
      {
         try
         {
            // Lock base on resource key
            mutex = new Mutex(true, CacheKey);

            // Wait until it is safe to enter (someone else might already be
            // doing this), but also add 30 seconds max.
            iOwnMutex = mutex.WaitOne(30000);

            // Now let's see if some one else has added it...
            data = (IEnumerable<Status>)cache[CacheKey];

            // They did, so send it...
            if (data != null)
            {
               return data;
            }

            if (iOwnMutex)
            {
               // Still not there, so now is the time to look for it!
               data = await CallTwitterApi(count, includeRetweets, excludeReplies);

               cache.Remove(CacheKey);
               cache.Add(CacheKey, data, null, GetTwitterExpiryDate(),
                  TimeSpan.Zero, CacheItemPriority.Normal, null);
            }
         }
         finally
         {
            // Release the Mutex.
            if ((mutex != null) && (iOwnMutex))
            {
               // The following line throws the error:
               // Object synchronization method was called from an
               // unsynchronized block of code.
               mutex.ReleaseMutex();
            }
         }
      }

      return data;
   }

   private DateTime GetTwitterExpiryDate()
   {
      string szExpiry = ConfigurationManager.AppSettings["twitterCacheExpiry"];
      int expiry = Int32.Parse(szExpiry);
      return DateTime.Now.AddMinutes(expiry);
   }

   private async Task<IEnumerable<Status>>
   CallTwitterApi(int count, bool includeRetweets, bool excludeReplies)
   {
      var auth = new SingleUserAuthorizer
      {
         CredentialStore = new SingleUserInMemoryCredentialStore
         {
            ConsumerKey = ConfigurationManager.AppSettings["twitterConsumerKey"],
            ConsumerSecret = ConfigurationManager.AppSettings["twitterConsumerKeySecret"],
            AccessToken = ConfigurationManager.AppSettings["twitterAccessToken"],
            AccessTokenSecret = ConfigurationManager.AppSettings["twitterAccessTokenSecret"]
         }
      };

      var ctx = new TwitterContext(auth);

      var tweets =
         await
         (from tweet in ctx.Status
          where (
             (tweet.Type == StatusType.Home)
             && (tweet.ExcludeReplies == excludeReplies)
             && (tweet.IncludeMyRetweet == includeRetweets)
             && (tweet.Count == count)
             && (tweet.RetweetCount < 1)
          )
          select tweet)
         .ToListAsync();

      return tweets;
   }

}
Run Code Online (Sandbox Code Playgroud)

问题出现在发布Mutex的finally代码块中(尽管我担心GetTweetData()方法的整体模式和方法):

if ((mutex != null) && (iOwnMutex))
{
    // The following line throws the error:
    // Object synchronization method was called from an
    // unsynchronized block of code.
    mutex.ReleaseMutex();
}
Run Code Online (Sandbox Code Playgroud)

如果我注释掉该行,代码可以正常工作,但(我假设)我应该释放创建它的Mutex.根据我的发现,这个问题与创建和释放互斥锁之间的线程变化有关.

由于我对异步编码缺乏一般知识,我不确定a)我使用的模式是否可行,b)如果是,我如何解决问题.

任何建议将不胜感激.

Ste*_*ary 5

使用这样的互斥锁是行不通的。一方面,a Mutex是线程仿射的,因此不能与async代码一起使用。

我注意到的其他问题:

  • Cache 是线程安全的,因此无论如何都不需要互斥(或任何其他保护)。
  • 异步方法应遵循基于任务的异步模式

关于缓存,有一个主要技巧:当您只有内存中的缓存时,然后缓存任务而不是结果数据。附带说明一下,我不得不怀疑是否HttpContext.Cache是最佳缓存,但是我将保持现状,因为您的问题更多是关于异步代码如何更改缓存模式。

所以,我建议这样的事情:

private const string CacheKey = "TwitterFeed";

public Task<IEnumerable<Status>> GetTweetsAsync(int count, bool includeRetweets, bool excludeReplies)
{
  var context = System.Web.HttpContext.Current;
  return GetTweetDataAsync(context, count, includeRetweets, excludeReplies);
}

private Task<IEnumerable<Status>> GetTweetDataAsync(HttpContext context, int count, bool includeRetweets, bool excludeReplies)
{
  var cache = context.Cache;
  Task<IEnumerable<Status>> data = cache[CacheKey] as Task<IEnumerable<Status>>;
  if (data != null)
    return data;
  data = CallTwitterApiAsync(count, includeRetweets, excludeReplies);
  cache.Insert(CacheKey, data, null, GetTwitterExpiryDate(), TimeSpan.Zero);
  return data;
}

private async Task<IEnumerable<Status>> CallTwitterApiAsync(int count, bool includeRetweets, bool excludeReplies)
{
  ...
}
Run Code Online (Sandbox Code Playgroud)

如果两个不同的请求(来自两个不同的会话)在相同的确切时间请求相同的Twitter提要,则该提要将被请求两次。但是我不会因此而睡不着。