簡體   English   中英

在C#中異步調用同步服務調用的策略

[英]Strategies for calling synchronous service calls asynchronously in C#

將業務邏輯封裝在同步服務調用之后,例如:

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

異步方式擴展/使用這些服務調用的最佳方法是什么?

目前,我已經創建了一個簡單的AsyncUtils類:

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);
            }
        }
    }
}

這讓我可以異步調用任何東西:

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

盡管這在簡單的用例中有效,但是如果其他進程需要協調結果,很快就會變得棘手,例如,如果我需要在當前線程可以繼續之前異步保存三個對象,那么我想一種等待/加入的方法工人線程。

到目前為止,我想到的選項包括:

  • 讓AsyncUtils返回WaitHandle
  • 讓AsyncUtils使用AsyncMethodCaller並返回IAsyncResult
  • 重寫API以包括Begin,End異步調用

例如類似的東西:

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#3.5和C#4的解決方案和想法(我們尚未升級,但會在不久的將來實現)。

期待您的想法。

考慮到您只需要保留.NET 2.0且不能在3.5或4.0上運行的要求,這可能是最佳選擇。

關於您當前的實現,我確實有三點評論。

  1. 您使用ThreadPool.UnsafeQueueUserWorkItem有特定原因嗎? 除非有特定的原因,否則我建議您改用ThreadPool.QueueUserWorkItem ,尤其是如果您在大型開發團隊中。 Unsafe版本可能會在您丟失調用堆棧時導致安全漏洞的出現,從而可能導致緊密控制權限的能力。

  2. 當前的異常處理設計(使用failureCallback )將吞沒所有異常,並且不提供反饋,除非定義了回調。 如果您無法正確處理異常,則最好傳播該異常並讓其冒泡。 或者,您可以以某種方式將其推回調用線程,盡管這將需要使用諸如IAsyncResult類的東西。

  3. 您目前無法判斷異步調用是否完成。 這將是在設計中使用IAsyncResult的另一個優點(盡管它確實為實現增加了一些復雜性)。


但是,一旦升級到.NET 4,我建議將其放在TaskTask<T> ,因為它設計得很干凈。 代替:

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

您可以使用內置工具,只需編寫:

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 );

當然,最后一行有些奇怪,但這主要是因為我試圖保留您現有的API。 如果您稍做修改,可以簡化它...

異步接口(基於IAsyncResult )僅在您進行了一些非阻塞調用時才有用。 接口的要點是使調用成為可能,而不會阻塞調用者線程。

  • 這在您可以進行一些系統調用並且系統會在發生某些情況(例如,何時收到HTTP響應或何時發生事件)時通知您的情況下很有用。

  • 使用基於IAsyncResult的接口的代價是您必須以某種尷尬的方式編寫代碼(通過使用回調進行每次調用)。 更糟糕的是,異步API使得無法使用whilefortry .. catch類的標准語言構造。

我真的看不出將同步 API封裝到異步接口中的意義,因為您將無法獲得好處(總是會有一些線程被阻塞),而您只會獲得更為尷尬的調用方式。

當然,以某種方式在后台線程上運行同步代碼是非常有意義的(以避免阻塞主應用程序線程)。 在.NET 4.0上使用Task<T>或在.NET 2.0上使用QueueUserWorkItem 但是,我不確定是否應該在服務中自動執行此操作-感覺在調用方執行此操作會更容易,因為您可能需要對服務執行多次調用。 使用異步API,您必須編寫如下內容:

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

使用同步API時,您將編寫如下內容:

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

以下是對里德后續問題的回應。 我並不是說這是正確的方法。

    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;
    }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM