我们有一个服务操作,可以接收并发的异步请求,并且必须一次处理一个请求。
在以下示例中,UploadAndImport(...)
方法接收多个线程上的并发请求,但是它对ImportFile(...)
方法的调用必须一次发生一次。
想象一下拥有许多工人(多线程)的仓库。人(客户)可以同时(同时)向仓库发送许多包(请求)。当一个包裹进来时,一个工人从头到尾负责它,掉下包裹的人可以离开(发射 - 忘记)。工人的工作是将每个包裹放在一个小滑槽上,一次只有一个工人可以将包裹放在滑槽上,否则会发生混乱。如果放弃包裹的人稍后检查(轮询终点),仓库应该能够报告包裹是否沿着滑槽下降。
接下来的问题是如何编写一个服务操作......
我们尝试了以下内容并且想知道两件事:
这是我们尝试过的服务代码。虽然感觉有点像黑客或kludge,但它仍然有效。
static ImportFileInfo _inProgressRequest = null;
static readonly ConcurrentDictionary<Guid, ImportFileInfo> WaitingRequests =
new ConcurrentDictionary<Guid, ImportFileInfo>();
public void UploadAndImport(ImportFileInfo request)
{
// Receive the incoming request
WaitingRequests.TryAdd(request.OperationId, request);
while (null != Interlocked.CompareExchange(ref _inProgressRequest, request, null))
{
// Wait for any previous processing to complete
Thread.Sleep(500);
}
// Process the incoming request
ImportFile(request);
Interlocked.Exchange(ref _inProgressRequest, null);
WaitingRequests.TryRemove(request.OperationId, out _);
}
public bool UploadAndImportIsComplete(Guid operationId) =>
!WaitingRequests.ContainsKey(operationId);
这是示例客户端代码。
private static async Task UploadFile(FileInfo fileInfo, ImportFileInfo importFileInfo)
{
using (var proxy = new Proxy())
using (var stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read))
{
importFileInfo.FileByteStream = stream;
proxy.UploadAndImport(importFileInfo);
}
await Task.Run(() => Poller.Poll(timeoutSeconds: 90, intervalSeconds: 1, func: () =>
{
using (var proxy = new Proxy())
{
return proxy.UploadAndImportIsComplete(importFileInfo.OperationId);
}
}));
}
很难在小提琴中写出这个最小可行的例子,但here is a start给出了一个感觉并且编译。
和以前一样,上面看起来像是一个黑客/ kludge,我们都在询问其方法中的潜在缺陷以及更合适/规范的替代模式。
答案 0 :(得分:1)
使用Producer-Consumer模式在线程数限制情况下管道请求的简单解决方案。
您仍然需要实施简单的进度报告或事件。我建议用Microsoft的SignalR库提供的异步通信替换昂贵的轮询方法。它使用WebSocket来启用异步行为。客户端和服务器可以在集线器上注册其回调。使用RPC,客户端现在可以调用服务器端方法,反之亦然。您可以使用集线器(客户端)将进度发布到客户端。根据我的经验,SignalR使用起来非常简单,而且记录非常好。它有一个用于所有着名服务器端语言(例如Java)的库。
在我的理解中进行轮询与完全相反。你不能忘记,因为你必须根据间隔检查一些东西。像SignalR这样的基于事件的通信是免费的,因为你开火并且会得到提醒(因为你忘了)。 &#34;事件方&#34;会调用你的回调,而不是你自己等着做!
要求5被忽略,因为我没有任何理由。等待线程完成将消除火灾并忘记角色。
private BlockingCollection<ImportFileInfo> requestQueue = new BlockingCollection<ImportFileInfo>();
private bool isServiceEnabled;
private readonly int maxNumberOfThreads = 8;
private Semaphore semaphore = new Semaphore(numberOfThreads);
private readonly object syncLock = new object();
public void UploadAndImport(ImportFileInfo request)
{
// Start the request handler background loop
if (!this.isServiceEnabled)
{
this.requestQueue?.Dispose();
this.requestQueue = new BlockingCollection<ImportFileInfo>();
// Fire and forget (requirement 4)
Task.Run(() => HandleRequests());
this.isServiceEnabled = true;
}
// Cache multiple incoming client requests (requirement 1) (and enable throttling)
this.requestQueue.Add(request);
}
private void HandleRequests()
{
while (!this.requestQueue.IsCompleted)
{
// Wait while thread limit is exceeded (some throttling)
this.semaphore.WaitOne();
// Process the incoming requests in a dedicated thread (requirement 2) until the BlockingCollection is marked completed.
Task.Run(() => ProcessRequest());
}
// Reset the request handler after BlockingCollection was marked completed
this.isServiceEnabled = false;
this.requestQueue.Dispose();
}
private void ProcessRequest()
{
ImportFileInfo request = this.requestQueue.Take();
UploadFile(request);
// You updated your question saying the method "ImportFile()" requires synchronization.
// This a bottleneck and will significantly drop performance, when this method is long running.
lock (this.syncLock)
{
ImportFile(request);
}
this.semaphore.Release();
}
说明:
答案 1 :(得分:1)
问题在于您的总带宽非常小 - 一次只能运行一个作业 - 并且您希望处理并行请求。这意味着排队时间可能会有很大差异。它可能不是在内存中实现作业队列的最佳选择,因为它会使您的系统更加脆弱,并且在您的业务增长时更难以扩展。
一种传统的,可扩展的方法来构建这个:
此解决方案要求您选择Web服务器。一个常见的选择是运行ASP.NET的IIS。在该平台上,保证每个请求都以单线程方式处理(即,您不必过多担心竞争条件),但由于名为thread agility的功能,请求可能会结束使用不同的线程,但在原始同步上下文中,这意味着除非您正在调试和检查线程ID,否则您可能永远不会注意到。
答案 2 :(得分:0)
鉴于我们系统的约束上下文,这是我们最终使用的实现:
static ImportFileInfo _importInProgressItem = null;
static readonly ConcurrentQueue<ImportFileInfo> ImportQueue =
new ConcurrentQueue<ImportFileInfo>();
public void UploadAndImport(ImportFileInfo request) {
UploadFile(request);
ImportFileSynchronized(request);
}
// Synchronize the file import,
// because the database allows a user to perform only one write at a time.
private void ImportFileSynchronized(ImportFileInfo request) {
ImportQueue.Enqueue(request);
do {
ImportQueue.TryPeek(out var next);
if (null != Interlocked.CompareExchange(ref _importInProgressItem, next, null)) {
// Queue processing is already under way in another thread.
return;
}
ImportFile(next);
ImportQueue.TryDequeue(out _);
Interlocked.Exchange(ref _importInProgressItem, null);
}
while (ImportQueue.Any());
}
public bool UploadAndImportIsComplete(Guid operationId) =>
ImportQueue.All(waiting => waiting.OperationId != operationId);
此解决方案适用于我们期望的负载。该负载涉及最多约15-20个并发PDF文件上传。最多15-20个文件的批次往往会一次到达,然后安静几个小时,直到下一批文件到达。
非常欢迎批评和反馈。