接收并发异步请求并一次处理一个

时间:2018-05-05 00:09:38

标签: c# .net multithreading asynchronous concurrency

背景

我们有一个服务操作,可以接收并发的异步请求,并且必须一次处理一个请求。

在以下示例中,UploadAndImport(...)方法接收多个线程上的并发请求,但是它对ImportFile(...)方法的调用必须一次发生一次。

Layperson Description

想象一下拥有许多工人(多线程)的仓库。人(客户)可以同时(同时)向仓库发送许多包(请求)。当一个包裹进来时,一个工人从头到尾负责它,掉下包裹的人可以离开(发射 - 忘记)。工人的工作是将每个包裹放在一个小滑槽上,一次只有一个工人可以将包裹放在滑槽上,否则会发生混乱。如果放弃包裹的人稍后检查(轮询终点),仓库应该能够报告包裹是否沿着滑槽下降。

问题

接下来的问题是如何编写一个服务操作......

  1. 可以接收并发客户端请求,
  2. 在多个线程上接收并处理这些请求,
  3. 处理接收请求的同一线程上的请求,
  4. 一次处理一个请求,
  5. 是单向即发即弃操作,
  6. 有一个单独的轮询端点,用于报告请求完成情况。
  7. 我们尝试了以下内容并且想知道两件事:

    1. 我们有没有考虑过任何竞争条件?
    2. 是否有更规范的方法在C#.NET中使用面向服务的体系结构(我们碰巧使用WCF)对此方案进行编码?
    3. 示例:我们尝试过什么?

      这是我们尝试过的服务代码。虽然感觉有点像黑客或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,我们都在询问其方法中的潜在缺陷以及更合适/规范的替代模式。

3 个答案:

答案 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();
}

说明:

  • BlockingCollection是IDisposable
  • TODO:你必须&#34;关闭&#34; BlockingCollection标记完成: &#34; BlockingCollection.CompleteAdding()&#34;或者它将不确定地循环等待进一步的请求。也许您为客户端引入了一个额外的请求方法来取消和/或更新进程,并将BlockingCollection标记为已完成。或者是在将其标记为已完成之前等待空闲时间的计时器。或者让你的请求处理程序线程阻塞或旋转。
  • 如果您想要取消支持,请将Take()和Add(...)替换为TryTake(...)和TryAdd(...)
  • 代码未经过测试
  • 您的&#34; ImportFile()&#34;方法是多线程环境中的瓶颈。我建议让它线程安全。如果I / O需要同步,我会将数据缓存在BlockingCollection中,然后逐个将它们写入I / O.

答案 1 :(得分:1)

问题在于您的总带宽非常小 - 一次只能运行一个作业 - 并且您希望处理并行请求。这意味着排队时间可能会有很大差异。它可能不是在内存中实现作业队列的最佳选择,因为它会使您的系统更加脆弱,并且在您的业务增长时更难以扩展。

一种传统的,可扩展的方法来构建这个:

  • 接受请求的HTTP服务,负载均衡/冗余,没有会话状态。
  • SQL Server数据库,用于将请求保留在队列中,返回持久的唯一作业ID。
  • 用于处理队列的Windows服务,一次处理一个作业,并将作业标记为已完成。该服务的工作进程可能是单线程的。

此解决方案要求您选择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个文件的批次往往会一次到达,然后安静几个小时,直到下一批文件到达。

非常欢迎批评和反馈。