简体   繁体   English

在C#中异步调用同步服务调用的策略

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

With business logic encapsulated behind synchronous service calls eg: 将业务逻辑封装在同步服务调用之后,例如:

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

What is the best way to extend/use these service calls in an asynchronous fashion? 异步方式扩展/使用这些服务调用的最佳方法是什么?

At present I've created a simple AsyncUtils class: 目前,我已经创建了一个简单的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);
            }
        }
    }
}

Which lets me call anything asynchronously: 这让我可以异步调用任何东西:

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

Whilst this works in simple use cases it quickly gets tricky if other processes need to coordinate about the results, for example if I need to save three objects asynchronously before the current thread can continue then I'd like a way to wait-on/join the worker threads. 尽管这在简单的用例中有效,但是如果其他进程需要协调结果,很快就会变得棘手,例如,如果我需要在当前线程可以继续之前异步保存三个对象,那么我想一种等待/加入的方法工人线程。

Options I've thought of so far include: 到目前为止,我想到的选项包括:

  • having AsyncUtils return a WaitHandle 让AsyncUtils返回WaitHandle
  • having AsyncUtils use an AsyncMethodCaller and return an IAsyncResult 让AsyncUtils使用AsyncMethodCaller并返回IAsyncResult
  • rewriting the API to include Begin, End async calls 重写API以包括Begin,End异步调用

eg something resembling: 例如类似的东西:

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

Are there other approaches I should consider? 我还应该考虑其他方法吗? What are the benefits and potential pitfalls of each? 每种方案都有哪些好处和潜在的陷阱?

Ideally I'd like to keep the service layer simple/synchronous and provide some easy to use utility methods for calling them asynchronously. 理想情况下,我想使服务层保持简单/同步,并提供一些易于使用的实用程序方法来异步调用它们。 I'd be interested in hearing about solutions and ideas applicable to C# 3.5 and C# 4 (we haven't upgraded yet but will do in the relatively near future). 我想听听适用于C#3.5和C#4的解决方案和想法(我们尚未升级,但会在不久的将来实现)。

Looking forward to your ideas. 期待您的想法。

Given your requirement to stay .NET 2.0 only, and not work on 3.5 or 4.0, this is probably the best option. 考虑到您只需要保留.NET 2.0且不能在3.5或4.0上运行的要求,这可能是最佳选择。

I do have three remarks on your current implementation. 关于您当前的实现,我确实有三点评论。

  1. Is there a specific reason you're using ThreadPool.UnsafeQueueUserWorkItem ? 您使用ThreadPool.UnsafeQueueUserWorkItem有特定原因吗? Unless there is a specific reason this is required, I would recommend using ThreadPool.QueueUserWorkItem instead, especially if you're in a large development team. 除非有特定的原因,否则我建议您改用ThreadPool.QueueUserWorkItem ,尤其是如果您在大型开发团队中。 The Unsafe version can potentially allow security flaws to appear as you lose the calling stack, and as a result, the ability to control permissions as closely. Unsafe版本可能会在您丢失调用堆栈时导致安全漏洞的出现,从而可能导致紧密控制权限的能力。

  2. The current design of your exception handling, using the failureCallback , will swallow all exceptions, and provide no feedback, unless a callback is defined. 当前的异常处理设计(使用failureCallback )将吞没所有异常,并且不提供反馈,除非定义了回调。 It might be better to propogate the exception and let it bubble up if you're not going to handle it properly. 如果您无法正确处理异常,则最好传播该异常并让其冒泡。 Alternatively, you could push this back onto the calling thread in some fashion, though this would require using something more like IAsyncResult . 或者,您可以以某种方式将其推回调用线程,尽管这将需要使用诸如IAsyncResult类的东西。

  3. You currently have no way to tell if an asynchronous call is completed. 您目前无法判断异步调用是否完成。 This would be the other advantage of using IAsyncResult in your design (though it does add some complexity to the implementation). 这将是在设计中使用IAsyncResult的另一个优点(尽管它确实为实现增加了一些复杂性)。


Once you upgrade to .NET 4, however, I would recommend just putting this in a Task or Task<T> , as it was designed to handle this very cleanly. 但是,一旦升级到.NET 4,我建议将其放在TaskTask<T> ,因为它设计得很干净。 Instead of: 代替:

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

You can use the built-in tools and just write: 您可以使用内置工具,只需编写:

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

Granted, the last line there is a bit odd, but that's mainly because I tried to keep your existing API. 当然,最后一行有些奇怪,但这主要是因为我试图保留您现有的API。 If you reworked it a bit, you could simplify it... 如果您稍做修改,可以简化它...

Asynchronous interface (based on IAsyncResult ) is useful only when you have some non-blocking call under the cover. 异步接口(基于IAsyncResult )仅在您进行了一些非阻塞调用时才有用。 The main point of the interface is to make it possible to do the call without blocking the caller thread. 接口的要点是使调用成为可能,而不会阻塞调用者线程。

  • This is useful in scenarios when you can make some system call and the system will notify you back when something happens (eg when a HTTP response is received or when an event happens). 这在您可以进行一些系统调用并且系统会在发生某些情况(例如,何时收到HTTP响应或何时发生事件)时通知您的情况下很有用。

  • The price for using IAsyncResult based interface is that you have to write code in a somewhat awkward way (by making every call using callback). 使用基于IAsyncResult的接口的代价是您必须以某种尴尬的方式编写代码(通过使用回调进行每次调用)。 Even worse, asynchronous API makes it impossible to use standard language constructs like while , for , or try .. catch . 更糟糕的是,异步API使得无法使用whilefortry .. catch类的标准语言构造。

I don't really see the point of wrapping synchronous API into asynchronous interface, because you won't get the benefit (there will always be some thread blocked) and you'll just get more awkward way of calling it. 我真的看不出将同步 API封装到异步接口中的意义,因为您将无法获得好处(总是会有一些线程被阻塞),而您只会获得更为尴尬的调用方式。

Of course, it makes a perfect sense to run the synchronous code on a background thread somehow (to avoid blocking the main application thread). 当然,以某种方式在后台线程上运行同步代码是非常有意义的(以避免阻塞主应用程序线程)。 Either using Task<T> on .NET 4.0 or using QueueUserWorkItem on .NET 2.0. 在.NET 4.0上使用Task<T>或在.NET 2.0上使用QueueUserWorkItem However, I'm not sure if this should be done automatically in the service - it feels like doing this on the caller side would be easier, because you may need to perform multiple calls to the service. 但是,我不确定是否应该在服务中自动执行此操作-感觉在调用方执行此操作会更容易,因为您可能需要对服务执行多次调用。 Using asynchronous API, you'd have to write something like: 使用异步API,您必须编写如下内容:

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

When using synchronous API, you'd write something like: 使用同步API时,您将编写如下内容:

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

The following is a response to Reed's follow-up question. 以下是对里德后续问题的回应。 I'm not suggesting that it's the right way to go. 我并不是说这是正确的方法。

    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