Typesafe fire-and-forget asynchronous delegate inv

2019-02-02 12:27发布

问题:

I recently found myself needing a typesafe "fire-and-forget" mechanism for running code asynchronously.

Ideally, what I would want to do is something like:

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

Unfortunately, the obvious choice of calling BeginInvoke() without a corresponding EndInvoke() does not work - it results in a slow resource leak (since the asyn state is held by the runtime and never released ... it's expecting an eventual call to EndInvoke(). I also can't run the code on the .NET thread pool because it may take a very long time to complete (it's advised to only run relatively short-lived code on the thread pool) - this makes it impossible to use the ThreadPool.QueueUserWorkItem().

Initially, I only needed this behavior for methods whose signature matches Action, Action<...>, or Func<...>. So I put together a set of extension methods (see listing below) that let me do this without running into the resource leak. There are overloads for each version of Action/Func.

Unfortunately, I now want to port this code to .NET 4 where the number of generic parameters on Action and Func have been increased substantially. Before I write a T4 script to generate these, I was also hoping to find a simpler more elegant way to do this. Any ideas are welcome.

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

}

回答1:

You can pass EndInvoke as AsyncCallback for BeginInvoke:

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

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

Does that help?



回答2:

I notice nobody's responded to this:

I also can't run the code on the .NET thread pool because it may take a very long time to complete (it's advised to only run relatively short-lived code on the thread pool) - this makes it impossible to use the ThreadPool.QueueUserWorkItem().

I'm not sure if you're aware of this, but async delegates actually do exactly this - they queue the work on a worker thread in the ThreadPool, exactly the same as if you did QueueUserWorkItem.

The only time when async delegates behave differently is when they're special framework delegates like Stream.BeginRead or Socket.BeginSend. These use I/O completion ports instead.

Unless you're spinning of hundreds of these tasks in an ASP.NET environment, I would recommend simply using the thread pool.

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

Or, in .NET 4, you can use the task factory:

Task.Factory.StartNew(action);

(Note that the above will also use the thread pool!)



回答3:

How about something like:

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

Use it like:

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

To add type safety, just expand out the helper methods. T4 is probably best here.



回答4:

The compiler-generated BeginInvoke method is also called on the thread pool (reference). So I think ThreadPool.QueueUserWorkItem would be alright, except that you're being a bit more explicit about it I guess (and I suppose a future CLR could choose to run BeginInvoke'ed methods on a different thread pool).



回答5:

That clever chap Skeet approaches this subject here.

There's a different approach to "fire and forget" about half way down.



回答6:

You could write your own threadpool implementation. It probably sounds like wore work than it would actually be. Then you don't have to abide by "only run relatively short-lived code" advisement.



回答7:

Give this extension method a shot (per C# Is action.BeginInvoke(action.EndInvoke,null) a good idea?) to ensure no memory leaks:

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

And you could use it with generic parameters as:

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