C#中的安全的异步委托调用方式:fire-and-forget(发出请求后即忘记)模式

13

我最近发现自己需要一个类型安全的异步“打火机”机制。

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

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

很遗憾,显而易见的选择——调用没有相应 EndInvoke()BeginInvoke() 不起作用——它会导致缓慢的资源泄漏(因为异步状态由运行时持有且从不释放...期望最终调用 EndInvoke())。我也不能在.NET线程池上运行代码,因为它可能需要很长时间才能完成(建议只在线程池上运行相对较短的代码),这使得使用 ThreadPool.QueueUserWorkItem() 成为不可能。

最初,我只需要对匹配 Action, Action<...>, 或 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。我不确定是否存在类型安全的方式来对该代码进行修剪。微软最初是如何生成所有这些代码的呢? - spender
1
@spender:编译器会自动为每个委托类型生成BeginInvoke/EndInvoke方法,这些方法是针对委托参数进行强类型化的。 - LBushkin
当然。在我看来,这似乎鼓励了相当多的样板代码,我很惊讶.NET4没有引入更通用(或超级通用)的声明这些构造的方式...尽管我同意,在大多数理智的代码中,参数列表不会超过5个左右。 - spender
7个回答

8
您可以将EndInvoke作为BeginInvoke的AsyncCallback参数传递:
Action<byte[], int, int> action = // ...

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

这有帮助吗?

这是我考虑过的一个问题……与我一起工作的其他开发人员担心会很容易忘记传递 action.EndInvoke 参数,导致代码内部出现静默泄漏。我们甚至考虑编写自定义 FxCop 规则来解决这个问题,但它太耗时了,并且存在代码仍可能进入生产环境的风险。然而,我再次考虑了这个选项的优点。 - LBushkin
至少你可以简化一下FireAndForget的实现。 - dtb
实际代码版本在OnAction/OnFunc完成方法中有一些更多的逻辑,我不想将其混杂到问题中(主要是记录异步调用完成所需的时间),但原则上是可以的。 - LBushkin

5
如何像这样做一些事情:
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。实际上,在这种情况下没有必要处理原始 Task 对象,因为只有在需要 Task 执行阻塞等待时才需要处理。请参见此问题 - Simon P Stevens

5

我注意到还没有人回复这个问题:

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

我不知道你是否意识到这一点,但异步委托实际上正是这样做的-它们将工作排队到ThreadPool中的工作线程中,就像您执行的QueueUserWorkItem一样。

仅当它们是特殊的框架委托时,如Stream.BeginReadSocket.BeginSend时,异步委托的行为才会有所不同。这些使用I/O完成端口。

除非您正在ASP.NET环境中启动数百个此类任务,否则我建议仅使用线程池。

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

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

Task.Factory.StartNew(action);

(请注意,上述代码也将使用线程池!)

确实,这是一个问题。鉴于这个事实,我将不得不重新审查方法。在MSDN上是否有任何明确的文档说明这一点? - LBushkin
@LBushkin:你知道,那是一个非常好的问题,我不认为有任何清晰的文档,它可能被认为是一种实现细节。当我为别人关于ASP.NET线程池饥饿的问题做了一些实验时,我确信发现了这一点;我进入了一个异步回调并看到它确实占用了线程池中的一个工作线程。如果你找到了任何文档,请告诉我,这将很好地提供链接。 - Aaronaught
是的,我也试图找到这方面的参考资料,但没有找到。 - spender

4
编译器生成的BeginInvoke方法也在线程池上调用(参考)。所以我认为ThreadPool.QueueUserWorkItem应该没问题,除非你想更加明确一些(而且我想未来的CLR可能会选择在不同的线程池上运行BeginInvoke方法)。

0

尝试使用这个扩展方法(参见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();

0

那位聪明的Skeet先生在这里探讨了这个主题。

在中途有一种不同的“点火并忘记”的方法。


是的,我以前见过这种方法。主要问题在于它失去了类型安全性(因此失去了智能感知和重构支持),但它确实支持更多的委托签名。 - LBushkin

0
你可以编写自己的线程池实现。这听起来可能比实际要花费更多的工作。然后您就不必遵守“仅运行相对短暂的代码”的建议。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接