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

发布于 2024-09-24 22:31:43 字数 2239 浏览 2 评论 0原文

将业务逻辑封装在同步服务调用后面,例如:

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 的解决方案和想法(我们尚未升级,但会在不久的将来升级)。

期待您的想法。

With business logic encapsulated behind synchronous service calls e.g.:

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:

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
  • having AsyncUtils use an AsyncMethodCaller and return an IAsyncResult
  • rewriting the API to include Begin, End async calls

e.g. 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).

Looking forward to your ideas.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

回忆躺在深渊里 2024-10-01 22:31:43

鉴于您要求仅保留 .NET 2.0,而不在 3.5 或 4.0 上工作,这可能是最好的选择。

我对你们目前的实施有三点评论。

  1. 您使用 ThreadPool 是否有特定原因.UnsafeQueueUserWorkItem?除非有特殊原因需要这样做,否则我建议使用 ThreadPool.QueueUserWorkItem 相反,特别是如果您在大型开发团队中。当您丢失调用堆栈时,不安全版本可能会出现安全缺陷,从而导致无法严格控制权限。

  2. 当前的异常处理设计使用failureCallback,将吞掉所有异常,并且不提供任何反馈,除非定义了回调。如果您不打算正确处理异常,最好传播异常并让它冒泡。或者,您可以以某种方式将其推回调用线程,尽管这需要使用更像 IAsyncResult 的东西。

  3. 您目前无法判断异步调用是否已完成。这将是在设计中使用 IAsyncResult 的另一个优点(尽管它确实增加了实现的一些复杂性)。


但是,一旦升级到 .NET 4,我建议将其放入 任务任务< /code>,因为它的设计目的是非常干净地处理这个问题。而不是:

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。如果你稍微修改一下,你可以简化它......

Given your requirement to stay .NET 2.0 only, and not work on 3.5 or 4.0, this is probably the best option.

I do have three remarks on your current implementation.

  1. Is there a specific reason you're using 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. 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.

  2. The current design of your exception handling, using the failureCallback, will swallow all exceptions, and provide no feedback, unless a callback is defined. 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.

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


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. 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. If you reworked it a bit, you could simplify it...

美人骨 2024-10-01 22:31:43

异步接口(基于 IAsyncResult)仅当您在幕后有一些非阻塞调用时才有用。该接口的要点是使得可以在不阻塞调用者线程的情况下进行调用。

  • 这在以下场景中很有用:您可以进行一些系统调用,并且系统会在发生某些情况时通知您(例如,当收到 HTTP 响应时或发生事件时)。

  • 使用基于 IAsyncResult 的接口的代价是您必须以一种有点尴尬的方式编写代码(通过使用回调进行每次调用)。更糟糕的是,异步 API 使得无法使用标准语言结构,例如 whilefortry..catch.

我真的不明白将同步 API包装到异步接口中的意义,因为你不会获得好处(总会有一些线程被阻塞)并且你'只会得到更尴尬的称呼方式。

当然,以某种方式在后台线程上运行同步代码是非常有意义的(以避免阻塞主应用程序线程)。在 .NET 4.0 上使用 Task 或在 .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);
});

Asynchronous interface (based on IAsyncResult) is useful only when you have some non-blocking call under the cover. 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 (e.g. when a HTTP response is received or when an event happens).

  • 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). Even worse, asynchronous API makes it impossible to use standard language constructs like while, for, or try..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.

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. 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:

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

When using synchronous API, you'd write something like:

ThreadPool.QueueUserWorkItem(() => {
  var foo = svc.GetFooId();
  foo.Prop = 123;
  svc.SaveFoo(foo);
});
森林散布 2024-10-01 22:31:43

以下是对里德后续问题的回应。我并不是说这是正确的做法。

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

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;
    }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文