2010-09-28 9 views
7

Với logic kinh doanh đóng gói đằng sau đồng bộ dịch vụ gọi ví dụ .:chiến lược để gọi dịch vụ đồng bộ gọi không đồng bộ trong C#

interface IFooService 
{ 
    Foo GetFooById(int id); 
    int SaveFoo(Foo foo); 
} 

cách tốt nhất để mở rộng/sử dụng các cuộc gọi dịch vụ trong một không đồng bộ thời trang là gì?

Hiện nay tôi đã tạo ra một AsyncUtils lớp đơn giản:

public static class AsyncUtils 
{ 
    public static void Execute<T>(Func<T> asyncFunc) 
    { 
     Execute(asyncFunc, null, null); 
    } 

    public static void Execute<T>(Func<T> asyncFunc, Action<T> successCallback) 
    { 
     Execute(asyncFunc, successCallback, null); 
    } 

    public static void Execute<T>(Func<T> asyncFunc, Action<T> successCallback, Action<Exception> failureCallback) 
    { 
     ThreadPool.UnsafeQueueUserWorkItem(state => ExecuteAndHandleError(asyncFunc, successCallback, failureCallback), null); 
    } 

    private static void ExecuteAndHandleError<T>(Func<T> asyncFunc, Action<T> successCallback, Action<Exception> failureCallback) 
    { 
     try 
     { 
      T result = asyncFunc(); 
      if (successCallback != null) 
      { 
       successCallback(result); 
      } 
     } 
     catch (Exception e) 
     { 
      if (failureCallback != null) 
      { 
       failureCallback(e); 
      } 
     } 
    } 
} 

nào cho phép tôi gọi bất cứ điều gì không đồng bộ:

AsyncUtils(
    () => _fooService.SaveFoo(foo), 
    id => HandleFooSavedSuccessfully(id), 
    ex => HandleFooSaveError(ex)); 

Trong khi công trình này trong trường hợp sử dụng đơn giản, nó nhanh chóng trở nên phức tạp nếu quá trình khác cần phải phối hợp về các kết quả, ví dụ nếu tôi cần lưu ba đối tượng không đồng bộ trước khi luồng hiện tại có thể tiếp tục thì tôi muốn một cách để đợi/tham gia các chuỗi công việc.

Tùy chọn Tôi đã nghĩ đến việc cho đến nay bao gồm:

  • có AsyncUtils trả về một WaitHandle
  • có AsyncUtils sử dụng một AsyncMethodCaller và trả về một IAsyncResult
  • viết lại API để bao gồm Begin, cuộc gọi End async

ví dụ một cái gì đó tương tự:

interface IFooService 
{ 
    Foo GetFooById(int id); 
    IAsyncResult BeginGetFooById(int id); 
    Foo EndGetFooById(IAsyncResult result); 
    int SaveFoo(Foo foo); 
    IAsyncResult BeginSaveFoo(Foo foo); 
    int EndSaveFoo(IAsyncResult result); 
} 

Có phương pháp nào khác mà tôi nên cân nhắc không? Những lợi ích và cạm bẫy tiềm năng của mỗi loại là gì?

Lý tưởng nhất là tôi muốn giữ cho lớp dịch vụ đơn giản/đồng bộ và cung cấp một số phương thức tiện ích dễ sử dụng để gọi chúng không đồng bộ. Tôi muốn được nghe về các giải pháp và ý tưởng áp dụng cho C# 3.5 và C# 4 (chúng tôi chưa nâng cấp nhưng sẽ thực hiện trong tương lai gần tương lai).

Mong ý tưởng của bạn.

+3

Tại sao bạn không sử dụng Thư viện song song công việc? Nó đã được thực hiện cho việc này. http://msdn.microsoft.com/en-us/library/dd460717.aspx –

+0

Những gì John nói, nhưng cụ thể, hãy thử 'Lazy '. –

+1

@Steven: Đó phải là Nhiệm vụ , đối với loại hoạt động này. Lazy là để khởi tạo. –

Trả lời

3

Với yêu cầu của bạn để duy trì .NET 2.0 chỉ và không hoạt động trên 3.5 hoặc 4.0, đây có lẽ là lựa chọn tốt nhất.

Tôi có ba nhận xét về việc triển khai hiện tại của bạn.

  1. Có lý do cụ thể nào bạn đang sử dụng ThreadPool.UnsafeQueueUserWorkItem không? Trừ khi có một lý do cụ thể này là bắt buộc, tôi khuyên bạn nên sử dụng ThreadPool.QueueUserWorkItem thay vào đó, đặc biệt nếu bạn đang ở trong một nhóm phát triển lớn. Phiên bản không an toàn có khả năng cho phép các lỗi bảo mật xuất hiện khi bạn mất ngăn xếp cuộc gọi và do đó, khả năng kiểm soát quyền càng chặt chẽ.

  2. Thiết kế hiện tại về xử lý ngoại lệ của bạn, sử dụng failureCallback, sẽ nuốt tất cả ngoại lệ và không cung cấp phản hồi, trừ khi xác định lại cuộc gọi lại. Nó có thể là tốt hơn để propogate ngoại lệ và để cho nó bong bóng lên nếu bạn sẽ không xử lý nó đúng cách. Ngoài ra, bạn có thể đẩy điều này trở lại vào chuỗi gọi trong một số thời trang, mặc dù điều này sẽ yêu cầu sử dụng một cái gì đó giống như IAsyncResult.

  3. Bạn hiện không có cách nào để biết liệu cuộc gọi không đồng bộ đã hoàn thành chưa. Đây sẽ là lợi thế khác của việc sử dụng IAsyncResult trong thiết kế của bạn (mặc dù nó có thêm một số phức tạp để thực hiện).


Khi bạn nâng cấp lên NET 4, tuy nhiên, tôi muốn giới thiệu chỉ cần đặt này trong Task hoặc Task<T>, vì nó được thiết kế để xử lý này rất sạch sẽ. Thay vì:

AsyncUtils(
    () => _fooService.SaveFoo(foo), 
    id => HandleFooSavedSuccessfully(id), 
    ex => HandleFooSaveError(ex)); 

Bạn có thể sử dụng các công cụ tích hợp và chỉ cần viết:

var task = Task.Factory.StartNew( 
       () => return _fooService.SaveFoo(foo)); 
task.ContinueWith( 
       t => HandleFooSavedSuccessfully(t.Result), 
        TaskContinuationOptions.NotOnFaulted); 
task.ContinueWith( 
       t => try { t.Wait(); } catch(Exception e) { HandleFooSaveError(e); }, 
        TaskContinuationOptions.OnlyOnFaulted); 

Cấp, dòng cuối cùng có một chút kỳ lạ, nhưng đó là chủ yếu vì tôi đã cố gắng để giữ hiện tại của bạn API. Nếu bạn đã chỉnh sửa lại một chút, bạn có thể đơn giản hóa nó ...

+0

Cảm ơn Reed, chúng tôi hiện đang sử dụng 3.5 nhưng hy vọng sẽ nâng cấp lên 4. Lời khuyên của bạn có khác biệt không? – chillitom

+1

@Chillitom: Nếu bạn có thể làm điều đó, tôi sẽ nhận được khung Rx (cho .NET 3.5), vì nó bao gồm một backport của Task Parallel Lib từ .NET 4. Sau đó bạn có thể sử dụng Task/Task và nhận tất cả những lợi thế mà họ cung cấp. Đó là bản tải xuống miễn phí tại: http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx –

+0

@Chillitom: Tôi đặt một phiên bản TPL cho mã của bạn - nó không hoàn toàn gọn gàng (mặc dù bạn có thể dễ dàng thực hiện một phương pháp để làm những gì bạn làm), nhưng nó sử dụng tất cả các công cụ khung, cộng với nó cung cấp cho bạn tất cả những lợi thế của việc có thể chờ đợi (task.Wait()), truy vấn để hoàn thành, và bỏ qua các phần bạn không cần ... –

2

Giao diện không đồng bộ (dựa trên IAsyncResult) chỉ hữu ích khi bạn có một số cuộc gọi không chặn dưới nắp. Điểm chính của giao diện là để thực hiện cuộc gọi mà không chặn luồng người gọi.

  • này rất hữu ích trong các tình huống khi bạn có thể thực hiện một số cuộc gọi hệ thống và hệ thống sẽ thông báo cho bạn trở lại khi một cái gì đó xảy ra (ví dụ khi một phản ứng HTTP được nhận hoặc khi một sự kiện xảy ra).

  • Giá để sử dụng giao diện dựa trên IAsyncResult là bạn phải viết mã theo cách hơi khó xử (bằng cách thực hiện mọi cuộc gọi bằng gọi lại). Thậm chí tệ hơn, API không đồng bộ khiến không thể sử dụng các cấu trúc ngôn ngữ chuẩn như while, for hoặc try .. catch.

tôi không thực sự nhìn thấy điểm của gói đồng bộ API vào không đồng bộ giao diện, bởi vì bạn sẽ không nhận được lợi ích (sẽ luôn có một số chủ đề bị chặn) và bạn sẽ chỉ nhận được một cách khó xử hơn khi gọi nó.

Tất nhiên, nó có ý nghĩa hoàn hảo để chạy mã đồng bộ trên một chuỗi nền bằng cách nào đó (để tránh chặn luồng ứng dụng chính). Hoặc sử dụng Task<T> trên .NET 4.0 hoặc sử dụng QueueUserWorkItem trên .NET 2.0. Tuy nhiên, tôi không chắc liệu điều này có nên được thực hiện tự động trong dịch vụ hay không - cảm giác như làm điều này ở phía người gọi sẽ dễ dàng hơn, bởi vì bạn có thể cần phải thực hiện nhiều cuộc gọi đến dịch vụ. Sử dụng API không đồng bộ, bạn phải viết một cái gì đó như:

svc.BeginGetFooId(ar1 => { 
    var foo = ar1.Result; 
    foo.Prop = 123; 
    svc.BeginSaveFoo(foo, ar2 => { 
    // etc... 
    } 
}); 

Khi sử dụng API đồng bộ, bạn muốn viết một cái gì đó như:

ThreadPool.QueueUserWorkItem(() => { 
    var foo = svc.GetFooId(); 
    foo.Prop = 123; 
    svc.SaveFoo(foo); 
}); 
+0

"Tôi không thực sự thấy điểm gói API đồng bộ vào giao diện không đồng bộ," Nếu trình bao bọc của bạn đẩy công việc lên một chuỗi nền, sử dụng IAsyncResult và mẫu async chuẩn có thể cung cấp cho bạn cách kiểm tra xem hoạt động có hoàn thành mà không bị chặn. Có nhiều lý do để làm điều này vì bạn đang tạo API không đồng bộ một cách hiệu quả. –

+1

@Reed: Bạn nói đúng là kiểm tra xem hoạt động đã hoàn thành có phải là lợi ích của việc sử dụng 'IAsyncresult' hay không, nhưng tôi nghĩ rằng nhược điểm của mẫu (lập trình dựa trên gọi lại) vẫn còn quan trọng hơn. –

+0

Trừ khi bạn có thể sử dụng .NET 4, mặc dù, thường không có lựa chọn thay thế tốt hơn. IAsyncResult thực sự là cách duy nhất trong .NET 2.0 cung cấp cho bạn một cách để cho biết liệu một hoạt động async có hoàn thành hay không, và cũng để đẩy lùi các lỗi về chuỗi đang gọi ... –

1

Sau đây là một phản ứng để theo dõi câu hỏi của Reed . Tôi không cho rằng đó là cách đi đúng đắn.

public static int PerformSlowly(int id) 
    { 
     // Addition isn't so hard, but let's pretend. 
     Thread.Sleep(10000); 
     return 42 + id; 
    } 

    public static Task<int> PerformTask(int id) 
    { 
     // Here's the straightforward approach. 
     return Task.Factory.StartNew(() => PerformSlowly(id)); 
    } 

    public static Lazy<int> PerformLazily(int id) 
    { 
     // Start performing it now, but don't block. 
     var task = PerformTask(id); 

     // JIT for the value being checked, block and retrieve. 
     return new Lazy<int>(() => task.Result); 
    } 

    static void Main(string[] args) 
    { 
     int i; 

     // Start calculating the result, using a Lazy<int> as the future value. 
     var result = PerformLazily(7); 

     // Do assorted work, then get result. 
     i = result.Value; 

     // The alternative is to use the Task as the future value. 
     var task = PerformTask(7); 

     // Do assorted work, then get result. 
     i = task.Result; 
    } 
+0

Có thể cho rằng, sử dụng Lazy có thể có ý nghĩa hơn nếu việc triển khai đã vượt quá hàng đợi của luồng, chứ không phải Task. Nhiệm vụ chỉ là quá sạch sẽ đã được hưởng lợi từ gói thêm. –

+1

@Steven: Vấn đề với Lazy là nó chặn ngay khi bạn cố gắng lấy giá trị. Tại thời điểm đó, nhiệm vụ thực hiện chính xác điều tương tự (cộng với cung cấp các lợi ích khác) ... Lazy thực sự là chỉ có cho instantiation lười an toàn trong một môi trường đa luồng. –

+0

Nhưng cảm ơn vì đã cho tôi biết bạn đang nghĩ gì;) –