5

Tôi có một ứng dụng web ASP.NET MVC làm cho các cuộc gọi dịch vụ web kiểu REST trở thành các máy chủ khác. Tôi có một kịch bản mà tôi đang thực hiện hai cuộc gọi HttpWebRequest đến hai dịch vụ riêng biệt. Tôi cần cả hai để hoàn thành để tiếp tục, nhưng thứ tự của họ không quan trọng. Họ có thể mất 1-2 giây và tôi đang chạy chúng theo trình tự bây giờ. Chạy song song sẽ làm giảm thời gian phản hồi của người dùng, nhưng cách tốt nhất là gì?Các khuyến nghị để thực hiện .NET HttpWebRequests song song trong ASP.NET

Trong nghiên cứu này, tôi có thể nghĩ đến một vài lựa chọn:

  • Execute một yêu cầu về các chủ đề chính, và quay lên một sợi thứ hai cho các yêu cầu khác. Nên tạo chủ đề mới hoặc sử dụng một hồ bơi thread? Nếu tôi sử dụng một hồ bơi, làm thế nào để tôi kích thước nó? Ngoài ra, không chắc chắn làm thế nào tôi có thể tham gia các chủ đề lại với nhau (ví dụ: sử dụng ThreadPool.RegisterWaitForSingleObject)?
  • Hãy thử và tận dụng hỗ trợ IAsyncResult được tích hợp sẵn cho một hoặc cả hai yêu cầu. Một lần nữa, không chắc chắn về chủ đề mà yêu cầu async thực hiện, vì vậy không chắc chắn làm thế nào để kích thước hồ bơi thread. Làm thế nào để tôi tham gia IAsyncResult trở lại chủ đề chính của tôi? Tất cả các ví dụ tôi tìm thấy thông tin quá trình trong gọi lại, nhưng tôi chỉ có thể chờ đợi trong chủ đề chính của tôi và sử dụng tài sản IsCompleted?

Tôi cần tìm giải pháp vừa hoạt động vừa thực hiện trên quy mô lớn. Đó là lý do tại sao tôi lo lắng về kích thước hồ bơi thread. Tôi ghét phải yêu cầu chặn vì họ đang chờ các chủ đề có sẵn.

Trả lời

1

Một trong các câu trả lời cho Multithreading WebRequests, a good and stable approach? : CSharp sử dụng ManualResetEvent event = new ManualResetEvent(), bộ đếm tham chiếu bằng số yêu cầu chuyến bay và Interlocked.Decrement để kiểm soát số event.Set(). Các chủ đề chính sau đó chờ đợi bằng cách gọi event.WaitOne().

Tuy nhiên, WaitHandles - Auto/ManualResetEvent and Mutex đề cập rằng ManualResetEvent "có thể chậm hơn đáng kể so với sử dụng các phương pháp theo dõi khác nhau" như Wait, Pulse và PulseAll.

Tôi đã ngừng cập nhật mã của mình cho bài đăng trên blog của Noah Blumenthal này: Run generic tasks async (fluent-ly). Tôi đã thực hiện hai thay đổi: triển khai IDisposable và gọi .Close() trên số ManualResetEvent và chuyển từ sử dụng lock() thành Interlocked.Increment().Decrement().

public class AsyncQueueManager : IDisposable { 
    private readonly ManualResetEvent waitHandle = new ManualResetEvent(true); 
    private int count = 0; 

    public AsyncQueueManager Queue(Action<object> action) { 
     Interlocked.Increment(ref count); 
     waitHandle.Reset(); 
     Action<object> actionWrapper = CreateActionWrapper(action); 
     WaitCallback waitCallback = new WaitCallback(actionWrapper); 
     ThreadPool.QueueUserWorkItem(waitCallback); 
     return this; 
    } 

    private Action<object> CreateActionWrapper(Action<object> action) { 
     Action<object> actionWrapper = (object state) => 
     { 
      try { 
       action(state); 
      } catch (Exception ex) { 
       // log 
      } finally { 
       if (Interlocked.Decrement(ref count) == 0) { 
        waitHandle.Set(); 
       } 
      } 
     }; 
     return actionWrapper; 
    } 

    public void Wait() { 
     waitHandle.WaitOne(); 
    } 
    public void Wait(TimeSpan timeout) { 
     waitHandle.WaitOne(timeout); 
    } 

    public void Dispose() { 
     waitHandle.Close(); 
    } 
} 
-2

Trừ khi bạn đang làm điều gì đó ưa thích kích thước của ThreadPool là cố định và thường đủ lớn để đáp ứng nhu cầu của bạn. Trong trường hợp cụ thể của bạn, bạn có thể chạy vào các vấn đề ràng buộc tài nguyên bằng cách sử dụng các cuộc gọi AsyncIO nếu dịch vụ web của bạn bị tải nặng và mỗi người gọi đã khởi tạo 2 chủ đề TP cho các cuộc gọi lại của riêng nó.

Có lẽ việc thực hiện dễ dàng nhất này là có hai chức năng gọi lại riêng biệt, một cho service1 và một cho service2 bên trong mỗi CompletionEvents đặt một số biến kích hoạt thành "true" và sau đó trong chủ đề chính của bạn chờ cho cả hai các biến kích hoạt được thiết lập. Bạn có thể làm điều này với ResetEvents, nhưng nếu máy chủ của bạn sẽ được tải xuống, bạn có thể muốn tránh điều này.

Pseudo code của quá trình này có thể là:

Dictionary<string, bool> callCompleted = new Dictionary<string, bool> 

string operation1Key = Guid.NewGuid().ToString(); 
string operation2Key = Guid.NewGuid().ToString(); 

callCompleted[operation1Key] = false; 
callCompleted[operation2Key] = false; 

//make your remote calls and pass the operation key as the data 
//.... 
//.... 

bool waiting = true; 
while (waiting) { 
    bool isFinished = true; 
    foreach(string operationKey in callCompleted.Keys) { 
     isFinished &= callCompleted[operationKey] 
     if (!isFinished) { break; } 
    } 

    waiting = !isFinished; 
} 

Its a rought chút kể từ khi tôi không biết bản chất chính xác như thế nào bạn đang thực hiện cuộc gọi của bạn, nhưng nó phải hoạt động khá tốt.

+0

Hmmm, tôi nghĩ việc chờ đợi mà bạn có trong mã giả (trong khi (đúng)) chỉ phù hợp với thời gian chờ đợi rất ngắn. Tôi nghĩ rằng bạn muốn sử dụng một số loại semaphore thay thế. – Mike

+0

Ông đã nói đây là ứng dụng ASP.NET MVC, vì vậy tôi sẽ giả định rằng thời gian chờ đợi nói chung sẽ khá ngắn. Khi bạn nói semaphore tôi không chắc chắn bối cảnh bạn đang đề cập đến như một semaphore sẽ hoạt động chính xác như một spin-lock (thời gian CPU sans có thể được khắc phục trong mã nhanh ở trên) và vẫn yêu cầu một vòng lặp tương tự như ở trên để phát hành khóa vì chúng ta đang xử lý nhiều luồng. – GrayWizardx

1

Tôi thích làm điều này một cách thủ công hơn, thay vì dựa vào yêu cầu web không đồng bộ hoặc kích thước tự động của hồ bơi của chuỗi (25 chủ đề theo mặc định). Tất nhiên, đó là những cách hoàn hảo để giải quyết vấn đề của bạn, nhưng tôi nghĩ đoạn mã sau dễ đọc hơn một chút (trong ví dụ dưới đây, _links sẽ chứa danh sách các liên kết của bạn trước khi xử lý xảy ra ...):

private static IList<String> _links = new List<String>(); 
private const int NumberOfThreads = 2; 

public void SpawnWebRequests() 
{ 
    IList<Thread> threadList = new List<Thread>(); 

    for (int i = 0; i < NumberOfThreads; i++) 
    { 
     var thread = new Thread(ProcessWebRequests); 
     threadList.Add(thread); 
     thread.Start(); 
    } 

    for (int i = 0; i < NumberOfThreads; i++) 
    { 
     threadList[i].Join(); 
    } 
} 

private static void ProcessWebRequests() 
{ 
    String link; 

    while (true) 
    { 
     lock(_links) 
     { 
      if (_links.Count == 0) 
       break; 

      link = _links.RemoveAt(0); 
     } 

     ProcessWebRequest(link); 
    } 
} 

private static void ProcessWebRequest(String link) 
{ 
    try 
    { 
     var request = (HttpWebRequest)WebRequest.Create(link); 
     request.Method = "HEAD"; // or "GET", since some sites (Amazon) don't allow HEAD 
     request.Timeout = DefaultTimeoutSeconds * 1000; 

     // Get the response (throws an exception if status != 200) 
     using (var response = (HttpWebResponse)request.GetResponse()) 
     { 
      if (response.StatusCode == HttpStatusCode.OK) 
       Log.Debug("Working link: {0}", request.RequestUri); 
     } 
    } 
    catch (WebException ex) 
    { 
     var response = ((HttpWebResponse)ex.Response); 
     var status = response != null 
         ? response.StatusCode 
         : HttpStatusCode.RequestTimeout; 

     Log.WarnException(String.Format("Broken link ({0}): {1}", status, link), ex); 

     // Don't rethrow, as this is an expected exception in many cases 
    } 
    catch (Exception ex) 
    { 
     Log.ErrorException(String.Format("Error processing link {0}", link), ex); 

     // Rethrow, something went wrong 
     throw; 
    } 
} 

Nếu bạn chỉ muốn quản lý kích thước của nhóm chủ đề (nếu bạn đang sử dụng ThreadPool.QueueUserWorkItem()), bạn có thể sử dụng ThreadPool.SetMaxThreads = 2).

Tất nhiên, nếu bạn muốn sử dụng cách tiếp cận không đồng bộ do Microsoft xử lý, hãy xem ví dụ sau: http://msdn.microsoft.com/en-us/library/86wf6409.aspx. Chỉ cần chắc chắn rằng bạn làm sạch từng phản ứng (thông qua một khối "sử dụng" hoặc bằng cách đóng đối tượng phản hồi)!

Hy vọng rằng sẽ giúp, Noah

+0

Bên trong vòng lặp 'while (true)' bạn cần phải có độ trễ. Vòng lặp như được viết sẽ ăn lên 100% cpu khi không có gì trong danh sách. Tôi sẽ khuyên bạn nên sử dụng một 'AutoResetEvent' để chờ đợi và báo hiệu khi một cái gì đó đã sẵn sàng để được xử lý. –

0

tôi khuyên bạn nên tạo một lớp người lao động mà không được HttpWebRequest và bắt đầu nó trong chủ đề riêng của mình cho mỗi kết nối. Bạn chỉ có thể tham gia các chủ đề và chờ cho đến khi cả hai kết thúc hoặc chuyển một phương thức gọi lại. Trong cả hai trường hợp, bạn cần tính đến lỗi kết nối, thời gian chờ và các ngoại lệ khác. Tôi thích sử dụng một cuộc gọi lại trả về kết quả kết nối và ManagedThreadId của chuỗi, mà tôi sử dụng để theo dõi các luồng. Lớp nhân viên sẽ bắt tất cả các ngoại lệ để bạn có thể xử lý chúng trong lớp gọi.

Bài viết này cung cấp một số thông tin chi tiết và bản sửa lỗi khi bạn vượt quá số lượng kết nối tối đa: http://support.microsoft.com/kb/821268.

Các vấn đề liên quan