繁体   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