2017-05-08 35 views
2

我注意到在我的流程中发生了初始减速,并且在进行多次hangdumps时,我能够使用以下代码隔离问题并重现该情形。我正在使用一个具有锁定功能的库,它最终会调用某些方法的用户端实现。这些方法使用httpclient进行异步调用。这些异步调用是从库内的这些锁中进行的。使用具有锁定的异步调用速度减慢

现在,我正在发生什么的理论(纠正我,如果我错了): 获取旋转的任务尝试获取锁并保持足够快的线程,以便第一个PingAsync方法需要等待默认的任务调度程序启动一个新的线程以便运行,这是基于默认的.net调度算法的0.5秒。这就是为什么我认为我注意到任务总数大于32的延迟,这也随着总任务数的增加而线性增加。

解决方法:

  1. 增加minthreads算,我认为这是治标,而不是实际的问题。
  2. 另一种方法是有一个有限的并发来控制任务的数量。但这些都是由传入HTTPRequests的网络服务器纺任务,通常我们不会拥有控制权(还是我们?)

我明白,结合ASYC和非异步是糟糕的设计,并使用sempahores'异步调用将是更好的方式去。假设我无法控制这个图书馆,那么如何减轻这个问题呢?

const int ParallelCount = 16; 
    const int TotalTasks = 33; 

    static object _lockObj = new object(); 
    static HttpClient _httpClient = new HttpClient(); 
    static int count = 0; 

    static void Main(string[] args) 
    { 
     ThreadPool.GetMinThreads(out int workerThreads, out int ioThreads); 
     Console.WriteLine($"Min threads count. Worker: {workerThreads}. IoThreads: {ioThreads}"); 

     ThreadPool.GetMaxThreads(out workerThreads, out ioThreads); 
     Console.WriteLine($"Max threads count. Worker: {workerThreads}. IoThreads: {ioThreads}"); 

     //var done = ThreadPool.SetMaxThreads(1024, 1000); 
     //ThreadPool.GetMaxThreads(out workerThreads, out ioThreads); 
     //Console.WriteLine($"Set Max Threads success? {done}."); 
     //Console.WriteLine($"Max threads count. Worker: {workerThreads}. IoThreads: {ioThreads}"); 

     //var done = ThreadPool.SetMinThreads(1024, 1000); 
     //ThreadPool.GetMinThreads(out workerThreads, out ioThreads); 
     //Console.WriteLine($"Set Min Threads success? {done}."); 
     //Console.WriteLine($"Min threads count. Worker: {workerThreads}. IoThreads: {ioThreads}"); 

     var startTime = DateTime.UtcNow; 
     var tasks = new List<Task>(); 

     for (int i = 0; i < TotalTasks; i++) 
     { 
      tasks.Add(Task.Run(() => LibraryMethod())); 

      //while (tasks.Count > ParallelCount) 
      //{ 
      // var task = Task.WhenAny(tasks.ToArray()).GetAwaiter().GetResult(); 

      // if (task.IsFaulted) 
      // { 
      //  throw task.Exception; 
      // } 

      // tasks.Remove(task); 
      //} 
     } 

     Task.WaitAll(tasks.ToArray()); 

     //while (tasks.Count > 0) 
     //{ 
     // var task = Task.WhenAny(tasks.ToArray()).GetAwaiter().GetResult(); 

     // if (task.IsFaulted) 
     // { 
     //  throw task.Exception; 
     // } 

     // tasks.Remove(task); 

     // Console.Write("."); 
     //} 

     Console.Write($"\nDone in {(DateTime.UtcNow-startTime).TotalMilliseconds}"); 
     Console.ReadLine(); 
    } 

假设这是其中文库的方法被称为部分,

public static void LibraryMethod() 
    { 
     lock (_lockObj) 
     { 
      SimpleNonAsync(); 
     } 
    } 

最终,该方法的用户执行被调用其是异步。

public static void SimpleNonAsync() 
    { 
      //PingAsync().Result; 
      //PingAsync().ConfigureAwaiter(false).Wait(); 
      PingAsync().Wait(); 
    } 

    private static async Task PingAsync() 
    { 
     Console.Write($"{Interlocked.Increment(ref count)}."); 

     await _httpClient.SendAsync(new HttpRequestMessage 
     { 
      RequestUri = new Uri([email protected]"http://127.0.0.1"), 
      Method = HttpMethod.Get 
     }); 
    } 
+1

异步调用上的阻塞破坏了异步的整个目的。 – SLaks

+0

是的。我明白那个。考虑到我没有修改这个库的权限,你是否建议使用非异步httpclient调用(假设有一个),这会影响性能,因为它是一个io操作? – iambatman

+1

请注意[任务(仍然)不是线程](https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not -parallel /)... –

回答

2

这些异步调用从库中,这些锁内进行。

这是一个设计缺陷。没有人应该在锁定的情况下调用任意代码。

也就是说,锁与您所看到的问题无关。

我知道结合asyc和非异步是不好的设计,并且使用sempahores的异步调用将是更好的方法。假设我无法控制这个图书馆,那么如何减轻这个问题呢?

问题库迫使你的代码是同步的。这意味着每个下载都会阻塞一个线程;只要图书馆的回调是同步的,就没有办法。

增加minthreads计数,我认为这是治疗症状,而不是实际问题。

如果你不能修改库,那么你必须使用每个请求一个线程,这成为一个可行的解决方法。你来治疗症状,因为你不能解决问题(即图书馆)。

另一种方法是有一个有限的并发来控制任务的数量。但是这些都是web服务器针对传入httprequests所执行的任务,通常我们无法控制它(或者我们会吗?)

不;导致问题的任务是您使用Task.Run自行旋转的任务。服务器上的任务是完全独立的;你的代码不能影响甚至检测它们。

如果您希望在不等待线程注入的情况下获得更高的并发性,那么您需要增加最小线程数,并且您还可能需要增加ServicePointManager.DefaultConnectionLimit。然后,您可以继续使用Task.Run,或(如我所愿)或Parallel或并行LINQ来执行并行处理。 Parallel/Parallel LINQ的一个很好的方面是它具有内置的节流支持,如果这也是需要的话。