我正在构建SE API 2.0的API包装器 目前,我正在实现缓存功能,直到现在这还不是问题。现在我考虑了并发性。这将是我的测试方法:
public static void TestConcurrency()
{
Stopwatch sw = new Stopwatch();
sw.Start();
IList<Task> tasks = new List<Task>();
for (int i = 0; i < 1000; i++)
{
tasks.Add(Task.Factory.StartNew(p => client.GetAnswers(), null));
}
Task.WaitAll(tasks.ToArray());
sw.Stop();
Console.WriteLine("elapsed: {0}", sw.Elapsed.ToString());
Console.ReadKey();
}
在内部,客户端有一个RequestHandler
类,它试图从缓存中获取一个值,如果它没有这样做,它会执行实际的请求。
/// <summary>
/// Checks the cache and then performs the actual request, if required.
/// </summary>
/// <typeparam name="T">The strong type of the expected API result against which to deserialize JSON.</typeparam>
/// <param name="endpoint">The API endpoint to query.</param>
/// <returns>The API response object.</returns>
private IApiResponse<T> InternalProcessing<T>(string endpoint) where T : class
{
IApiResponse<T> result = FetchFromCache<T>(endpoint);
return result ?? PerformRequest<T>(endpoint);
}
实际执行请求的代码与此问题无关。尝试访问缓存的代码执行以下操作:
/// <summary>
/// Attempts to fetch the response object from the cache instead of directly from the API.
/// </summary>
/// <typeparam name="T">The strong type of the expected API result against which to deserialize JSON.</typeparam>
/// <param name="endpoint">The API endpoint to query.</param>
/// <returns>The API response object.</returns>
private IApiResponse<T> FetchFromCache<T>(string endpoint) where T : class
{
IApiResponseCacheItem<T> cacheItem = Store.Get<T>(endpoint);
if (cacheItem != null)
{
IApiResponse<T> result = cacheItem.Response;
result.Source = ResultSourceEnum.Cache;
return result;
}
return null;
}
当调用ConcurrentDictionary
方法时,缓存存储的实际实现适用于Get<T>()
,I:
endpoint
的条目。Processing
,线程将暂停一小段时间,等待实际请求完成。< / LI>
endpoint
的条目,null
将作为endpoint
的响应推送到缓存中,表示正在处理该端点上的请求并在那里无需在同一端点上发出更多请求。然后返回null
,表示应该发出实际请求。/// <summary>
/// Attempts to access the internal cache and retrieve a response cache item without querying the API.
/// <para>If the endpoint is not present in the cache yet, null is returned, but the endpoint is added to the cache.</para>
/// <para>If the endpoint is present, it means the request is being processed. In this case we will wait on the processing to end before returning a result.</para>
/// </summary>
/// <typeparam name="T">The strong type of the expected API result.</typeparam>
/// <param name="endpoint">The API endpoint</param>
/// <returns>Returns an API response cache item if successful, null otherwise.</returns>
public IApiResponseCacheItem<T> Get<T>(string endpoint) where T : class
{
IApiResponseCacheItem cacheItem;
if (Cache.TryGetValue(endpoint, out cacheItem))
{
while (cacheItem.IsFresh && cacheItem.State == CacheItemStateEnum.Processing)
{
Thread.Sleep(10);
}
if (cacheItem.IsFresh && cacheItem.State == CacheItemStateEnum.Cached)
{
return (IApiResponseCacheItem<T>)cacheItem;
}
IApiResponseCacheItem value;
Cache.TryRemove(endpoint, out value);
}
Push<T>(endpoint, null);
return null;
}
这个问题是不确定的,有时会有两个请求通过,而不是像它设计的那样。
我正在考虑某个地方正在访问一些不是线程安全的东西。但我无法确定可能是什么。它可能是什么,或者我应该如何正确调试?
问题是ConcurrentDictionary
此方法未重新调整boolean
指示缓存是否已成功更新,因此如果此方法失败,null
将Get<T>()
返回两次。
/// <summary>
/// Attempts to push API responses into the cache store.
/// </summary>
/// <typeparam name="T">The strong type of the expected API result.</typeparam>
/// <param name="endpoint">The queried API endpoint.</param>
/// <param name="response">The API response.</param>
/// <returns>True if the operation was successful, false otherwise.</returns>
public bool Push<T>(string endpoint, IApiResponse<T> response) where T : class
{
if (endpoint.NullOrEmpty())
{
return false;
}
IApiResponseCacheItem item;
if (Cache.TryGetValue(endpoint, out item))
{
((IApiResponseCacheItem<T>)item).UpdateResponse(response);
return true;
}
else
{
item = new ApiResponseCacheItem<T>(response);
return Cache.TryAdd(endpoint, item);
}
}
解决方案是实现返回值,并更改Get<T>()
添加此内容:
if (Push<T>(endpoint, null) || retries > 1) // max retries for sanity.
{
return null;
}
else
{
return Get<T>(endpoint, ++retries); // retry push.
}
答案 0 :(得分:1)
IApiResponseCacheItem<T> cacheItem = Store.Get<T>(endpoint);
if (cacheItem != null)
{
// etc..
}
ConcurrentDirectionary是线程安全的,但不会自动使您的代码线程安全。上面的代码片段是问题的核心。两个线程可以同时调用Get()方法并获得null。他们将继续并同时调用PerformRequest()。您需要合并InternalProcessing()和FetchFromCache()并确保只有一个线程可以使用锁来调用PerformRequest。这可能会产生很差的并发性,也许你可以放弃重复的响应。很可能,SE服务器无论如何都会对请求进行序列化,因此它可能并不重要。