【问题标题】:Typesafe fire-and-forget asynchronous delegate invocation in C#C# 中的类型安全即发即弃异步委托调用
【发布时间】:2011-02-16 15:08:17
【问题描述】:

我最近发现自己需要一种类型安全的“即发即弃”机制来异步运行代码。

理想情况下,我想做的是:

var myAction = (Action)(() => Console.WriteLine("yada yada"));
myAction.FireAndForget(); // async invocation

不幸的是,在没有相应 EndInvoke() 的情况下调用 BeginInvoke() 的明显选择不起作用 - 它会导致缓慢的资源泄漏(因为异步状态由运行时保持并且从未释放......它期待最终调用EndInvoke()。我也无法在.NET 线程池上运行代码,因为它可能需要很长时间才能完成(建议只在线程池上运行相对短暂的代码)——这使得它无法使用ThreadPool.QueueUserWorkItem()

最初,我只对签名匹配ActionAction<...>Func<...> 的方法需要此行为。所以我把一组扩展方法(见下面的清单)放在一起,让我在不遇到资源泄漏的情况下做到这一点。每个版本的 Action/Func 都有重载。

不幸的是,我现在想将此代码移植到 .NET 4,其中 Action 和 Func 的泛型参数数量已大大增加。在我编写 T4 脚本来生成这些之前,我还希望找到一种更简单更优雅的方法来执行此操作。欢迎任何想法。

public static class AsyncExt
{
    public static void FireAndForget( this Action action )
    {
        action.BeginInvoke(OnActionCompleted, action);
    }

    public static void FireAndForget<T1>( this Action<T1> action, T1 arg1 )
    {
        action.BeginInvoke(arg1, OnActionCompleted<T1>, action);
    }

    public static void FireAndForget<T1,T2>( this Action<T1,T2> action, T1 arg1, T2 arg2 )
    {
        action.BeginInvoke(arg1, arg2, OnActionCompleted<T1, T2>, action);
    }

    public static void FireAndForget<TResult>(this Func<TResult> func, TResult arg1)
    {
        func.BeginInvoke(OnFuncCompleted<TResult>, func);
    }

    public static void FireAndForget<T1,TResult>(this Func<T1, TResult> action, T1 arg1)
    {
        action.BeginInvoke(arg1, OnFuncCompleted<T1,TResult>, action);
    }

    // more overloads of FireAndForget<..>() for Action<..> and Func<..>

    private static void OnActionCompleted( IAsyncResult result )
    {
        var action = (Action)result.AsyncState;
        action.EndInvoke(result);
    }

    private static void OnActionCompleted<T1>( IAsyncResult result )
    {
        var action = (Action<T1>)result.AsyncState;
        action.EndInvoke( result );
    }

    private static void OnActionCompleted<T1,T2>(IAsyncResult result)
    {
        var action = (Action<T1,T2>)result.AsyncState;
        action.EndInvoke(result);
    }

    private static void OnFuncCompleted<TResult>( IAsyncResult result )
    {
        var func = (Func<TResult>)result.AsyncState;
        func.EndInvoke( result );
    }

    private static void OnFuncCompleted<T1,TResult>(IAsyncResult result)
    {
        var func = (Func<T1, TResult>)result.AsyncState;
        func.EndInvoke(result);
    }

    // more overloads of OnActionCompleted<> and OnFuncCompleted<>

}

【问题讨论】:

  • 很酷的问题。痛苦的代码!它们唯一的共同点是它们都是 MulticastDelegate。我不确定是否会有一种类型安全的方式来节食代码。 MS 首先是如何生成所有这些的?
  • @spender:编译器会自动为每个委托类型生成BeginInvoke/EndInvoke 方法,这些方法对委托的参数进行强类型化。
  • 当然。在我看来,它鼓励大量的样板代码,我很惊讶 .net4 没有引入一种更通用(或超通用)的方式来声明这些类型的结构......尽管我在大多数情况下都同意这一点代码,参数列表的长度不会超过5个左右。

标签: c# generics asynchronous delegates .net-4.0


【解决方案1】:

您可以将 EndInvoke 作为 AsyncCallback 传递给 BeginInvoke:

Action<byte[], int, int> action = // ...

action.BeginInvoke(buffer, 0, buffer.Length, action.EndInvoke, null);

这有帮助吗?

【讨论】:

  • 这是我考虑过的……与我一起工作的其他开发人员担心的是,很容易忘记传递action.EndInvoke - 结果是在代码。我们甚至考虑为此编写一个自定义的 FxCop 规则,但它花费的时间太长,而且代码仍有可能将其投入生产。但是,我正在再次考虑此选项的优点。
  • 至少你可以稍微简化一下 FireAndForget 的实现。
  • 代码的真实版本实际上在 OnAction/OnFunc 完成的方法中有更多的逻辑,我不想在问题中混淆(主要是一些记录需要多长时间才能完成异步调用) - 但原则上是的。
【解决方案2】:

那个聪明的 Skeet 小伙子接近这个主题 here

有一种不同的方法来“解雇并忘记”大约一半。

【讨论】:

  • 是的,我以前见过这种方法。主要问题是它失去了类型安全性(因此失去了智能感知和重构支持)——但它确实支持更多的委托签名。
【解决方案3】:

编译器生成的BeginInvoke方法也在线程池(reference)上被调用。所以我认为 ThreadPool.QueueUserWorkItem 会没问题,除了我猜你对它更明确一点(我想未来的 CLR 可以选择在 不同上运行 BeginInvoke'ed 方法> 线程池)。

【讨论】:

    【解决方案4】:

    怎么样:

    public static class FireAndForgetMethods
    {
        public static void FireAndForget<T>(this Action<T> act,T arg1)
        {
            var tsk = Task.Factory.StartNew( ()=> act(arg1),
                                             TaskCreationOptions.LongRunning);
        }
    }
    

    像这样使用它:

    Action<int> foo = (t) => { Thread.Sleep(t); };
    foo.FireAndForget(100);
    

    要添加类型安全,只需扩展辅助方法即可。 T4 在这里可能是最好的。

    【讨论】:

    • ContinueWith(...) 返回第二个 Task 对象,因此您仍然有一个未处理的 Task 。实际上,在这种情况下不需要处理原始任务对象,因为只有当任务需要执行阻塞等待时才需要处理。见this question
    【解决方案5】:

    我注意到没有人对此作出回应:

    我也无法在 .NET 线程池上运行代码,因为它可能需要很长时间才能完成(建议只在线程池上运行相对短暂的代码)——这使得它无法使用ThreadPool.QueueUserWorkItem()。

    我不确定你是否意识到这一点,但异步委托实际上就是这样做的——它们在 ThreadPool 的工作线程上排队工作,就像你在 QueueUserWorkItem 中所做的一样。

    异步委托行为不同的唯一情况是它们是特殊的框架委托,例如 Stream.BeginReadSocket.BeginSend。它们使用 I/O 完成端口。

    除非您在 ASP.NET 环境中处理数百个这样的任务,否则我建议您简单地使用线程池。

    ThreadPool.QueueUserWorkItem(s => action());
    

    或者,在 .NET 4 中,您可以使用任务工厂:

    Task.Factory.StartNew(action);
    

    (注意上面也会用到线程池!)

    【讨论】:

    • 确实,这是个问题。鉴于这一事实,我将不得不重新审视这种方法。 MSDN 上是否有任何明确的文档说明了这一点?
    • @LBushkin:你知道,这是一个很好的问题,而且我认为没有任何明确的文档,它可能是一个实现细节。当我为别人关于 ASP.NET 线程池饥饿的问题做一些实验时,我确定地发现了;我进入了一个异步回调,发现它确实占用了池中的一个工作线程。如果您找到任何文档,请告诉我,如果有可以链接的内容会很好。
    • 是的,我也试着找了一个参考,然后画了一个空白。
    【解决方案6】:

    您可以编写自己的线程池实现。这可能听起来像是穿着工作而不是实际工作。那么你就不必遵守“只运行相对短暂的代码”的建议。

    【讨论】:

      【解决方案7】:

      试试这个扩展方法(根据C# Is action.BeginInvoke(action.EndInvoke,null) a good idea?)以确保没有内存泄漏:

      public static void FireAndForget( this Action action )
      {
          action.BeginInvoke(action.EndInvoke, null);
      }
      

      您可以将其与泛型参数一起使用:

      T1 param1 = someValue;
      T2 param2 = otherValue;
      (() => myFunc<T1,T2>(param1,param2)).FireAndForget();
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-06-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多