65.9K
CodeProject 正在变化。 阅读更多。
Home

异步方法调用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (966投票s)

2006年7月25日

CPOL

18分钟阅读

viewsIcon

2097618

downloadIcon

15

如何使用 .NET 以非阻塞方式调用方法。

引言

在本文中,我将解释异步方法调用以及如何使用它们。在研究了委托、线程和异步调用这么长时间之后,不分享一些我在这方面的智慧和知识简直是罪过,所以希望您不会在凌晨 1 点盯着 MSDN 文章,想着为什么当初要选择计算机行业。我将尝试循序渐进,并提供大量示例……总的来说,我将涵盖如何异步调用方法、如何向这些方法传递参数以及如何确定方法何时完成执行。最后,我将展示命令模式(Command Pattern)的使用,以简化部分代码。 .NET 异步方法调用的最大优点是,您可以采用项目中的任何方法,并可以异步调用它,而无需修改您方法的代码。尽管大部分魔力都隐藏在 .NET 框架中,但了解幕后发生的事情很重要,这就是我们今天要研究的内容。

同步与异步

我将尝试用一个例子来解释同步和异步方法调用,因为我知道 Code Project 的读者喜欢看代码,而不是读《战争与和平》(我并不是说这本书不好)。

同步方法调用

假设我们有一个函数 Foo(),它需要 10 秒钟才能执行。

private void Foo()
{
    // sleep for 10 seconds.
    Thread.Sleep(10000);
}

通常,当您的应用程序调用函数 Foo() 时,它需要等待 10 秒钟,直到 Foo() 完成并将控制权返回给调用线程。现在,假设您想调用 Foo() 100 次,那么我们知道控制权返回给调用线程将需要 1000 秒。这种类型的方法调用是同步的。

  1. 调用 Foo()
  2. Foo() 正在执行
  3. 控制权返回到调用线程

现在,让我们使用委托调用 Foo(),因为我们在这里的大部分工作都基于委托。幸运的是,.NET 框架中已经有一个委托允许我们调用一个不接受参数且没有返回值的方法。该委托称为 MethodeInvoker。让我们稍微玩一下它。

// create a delegate of MethodInvoker poiting
// to our Foo() function.
MethodInvoker simpleDelegate = new MethodInvoker(Foo);

// Calling Foo
simpleDelegate.Invoke();

即使使用上面的例子,我们仍然是同步调用 Foo()。调用线程在 Invoke() 函数完成并返回控制权给调用线程之前,仍然需要等待。

异步方法调用

但是,如果我想调用 Foo() 并且不想等待它完成执行怎么办?事实上,为了让事情更有趣,如果我不关心它何时完成怎么办?比如,我只想调用 Foo 100 次,而不必等待任何函数调用完成。基本上,这就做所谓的“发送即忘”(Fire and Forget)。你调用函数,你不等待它,然后就忘了它。而且……别忘了!我不想改变我的那个超级复杂、花哨的 Foo() 函数的任何一行代码。

// create a delegate of MethodInvoker poiting to
// our Foo function.
MethodInvoker simpleDelegate = new MethodInvoker(Foo);

// Calling Foo Async
for(int i=0; i<100; i++)
    simpleDelegate.BeginInvoke(null, null);

让我对上面的代码做一些说明。

  • 请注意,BeginInvoke() 是执行 Foo() 函数的代码行。但是,控制权会立即返回给调用者,而无需等待 Foo() 完成。
  • 上面的代码不知道对 Foo() 的调用何时完成,我稍后会讲到。
  • BeginInvoke() 用于代替 Invoke()。目前,请不要担心这个函数接受的参数;我稍后会更详细地讨论。

那么 .NET 在后台做了什么魔法?

一旦你要求框架异步调用某项操作,它就需要一个线程来完成工作。它不能是当前线程,因为那样会使调用成为同步的(阻塞)。相反,运行时会将执行函数的请求排队到 .NET 线程池中的一个线程。您实际上不需要为此编写任何代码,这一切都在后台发生。但是,仅仅因为它是透明的,并不意味着您不必关心它。有几点需要记住:

  • Foo() 在一个单独的线程上执行,该线程属于 .NET 线程池。
  • 一个 .NET 线程池通常有 25 个线程(您可以更改此限制),每次调用 Foo() 时,它都会在其中一个线程上执行。您无法控制是哪个线程。
  • 线程池是有限制的!一旦所有线程都被使用,异步方法调用就会被排队,直到池中的一个线程被释放。这称为**线程池饥饿**(Thread Pool Starvation),通常在这种情况下,性能会受到影响。

不要过深地钻研线程池,否则您可能会缺氧!

所以,让我们看一个线程池饥饿的例子。让我们修改我们的 Foo 函数,使其等待 30 秒,并让它报告以下内容:

  • 池中可用线程数
  • 线程是否在线程池中
  • 线程 ID。

我们知道初始时线程池包含 25 个线程,所以我将异步调用我的 Foo 函数 30 次(以查看第 25 次调用之后会发生什么)。

private void CallFoo30AsyncTimes()
{
    // create a delegate of MethodInvoker
    // poiting to our Foo function.
    MethodInvoker simpleDelegate =
        new MethodInvoker(Foo);

    // Calling Foo Async 30 times.
    for (int i = 0; i < 30; i++)
    {
         // call Foo()
        simpleDelegate.BeginInvoke(null, null);
    }
}
private void Foo()
{
    int intAvailableThreads, intAvailableIoAsynThreds;

    // ask the number of avaialbe threads on the pool,
    //we really only care about the first parameter.
    ThreadPool.GetAvailableThreads(out intAvailableThreads,
            out intAvailableIoAsynThreds);

    // build a message to log
    string strMessage =
        String.Format(@"Is Thread Pool: {1},
            Thread Id: {2} Free Threads {3}",
            Thread.CurrentThread.IsThreadPoolThread.ToString(),
            Thread.CurrentThread.GetHashCode(),
            intAvailableThreads);

    // check if the thread is on the thread pool.
    Trace.WriteLine(strMessage);

    // create a delay...
    Thread.Sleep(30000);

    return;
}

输出窗口

Is Thread Pool: True, Thread Id: 7 Free Threads 24
Is Thread Pool: True, Thread Id: 12 Free Threads 23
Is Thread Pool: True, Thread Id: 13 Free Threads 22
Is Thread Pool: True, Thread Id: 14 Free Threads 21
Is Thread Pool: True, Thread Id: 15 Free Threads 20
Is Thread Pool: True, Thread Id: 16 Free Threads 19
Is Thread Pool: True, Thread Id: 17 Free Threads 18
Is Thread Pool: True, Thread Id: 18 Free Threads 17
Is Thread Pool: True, Thread Id: 19 Free Threads 16
Is Thread Pool: True, Thread Id: 20 Free Threads 15
Is Thread Pool: True, Thread Id: 21 Free Threads 14
Is Thread Pool: True, Thread Id: 22 Free Threads 13
Is Thread Pool: True, Thread Id: 23 Free Threads 12
Is Thread Pool: True, Thread Id: 24 Free Threads 11
Is Thread Pool: True, Thread Id: 25 Free Threads 10
Is Thread Pool: True, Thread Id: 26 Free Threads 9
Is Thread Pool: True, Thread Id: 27 Free Threads 8
Is Thread Pool: True, Thread Id: 28 Free Threads 7
Is Thread Pool: True, Thread Id: 29 Free Threads 6
Is Thread Pool: True, Thread Id: 30 Free Threads 5
Is Thread Pool: True, Thread Id: 31 Free Threads 4
Is Thread Pool: True, Thread Id: 32 Free Threads 3
Is Thread Pool: True, Thread Id: 33 Free Threads 2
Is Thread Pool: True, Thread Id: 34 Free Threads 1
Is Thread Pool: True, Thread Id: 35 Free Threads 0
Is Thread Pool: True, Thread Id: 7 Free Threads 0
Is Thread Pool: True, Thread Id: 12 Free Threads 0
Is Thread Pool: True, Thread Id: 13 Free Threads 0
Is Thread Pool: True, Thread Id: 14 Free Threads 0
Is Thread Pool: True, Thread Id: 15 Free Threads 0

让我们对输出做一些说明:

  • 首先,请注意所有线程都在线程池中。
  • 请注意,每次调用 Foo 时,都会分配一个新的线程 ID。但是,您可以看到有些线程被回收了。
  • 在调用 Foo() 25 次后,您可以看到池中没有更多可用线程了。此时,应用程序“等待”一个空闲线程。
  • 一旦线程被释放,程序会立即获取它,调用 Foo(),此时池中仍有 0 个可用线程。这种情况会一直持续到 Foo() 被调用 30 次。

因此,不做什么太花哨的事情,我们就可以对异步调用方法做出一些评论:

  • 要知道您的代码将在一个单独的线程中运行,因此可能存在某些线程安全问题。这是一个独立的主题,我在这里不介绍。
  • 请记住,线程池是有限制的。如果您计划异步调用许多函数,并且它们需要很长时间才能执行,则可能会发生线程池饥饿。

BeginInvoke() 和 EndInvoke()

到目前为止,我们已经看到了如何在不知道方法何时完成的情况下调用它。但是,通过 EndInvoke(),可以做更多的事情。首先,EndInvoke 会阻塞直到您的函数完成执行;因此,调用 BeginInvoke 然后调用 EndInvoke 实际上几乎就像以阻塞模式调用函数(因为 EndInvoke 将等待直到函数完成)。但是,.NET 运行时如何知道将 BeginInvokeEndInvoke 绑定在一起呢?嗯,这就是 IAsyncResult 发挥作用的地方。调用 BegineInvoke 时,返回对象是 IAsyncResult 类型的一个对象;它是框架跟踪函数执行的粘合剂。可以将其看作是一个小标签,让您知道函数的状态。有了这个强大超级小标签,您可以找出函数何时完成执行,还可以使用此标签附加任何您可能想传递给函数的状态对象。好的!让我们看一些例子,这样就不会太混乱……让我们创建一个新的 Foo 函数。

private void FooOneSecond()
{
    // sleep for one second!
    Thread.Sleep(1000);
}
private void UsingEndInvoke()
{
    // create a delegate of MethodInvoker poiting to our Foo function.
    MethodInvoker simpleDelegate = new MethodInvoker(FooOneSecond);

    // start FooOneSecond, but pass it some data this time!
    // look at the second parameter
    IAsyncResult tag =
        simpleDelegate.BeginInvoke(null, "passing some state");

    // program will block until FooOneSecond is complete!
    simpleDelegate.EndInvoke(tag);

    // once EndInvoke is complete, get the state object
    string strState = (string)tag.AsyncState;

    // write the state object
    Trace.WriteLine("State When Calling EndInvoke: "
        + tag.AsyncState.ToString());
}

关于异常怎么办,我如何捕获它们?

现在,让我们让事情变得更复杂一些。让我修改 FooOneSecond 函数,让它抛出一个异常。现在,您应该会想知道如何捕获这个异常。是在 BeginInvoke 中,还是在 EndInvoke 中?或者是否有可能捕获这个异常?嗯,它不在 BeginInvoke 中。BeginInvoke 的作用仅仅是在 ThreadPool 上启动函数。报告函数完成的所有信息,包括异常,实际上是 EndInvoke 的工作。请注意下一个代码片段:

private void FooOneSecond()
{
    // sleep for one second!
    Thread.Sleep(1000);
    // throw an exception
    throw new Exception("Exception from FooOneSecond");
}

现在,让我们调用 FooOneSecond,看看是否能捕获异常。

private void UsingEndInvoke()
{
    // create a delegate of MethodInvoker poiting
    // to our Foo function.
    MethodInvoker simpleDelegate =
        new MethodInvoker(FooOneSecond);

    // start FooOneSecond, but pass it some data this time!
    // look at the second parameter
    IAsyncResult tag = simpleDelegate.BeginInvoke(null, "passing some state");

    try
    {
        // program will block until FooOneSecond is complete!
        simpleDelegate.EndInvoke(tag);
    }
    catch (Exception e)
    {
        // it is here we can catch the exception
        Trace.WriteLine(e.Message);
    }

    // once EndInvoke is complete, get the state object
    string strState = (string)tag.AsyncState;

    // write the state object
    Trace.WriteLine("State When Calling EndInvoke: "
        + tag.AsyncState.ToString());
}

通过运行代码,您会看到异常仅在调用 EndInvoke 时才抛出并被捕获。如果您决定从不调用 EndInvoke,那么您将不会收到异常。但是,在调试器中运行此代码时,根据您的异常设置,调试器可能会在抛出异常时停止。但这只是调试器。使用发布版本,如果您不调用 EndInvoke,您将永远不会收到异常。

将参数传递给您的方法

好的,调用不带参数的函数不会走多远,所以我将修改我那个超级花哨且复杂的 Foo 函数,让它接受一些参数。

private string FooWithParameters(string param1,
               int param2, ArrayList list)
{
    // lets modify the data!
    param1 = "Modify Value for param1";
    param2 = 200;
    list = new ArrayList();

    return "Thank you for reading this article";
}

让我们使用 BeginInvokeEndInvoke 调用 FooWithParameters。首先,在我们做任何事情之前,我们必须有一个与此方法签名匹配的委托。

public delegate string DelegateWithParameters(string param1, 
                       int param2, ArrayList list);

可以将 BeginInvokeEndInvoke 看作是将我们的函数分解成两个独立的方法。BeginInvoke 负责接受所有输入参数,后面跟着每个 BeginInvoke 都有的两个额外参数(回调委托和状态对象)。EndInvoke 负责返回所有输出参数(标记为 refout 的参数)以及返回值(如果有)。让我们回到我们的例子,看看哪些被视为输入参数,哪些被视为输出参数。param1param2list 都被视为输入参数,因此它们将被接受为 BeginInvoke 方法的参数。返回的 string 类型的值被视为输出参数,因此它将是 EndInvoke 的返回类型。很酷的一点是,编译器能够根据您的委托声明生成正确的 BeginInvokeEndInvoke 签名。请注意,我决定修改输入参数的值,以检查其行为是否符合我的预期,而无需调用 BeginInvokeEndInvoke。我还重新分配了传递给它的 ArrayList 为一个新的 ArrayList。所以,试着猜猜输出会是什么……

private void CallFooWithParameters()
{
    // create the paramets to pass to the function
    string strParam1 = "Param1";
    int intValue = 100;
    ArrayList list = new ArrayList();
    list.Add("Item1");

    // create the delegate
    DelegateWithParameters delFoo =
        new DelegateWithParameters(FooWithParameters);

    // call the BeginInvoke function!
    IAsyncResult tag =
        delFoo.BeginInvoke(strParam1, intValue, list, null, null);

    // normally control is returned right away,
    // so you can do other work here...

    // calling end invoke to get the return value
    string strResult = delFoo.EndInvoke(tag);

    // write down the parameters:
    Trace.WriteLine("param1: " + strParam1);
    Trace.WriteLine("param2: " + intValue);
    Trace.WriteLine("ArrayList count: " + list.Count);
}

让我们再看看我们的 FooWithParameters,这样您就不需要向上滚动了。

private string FooWithParameters(string param1,
        int param2, ArrayList list)
{
    // lets modify the data!
    param1 = "Modify Value for param1";
    param2 = 200;
    list = new ArrayList();

    return "Thank you for reading this article";
}

让我给您看三行在调用 EndInvoke() 后输出窗口中的内容:

param1: Param1
param2: 100
ArrayList count: 1

好的,让我们分析一下。即使我的函数修改了输入参数的值,在调用 EndInvoke 后我们也看不到这些更改。字符串是不可变类型,因此会创建一个字符串的副本,并且更改不会传递回调用者。整数是值类型,通过值传递时会创建副本。最后,重新创建 ArrayList 没有返回给调用者,因为传递给它的 ArrayList 的引用是按值传递的,实际上,重新创建 ArrayList 只是为 ArrayList 创建了一个新的分配,并将其分配给传递的“复制”的引用。实际上,该引用丢失了,通常被视为内存泄漏;但幸运的是,.NET 垃圾回收器最终会处理它。那么,如果我们想收回我们新分配的 ArrayList 以及我们对参数所做的其他更改怎么办?我们需要做什么?很简单;我们只需将 ArrayList 标记为 ref 参数。为了好玩,我们还添加了输出参数,以显示 EndInvoke 如何变化。

private string FooWithOutAndRefParameters(string param1,
        out int param2, ref ArrayList list)
{
    // lets modify the data!
    param1 = "Modify Value for param1";
    param2 = 200;
    list = new ArrayList();

    return "Thank you for reading this article";
}

让我们看看什么被视为输出参数,什么被视为输入参数……

  • Param1 是一个输入参数,它只会在 BeginInvoke 中被接受。
  • Param2 是输入和输出;因此,它将被传递给 BeginInvokeEndInvokeEndInvoke 将提供更新后的值)。
  • list 是按引用传递的,因此它也将被传递给 BeginInvokeEndInvoke

让我们看看我们的委托现在是什么样子:

public delegate string DelegateWithOutAndRefParameters(string param1, 
                out int param2, ref ArrayList list);

最后,让我们看看调用 FooWithOutAndRefParameters 的函数:

private void CallFooWithOutAndRefParameters()
{
    // create the paramets to pass to the function
    string strParam1 = "Param1";
    int intValue = 100;
    ArrayList list = new ArrayList();
    list.Add("Item1");

    // create the delegate
    DelegateWithOutAndRefParameters delFoo =
      new DelegateWithOutAndRefParameters(FooWithOutAndRefParameters);

    // call the beginInvoke function!
    IAsyncResult tag =
        delFoo.BeginInvoke(strParam1,
            out intValue,
            ref list,
            null, null);

    // normally control is returned right away,
    // so you can do other work here...

    // calling end invoke notice that intValue and list are passed
    // as arguments because they might be updated within the function.
    string strResult =
        delFoo.EndInvoke(out intValue, ref list, tag);

    // write down the parameters:
    Trace.WriteLine("param1: " + strParam1);
    Trace.WriteLine("param2: " + intValue);
    Trace.WriteLine("ArrayList count: " + list.Count);
    Trace.WriteLine("return value: " + strResult);
}

输出如下

param1: Param1
param2: 200
ArrayList count: 0
return value: Thank you for reading this article

请注意,param1 没有改变。它是一个输入参数,param2 作为输出参数传递,并更新为 200。数组列表已被重新分配,现在我们看到它指向一个元素数为零的新引用(原始引用已丢失)。我希望您现在已经理解了如何使用 BeginInvokeEndInvoke 传递参数。让我们继续看看如何获得非阻塞函数的完成通知。

他们不想让你知道的关于 IAsyncResult 的秘密

您应该想知道 EndInvoke 如何为我们提供输出参数和更新的 ref 参数。或者,更好的是,EndInvoke 如何抛出我们在函数中抛出的异常。例如,假设我们对 Foo 调用了 BegineInvoke,然后 Foo 完成了执行,现在,我们通常会调用 EndInvoke,但如果我们决定在 Foo 完成 20 分钟后调用 EndInvoke 怎么办?请注意,EndInvoke 仍会为您提供那些输出或 ref 参数,并且它仍然会抛出异常(如果抛出了异常)。那么,所有这些信息都存储在哪里?为什么 EndInvoke 能够在函数完成很久之后获取所有这些数据?嗯……关键在于 IAsyncResult 对象!我决定更深入地研究这个对象,正如我所怀疑的,正是这个对象保留了有关您的函数调用的所有信息。请注意,EndInvoke 接受一个参数,该参数是 IAsyncResult 类型的对象。此对象包含信息,例如:

  • 函数是否已完成?
  • 对用于 BeginInvoke 的委托的引用
  • 所有输出参数及其值
  • 所有 ref 参数及其更新值
  • 返回值
  • 如果抛出了异常,则为异常
  • 以及更多……

IAsyncResult 看起来非常无害,因为它只是一个访问少数属性的接口,但实际上,它是一个 System.Runtime.Remoting.Messaging.AsyncResult 类型的对象。

AsyncResult

现在,如果我们深入挖掘,我们会发现 AsyncResult 包含一个名为 _replyMsg 的对象,类型为 System.Runtime.Remoting.Messaging.ReturnMessage,您猜怎么着……找到了“圣杯”!

Check out the _replyMsg property... Its all there!

我不得不缩小上述图像的大小,以免您需要向右滚动阅读,您可以直接单击图像进行查看。

我们可以清楚地看到返回值、输出参数和 ref 参数。甚至还有一个用于保存异常的异常属性。请注意,我在调试窗口中展开了 OutArgs 以显示值 200 和对新分配的 ArrayList 的引用。您还可以在 ReturnValue 属性中看到字符串“Thank you for reading this article”。如果我们有异常,那么 EndInvoke 会将其抛出供我们捕获。我认为这足以证明有关您的函数调用的所有信息都保存在您从 BeginInvoke 返回的那个小小的 IAsyncResult 对象中,它就像您数据的钥匙。如果我们丢失了这个对象,我们将永远不知道我们的输出参数、ref 参数和返回值。没有这个对象,也不可能捕获异常。它是钥匙!您丢失了它,信息就会永远丢失在 .NET 运行时的迷宫中……好吧,我有点跑题了。我想我已经表达了我的观点。

使用回调委托,好莱坞风格的“别找我,我找你!”

此时,您应该已经理解了如何传递参数、如何传递状态以及您的方法是在 ThreadPool 中的某个线程上执行的事实。我唯一没有真正涵盖的是在方法完成执行时获得通知的想法。毕竟,阻塞并等待方法完成并不能真正完成多少事情。为了在方法完成时获得通知,您必须在 BeginInvoke 中提供一个回调委托。好的,举个例子!看以下两个函数:

private void CallFooWithOutAndRefParametersWithCallback()
{
    // create the paramets to pass to the function
    string strParam1 = "Param1";
    int intValue = 100;
    ArrayList list = new ArrayList();
    list.Add("Item1");

    // create the delegate
    DelegateWithOutAndRefParameters delFoo =
        new DelegateWithOutAndRefParameters(FooWithOutAndRefParameters);

    delFoo.BeginInvoke(strParam1,
        out intValue,
        ref list,
        new AsyncCallback(CallBack), // callback delegate!
        null);
}

private void CallBack(IAsyncResult ar)
{
    // define the output parameter
    int intOutputValue;
    ArrayList list = null;

    // first case IAsyncResult to an AsyncResult object, so we can get the
    // delegate that was used to call the function.
    AsyncResult result = (AsyncResult)ar;

    // grab the delegate
    DelegateWithOutAndRefParameters del =
        (DelegateWithOutAndRefParameters) result.AsyncDelegate;

    // now that we have the delegate,
    // we must call EndInvoke on it, so we can get all
    // the information about our method call.

    string strReturnValue = del.EndInvoke(out intOutputValue,
        ref list, ar);
}

在这里,您可以看到我们在调用 BeginInvoke 时将一个委托传递给了 CallBack 函数。当 FooWithOutAndRefParameters 方法完成执行时,.NET 会通知我们。和以前一样,我们都知道如果我们想获取输出参数,就必须调用 EndInvoke。请注意,为了调用 EndInvoke,我需要做一些体操才能获取委托。

AsyncResult result = (AsyncResult)ar;
// grab the delegate
DelegateWithOutAndRefParameters del =
    (DelegateWithOutAndRefParameters) result.AsyncDelegate;

等一下!回调是在哪个线程上执行的?

毕竟,回调是由 .NET 使用您的委托调用的,但仍然是 .NET 在调用这个委托。您有权也有责任知道您的代码在哪个线程上执行。为了清晰地说明正在发生的事情,我决定再次修改我的 Foo 函数以包含线程信息,并添加 4 秒的延迟。

private string FooWithOutAndRefParameters(string param1,
        out int param2, ref ArrayList list)
{
    // log thread information
    Trace.WriteLine("In FooWithOutAndRefParameters: Thread Pool? "
        + Thread.CurrentThread.IsThreadPoolThread.ToString() +
        " Thread Id: " + Thread.CurrentThread.GetHashCode());

    // wait for 4 seconds as if this functions takes a while to run.
    Thread.Sleep(4000);

    // lets modify the data!
    param1 = "Modify Value for param1";
    param2 = 200;
    list = new ArrayList();

    return "Thank you for reading this article";
}

我还向回调函数添加了线程信息。

private void CallBack(IAsyncResult ar)
{
    // which thread are we on?
    Trace.WriteLine("In Callback: Thread Pool? "
        + Thread.CurrentThread.IsThreadPoolThread.ToString() +
        " Thread Id: " + Thread.CurrentThread.GetHashCode());

    // define the output parameter
    int intOutputValue;
    ArrayList list = null;

    // first case IAsyncResult to an AsyncResult object,
    // so we can get the delegate that was used to call the function.
    AsyncResult result = (AsyncResult)ar;

    // grab the delegate
    DelegateWithOutAndRefParameters del =
        (DelegateWithOutAndRefParameters) result.AsyncDelegate;

    // now that we have the delegate, we must call EndInvoke on it, so we
    // can get all the information about our method call.
    string strReturnValue = del.EndInvoke(out intOutputValue, ref list, ar);
}

我决定通过我窗体上的一个按钮来多次执行 FooWithOutAndRefParameters

private void button4_Click(object sender, EventArgs e)
{
    CallFooWithOutAndRefParametersWithCallback();
}

让我们看看按下按钮三次(三次调用函数)后的输出:

In FooWithOutAndRefParameters: Thread Pool? True Thread Id: 7
In FooWithOutAndRefParameters: Thread Pool? True Thread Id: 12
In FooWithOutAndRefParameters: Thread Pool? True Thread Id: 13
In Callback: Thread Pool? True Thread Id: 7
In Callback: Thread Pool? True Thread Id: 12
In Callback: Thread Pool? True Thread Id: 13

请注意,我的 Foo 函数被执行了三次,一次接一次,在三个独立的线程上。所有线程都在线程池中。同时请注意,回调也分别被执行了三次,它们也都在线程池中。有趣的是,回调似乎与 Foo 在同一个线程 ID 上执行。线程 7 执行 Foo;4 秒后,回调也在线程 7 上执行。线程 12 和 13 也是如此。这就像回调是我 Foo 函数的延续。我按了很多次按钮,试图看看回调是否会在 Foo 执行的线程 ID 以外的线程上被调用,但我未能实现。如果您仔细想想,这是完全有道理的。试想一下,.NET 如果会获取一个线程来调用 Foo,然后再获取另一个线程来调用回调,那将是多么浪费!更不用说,如果您的线程池饥饿了,您将不得不等待一个空闲线程才能调用回调!那将是一场灾难。

使用命令模式来清理!

好的!面对现实吧,事情有点乱。我们有 BeginInvokeEndInvoke、回调,它们都分散在各处!让我们尝试使用命令模式来清理方法调用。使用 命令模式很简单。基本上,您创建一个实现简单接口的命令对象,例如:

public interface ICommand
{
    void Execute();
}

是时候停止到处使用我们无用的 Foo 函数,尝试做一些更真实的事情了!所以,让我们创建一个更现实的场景。假设我们有以下内容:

  • 我们有一个用户窗体,其中包含一个显示客户行的网格。
  • 网格根据客户 ID 的搜索条件更新。但是,数据库很远,获取客户数据集需要 5 秒钟,而我们不想在等待时阻塞 UI。
  • 我们有一个不错的业务对象,它负责根据客户 ID 获取我们的客户数据集。

假设这是我们的业务层。为了简化示例,我硬编码了通常来自数据层的内容。

public class BoCustomer
{
    public DataSet GetCustomer(int intCustomerId)
    {
        // call data layer and get customer information
        DataSet ds = new DataSet();
        DataTable dt = new DataTable("Customer");
        dt.Columns.Add("Id", typeof(int));
        dt.Columns.Add("FirstName", typeof(string));
        dt.Columns.Add("LastName", typeof(string));

        dt.Rows.Add(intCustomerId, "Mike", "Peretz");
        ds.Tables.Add(dt);

        // lets make this take some time...
        System.Threading.Thread.Sleep(2000);
        return ds;
    }
}

现在,让我们创建我们的命令,它负责根据客户 ID 更新网格。

public class GetCustomerByIdCommand : ICommand
{
    private GetCustomerByIdDelegate m_invokeMe;
    private DataGridView m_grid;
    private int m_intCustmerId;

    // notice that the delegate is private,
    // only the command can use it.
    private delegate DataSet GetCustomerByIdDelegate(int intCustId);

    public GetCustomerByIdCommand(BoCustomer boCustomer,
        DataGridView grid,
        int intCustId)
    {
        m_grid = grid;
        m_intCustmerId = intCustId;

        // setup the delegate to call
        m_invokeMe =
            new GetCustomerByIdDelegate(boCustomer.GetCustomer);
    }

    public void Execute()
    {
        // call the method on the thread pool
        m_invokeMe.BeginInvoke(m_intCustmerId,
            this.CallBack, // callback!
            null);
    }

    private void CallBack(IAsyncResult ar)
    {
        // get the dataset as output
        DataSet ds = m_invokeMe.EndInvoke(ar);

        // update the grid a thread safe fasion!
        MethodInvoker updateGrid = delegate
        {
            m_grid.DataSource = ds.Tables[0];
        };

        if (m_grid.InvokeRequired)
            m_grid.Invoke(updateGrid);
        else
            updateGrid();
    }
}

请注意,GetCustomerByIdCommand 接收了执行命令所需的所有信息。

  • 需要更新的网格。
  • 要搜索的客户 ID。
  • 对业务层的引用。

还请注意,委托隐藏在命令对象内部,因此客户端无需了解命令的内部工作原理。客户端需要做的就是构建命令并调用它的 Execute。我们现在都知道异步方法调用是在 ThreadPool 上进行的,并且我们都应该知道从 ThreadPool 或任何非 UI 线程更新 UI 是不健康的!因此,为了解决这个问题,我们将此实现隐藏在命令中,并根据网格检查 InvokeRequired() 是否为 true。如果为 true,我们使用 Control.Invoke 来确保调用被封送到 UI 线程。(注意,我使用的是 .NET 2.0 的创建匿名方法的功能。)让我们看看窗体是如何创建命令并执行它的!

private ICommand m_cmdGetCustById;
private void button1_Click(object sender, EventArgs e)
{
    // get the custmer id from the screen
    int intCustId = Convert.ToInt32(m_txtCustId.Text);

    // use the buisness layer to get the data
    BoCustomer bo = new BoCustomer();

    // create a command that has all the tools to update the grid
    m_cmdGetCustById = new GetCustomerByIdCommand(
        bo, m_grid, intCustId);

    // call the command in a non blocking mode.
    m_cmdGetCustById.Execute();
}

请注意,Execute 是非阻塞的。但在您发疯创建一百万个命令类之前,请记住这些:

  • 命令模式可能会导致类爆炸,所以要明智地选择您的武器。
  • 在我的例子中,创建一个具有线程安全更新网格逻辑的命令基类会很容易,但我保持了示例的简单性。
  • TextBox 传递到命令对象中也是可以接受的,这样它就可以以更动态的方式获取输入,并允许命令随时被调用而无需重新创建它。
  • 请注意,委托、BeginInvokeEndInvoke、回调以及我们确保 UI 安全更新的疯狂代码都封装在我的命令中,这是一件好事!

结论

呼!我花了一周时间才写完这篇有趣的文章。我试图涵盖以非阻塞模式调用方法的所有重要方面。以下是一些需要记住的事情:

  • 委托将包含 BeginInvokeEndInvoke 的正确签名,您应该预期在调用 EndInvoke 时会获得所有输出参数和异常。
  • 别忘了,使用 BeginInvoke 时会消耗 ThreadPool 的资源,所以不要过度使用!
  • 如果您计划使用回调,最好使用命令模式来隐藏与之相关的那些令人头疼的代码。
  • 就我个人而言,UI 应该只在进行 UI 操作时才阻塞,所以现在您没有借口了!

感谢您的阅读,祝您有美好的一天,祝大家 .NET 编程愉快!

© . All rights reserved.