如何使用异步操作正确获取数据?

时间:2015-12-07 12:03:20

标签: c# multithreading

我需要通过等待已经运行的操作的结果来处理并发请求。

private readonly object _gettingDataTaskLock = new object();
private Task<Data> _gettingDataTask;

public virtual Data GetData(Credential credential)
{
    Task<Data> inProgressDataTask = null;

    lock (_gettingDataTaskLock)
    {
        if (_gettingDataTask == null)
        {
            _gettingDataTask = Task.Factory.StartNew(() 
                                                    => GetDataInternal(credential));
            _gettingDataTask.ContinueWith((task) =>
            {
                lock (_gettingDataTaskLock)
                {
                    _gettingDataTask = null;
                }
             },
             TaskContinuationOptions.ExecuteSynchronously);
         }

          inProgressDataTask = _gettingDataTask;
     }

     try
     {
         return inProgressDataTask.Result;
     }
     catch (AggregateException ex)
     {
         _logger.ErrorException(ex, "An error occurs during getting full data");
         throw ex.InnerException;
      }
 }

我有一个潜在的问题:几个电话GetData可能与credentials不同。首先使用正确的凭据调用,然后使用错误的凭据进行调用。这对夫妇的请求得到了与第一个相同的答案。如何解决这个问题?

有没有办法简化这段代码并使其线程安全且无bug?

3 个答案:

答案 0 :(得分:3)

更新:

似乎这里实现了双锁检查,就像单例模式一样。 GetDataInternal()只要执行就会被缓存。 例如,如果它在100毫秒内执行,并且每10毫秒运行一个GetData(),则前10个调用将使用相同的GetDataInternal()

TaskContinuationOptions.ExecuteSynchronously用于确保在同一个线程中运行continuation。

 public virtual Data GetData(Credential credential)
 {
     Task<Data> inProgressDataTask = Task.Factory.StartNew(() => GetDataInternal(credential));

     try
     {
         return inProgressDataTask.Result;
     }
     catch (AggregateException ex)
     {
         _logger.ErrorException(ex, "An error occurs during getting full data");
         throw ex.InnerException;
     }
 }

异步/等待场景:

Data GetInternalData()更改为async Task<Data> GetInternalData()

将方法GetData()更改为

public virtual async Task<Data> GetData()
{
     try
     {
         return await GetInternalData();
     }
     catch (AggregateException ex)
     {
         _logger.ErrorException(ex, "An error occurs during getting full data");
         throw ex.InnerException;
     }
 }

答案 1 :(得分:2)

以下是我对要求的解释:

  • 数据请求可能同时使用相同/不同的凭据。
  • 对于每个唯一的凭据集,最多只能进行一次GetDataInternal次呼叫,其中一次调用的结果会在准备就绪时返回给所有排队的服务员。
  • 在此之后,上一次通话的结果无效,并且将允许使用同一组凭据进行新的GetDataInternal通话。
  • 允许使用不同凭据并行呼叫GetDataInternal

轻松自负。

private readonly Dictionary<Credential, Lazy<Data>> Cache
    = new Dictionary<Credential, Lazy<Data>>();

public Data GetData(Credential credential)
{
    if (credential == null)
    {
        // Pass-through, no caching.
        return GetDataInternal(null);
    }

    Lazy<Data> lazy;

    lock (Cache)
    {
        if (!Cache.TryGetValue(credential, out lazy))
        {
            // ExecutionAndPublication is the default LazyThreadSafetyMode, but I 
            // wanted to spell it out to drive the point home: this Lazy instance 
            // will only allow a single call to GetDataInternal, even if multiple
            // threads query its Value property simultaneously.
            lazy = new Lazy<Data>(
                () => GetDataInternal(credential),
                LazyThreadSafetyMode.ExecutionAndPublication
            );

            Cache.Add(credential, lazy);
        }
    }

    // We have released the lock on Cache to allow other threads
    // (with potentially *different* credentials) to access the
    // cache and proceed with their own work in parallel with us.
    Data data;

    try
    {
        // Wait for the GetDataInternal call to complete.
        data = lazy.Value;
    }
    finally
    {
        // At this point the call to GetDataInternal has completed and Data is ready.
        // We will invalidate the cache entry if another thread hasn't done so already.
        lock (Cache)
        {
            // The lock is required to ensure the atomicity of our "fetch + compare + remove" operation.
            // *ANY* thread is allowed to invalidate the cached value, not just the thread that created it.
            // This ensures that the cache entry is cleaned up sooner rather than later.
            // The equality check on the Lazy<Data> instance ensures that the cache entry
            // is not cleaned up too soon, and prevents the following race:
            // (assume all operations use identical credentials)
            // - Thread A creates and stores a Lazy<Data> instance in the cache.
            // - Thread B fetches the Lazy<Data> instance created by Thread A.
            // - Threads A and B access Lazy<Data>.Value simultaneously.
            // - Thread B wins the race and enters the second (this) protected
            //   region and invalidates the cache entry created by Thread A.
            // - Thread C creates and stores a *NEW* Lazy<Data> instance in the cache.
            // - Thread C accesses its Lazy<Data>.Value.
            // - Thread A finally gets to invalidate the cache, and OOPS, Thread C's cache
            //   entry is invalidated before the call to Lazy<Data>.Value has completed.
            // With the equality check in place, Thread A will *not*
            // invalidate the cache entry created by another thread.
            Lazy<Data> currentLazy;

            if (Cache.TryGetValue(credential, out currentLazy) && lazy == currentLazy)
            {
                // Need to invalidate.
                Cache.Remove(credential);
            }
        }
    }

    return data;
}

注意事项:

  • 凭据必须覆盖EqualsGetHashCode才能进行相等比较。
  • 如果您选择将其设为虚拟,请非常小心覆盖此方法。
  • 删除了错误处理,专注于&#34; meat&#34;。

如果您使用的是.NET 4.5或更高版本(如果您使用的是.NET 4.0,那么上面会转换为Task<Data> - 返回方法并进行一些小的调整)。如果这是一项要求,请告诉我,如果需要,请告诉您哪个版本的.NET。

编辑:我添加了一个try/finally - 应该一直在那里开始。

此外,这是一个ConcurrentDictionary版本,我在其中解决了它的局限性 - 你会发现它只是更清洁一点:

private readonly ConcurrentDictionary<Credential, Lazy<Data>> Cache
    = new ConcurrentDictionary<Credential, Lazy<Data>>();

public Data GetData(Credential credential)
{
    if (credential == null)
    {
        // Pass-through, no caching.
        return GetDataInternal(null);
    }

    // This instance will be thrown away if a cached
    // value with our "credential" key already exists.
    Lazy<Data> newLazy = new Lazy<Data>(
        () => GetDataInternal(credential),
        LazyThreadSafetyMode.ExecutionAndPublication
    );

    Lazy<Data> lazy = Cache.GetOrAdd(credential, newLazy);
    bool added = ReferenceEquals(newLazy, lazy); // If true, we won the race.
    Data data;

    try
    {
        // Wait for the GetDataInternal call to complete.
        data = lazy.Value;
    }
    finally
    {
        // Only the thread which created the cache value
        // is allowed to remove it, to prevent races.
        if (added) {
            Cache.TryRemove(credential, out lazy);
        }
    }

    return data;
}

答案 2 :(得分:0)

当您使用MultithreadingMultiprocessing时,您必须采取措施来管理它们。 Multithreading是一个庞大而复杂的话题。您的代码可能会出现不同的情况;假设在运行这行代码之前:

return inProgressDataTask.Result;

每个线程执行以下代码行:

_gettingDataTask = null;

这是可能的,因为Task已在async中运行。 请注意,inProgressDataTask是指_gettingDataTask所指的地方:

inProgressDataTask = _gettingDataTask;

_gettingDataTask对象在所有任务(线程)之间共享,这可能会导致程序出错。如果你想使用它在这种情况下,你可以使用像Singleton模式这样的东西。如果您使用共享对象,请不要在代码的锁定部分使用Task.Factory.StartNew_gettingDataTask.ContinueWith(在异步中运行)。

或者假设在运行这行代码之前:

_gettingDataTask = null;

其他一些线程执行这行代码:

if (_gettingDataTask == null)

这使得与第一个线程相关的所有答案都被执行。

为了澄清问题,请抓住新项目并在其中添加以下代码:

class Test
{
    private readonly object _gettingDataTaskLock = new object();
    private Task<int> _gettingDataTask;

    public int GetData(int credential)
    {
        Task<int> inProgressDataTask = null;
        Console.WriteLine(credential + "-TEST0");
        lock (_gettingDataTaskLock)
        {
            Console.WriteLine(credential + "-TEST1");
            if (_gettingDataTask == null)
            {
                Console.WriteLine(credential + "-TEST2");
                _gettingDataTask = Task.Factory.StartNew(()
                    => GetDataInternal(credential));
                _gettingDataTask.ContinueWith((task) =>
                {
                    lock (_gettingDataTaskLock)
                    {
                        Console.WriteLine(credential + "-TEST3");
                        _gettingDataTask = null;
                        Console.WriteLine(credential + "-TEST4");
                    }
                },
                    TaskContinuationOptions.ExecuteSynchronously);
                Console.WriteLine(credential + "-TEST5");
            }

            //_gettingDataTask.Wait();
            Console.WriteLine(credential + "-TEST6");
            inProgressDataTask = _gettingDataTask;
            Console.WriteLine(credential + "-TEST7");
        }
        Console.WriteLine(credential + "-TEST8");
        try
        {
            Console.WriteLine(credential + "-TEST9");
            return inProgressDataTask.Result;
        }
        catch (AggregateException ex)
        {
            throw ex.InnerException;
        }
    }

    private int GetDataInternal(int credential)
    {
        return credential;
    }
}

在main方法中写:

private static void Main(string[] args)
{
    var test = new Test();
    var results = new List<int>();
    for (int i = 0; i < 5; i++)
        results.Add(test.GetData(i));

    Task.WaitAll();
    Console.WriteLine(string.Join(",",results));
    Console.ReadLine();
}

现在看到结果。

最后,根据给出的描述,您的代码的正确实现之一如下:

public virtual Data GetData(Credential credential)
{
    Task<Data> inProgressDataTask = null;
    lock (_gettingDataTaskLock)
    {
        inProgressDataTask = Task.Factory.StartNew(() => GetDataInternal(credential));
        inProgressDataTask.ContinueWith((task) =>
        {
            lock (_gettingDataTaskLock)
            {
                //inProgressDataTask = null;
            }
        },
        TaskContinuationOptions.ExecuteSynchronously);
    }

    try
    {
        return inProgressDataTask.Result;
    }
    catch (AggregateException ex)
    {
        throw ex.InnerException;
    }
}