我需要通过等待已经运行的操作的结果来处理并发请求。
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?
答案 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;
}
注意事项:
Equals
和GetHashCode
才能进行相等比较。如果您使用的是.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)
当您使用Multithreading
或Multiprocessing
时,您必须采取措施来管理它们。 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;
}
}