ILove's Dev Home - 休息的时候不要忘记 别人还在奔跑

  博客园 :: 首页 :: 新随笔 :: 联系 ::  :: 管理 ::
  21 随笔 :: 6 文章 :: 238 评论 :: 5 引用
在上一篇文章中,我们探讨了使用Thread类实现异步的方法。

在整个过程中,可以发现Delegate这个东西出现了很多次。而仔细研究Delegate,我们发现每一个Delegate类型都自动产生了Invoke、BeginInvoke、EndInvoke等方法。而BeginInvoke、EndInvoke这两个方法,我们马上就可以猜到这是用来实现异步的~~

那么我们现在就看一下怎样使用委托来实现异步。

Delegate的BeginInvoke、EndInvoke两个方法,是编译器自动生成的,专门用来实现异步,这里是MSDN中关于这两个方法的说明:

异步委托提供以异步方式调用同步方法的能力。当同步调用一个委托时,“Invoke”方法直接对当前线程调用目标方法。如果编译器支持异步委托,则它将生成“Invoke”方法以及“BeginInvoke”和“EndInvoke”方法。如果调用“BeginInvoke”方法,则公共语言运行库 (CLR) 将对请求进行排队并立即返回到调用方。将对来自线程池的线程调用该目标方法。提交请求的原始线程自由地继续与目标方法并行执行,该目标方法是对线程池线程运行的。如果在对“BeginInvoke”方法的调用中指定了回调方法,则当目标方法返回时将调用该回调方法。在回调方法中,“EndInvoke”方法获取返回值和所有输入/输出参数。如果在调用“BeginInvoke”时未指定任何回调方法,则可以从调用“BeginInvoke”的线程中调用“EndInvoke”。

其中,BeginInvoke用来启动异步,与Thread类不同的是这里的异步使用CLR管理的。BeginInvoke方法的最后两个参数总是一个AsyncCallback委托对象和一个object类型,其中AsyncCallback委托就是当异步执行完成时将要被调用的函数入口,也就是上一篇中用来实现“在异步完成时通知我”这个功能的。而最后一个object类型,则是用来传递参数的,其实与上一篇中ParameterizedThreadStart委托的参数是类似的——不过他们还是有着明显的区别:使用ParameterizedThreadStart委托时永远只能接受一个object类型的参数,因此如果原本要异步执行的函数具有多个参数,必须进行封装;而使用BeginInvoke方法则不同,编译器生成的BeginInvoke方法前面几个参数(除了最后两个)的类型跟声明委托时的参数个数和类型完全相同,这样就不必再封装参数了,最后一个object参数只是一个补充的参数,一般情况下是不需要的:
        private void DoMain(string cmd, string[] args)
        
{
            SumDelegate handle 
= new SumDelegate(this.Sum);
            IAsyncResult ar 
= handle.BeginInvoke(12nullnull);
        }


        
public delegate int SumDelegate(int x, int y);

        
public int Sum(int x, int y)
        
{
            
return x + y;
        }


我们可以看到,在调用BeginInvoke的时候,方法的后面两个参数就是对应的AsyncCallback和object参数,这里因为我们没有用到这个回调和参数,就都传递了null;而BeginInvoke的前面两个方法,就对应的是Sum函数的两个参数x和y。因此,这个BeginInvoke方法还在代码编译的时候就帮我们检查了函数的输入参数个数以及类型。

当使用Thread类时,我们可以通过判断Thread类的ThreadStatus来判断线程是否已经执行结束。而如果用Delegate.BeginInvoke方法,我们则需要根据其返回的一个IAsyncResult对象的IsCompleted属性来获取“异步操作是否已完成的指示”:当这个属性变成True时,就表示异步已经执行结束:
        private void DoMain(string cmd, string[] args)
        
{
            SumDelegate handle 
= new SumDelegate(this.Sum);
            IAsyncResult ar 
= handle.BeginInvoke(12nullnull);
            
while(!ar.IsCompleted)
            
{
                Thread.Sleep(
10);
            }

            
// 异步已经执行完毕
        }


        
public delegate int SumDelegate(int x, int y);

        
public int Sum(int x, int y)
        
{
            
return x + y;
        }



当然,前面我们提到,BeginInvoke方法总是会接收一个AsyncCallback类型的委托,当异步执行完毕后,CLR就会自动调用这个委托封装的函数。因此,我们还可以通过这个委托来接受异步已经完成的通知:
        private void DoMain(string cmd, string[] args)
        
{
            SumDelegate handle 
= new SumDelegate(this.Sum);
            AsyncCallback callback 
= new AsyncCallback(this.OnSumCompleted);
            IAsyncResult ar 
= handle.BeginInvoke(12, callback, null);
        }


        
public delegate int SumDelegate(int x, int y);

        
public int Sum(int x, int y)
        
{
            
return x + y;
        }


        
public void OnSumCompleted(IAsyncResult ar)
        
{
            
// 异步已经执行完毕
            Debug.Assert(ar.IsCompleted);
        }

注意这里,当向BeginInvoke传入的AsyncCallback被执行时,IAsyncResult对象的IsCompleted属性一定是True。另外,BeginInvoke方法传递的最后一个object参数,实际上就是保存在了IAsyncResult的AsyncState属性中。

上面已经提到了两种等待异步调用执行完毕的方法:主动轮询 和 异步执行完毕时执行回调方法。除了这两种方法,我们还可以通过EndInvoke方法来直接阻塞线程(并不是每次都会阻塞,这个我们下面再讲)直到异步执行完成:
        private void DoMain(string cmd, string[] args)
        
{
            SumDelegate handle 
= new SumDelegate(this.Sum);
            AsyncCallback callback 
= new AsyncCallback(this.OnSumCompleted);
            IAsyncResult ar 
= handle.BeginInvoke(12nullnull);
            
int value = handle.EndInvoke(ar);
            Debug.Assert(value 
== 3);
        }


        
public delegate int SumDelegate(int x, int y);

        
public int Sum(int x, int y)
        
{
            
return x + y;
        }

当调用EndInvoke时,必须把BeginInvoke返回的IAsyncResult对象作为参数传递,这样EndInvoke才可以通过IAsyncResult对象得知要等待哪个方法异步执行完毕。因为在BeginInvoke返回的IAsyncResult中,属性AsyncWaitHandle指示了用于等待异步执行完毕的一个句柄。如果你调用了很多次BeginInvoke,就会启动很多个异步任务,每次调用返回的IAsyncResult就会对应的保存了不同的句柄。另外,这里可以看到,EndInvoke方法的返回结果,实际上就是我们在定义SumDelegate委托时声明的返回值类型,这个也是编译器自动帮我们生成的。

那么,我们刚才提到EndInvoke方法“并不是每次都会阻塞”。为什么呢?原因很简单:在EndInvoke方法内部,首先会判断IAsyncResult.IsCompleted属性,如果为True,则直接返回执行结果,否则调用AsyncWaitHandle这个句柄的WaitOne方法,这个方法“阻止当前线程,直到当前的 WaitHandle 收到异步调用已经结束的信号”,然后返回执行结果。

因此,与之对应的,我们还有另外一个方法来等待异步执行结束,那就是我们直接访问AsyncWaitHandle:
        private void DoMain(string cmd, string[] args)
        
{
            SumDelegate handle 
= new SumDelegate(this.Sum);
            AsyncCallback callback 
= new AsyncCallback(this.OnSumCompleted);
            IAsyncResult ar 
= handle.BeginInvoke(12nullnull);
            
if(!ar.IsCompleted)
            
{
                ar.AsyncWaitHandle.WaitOne();
            }

            
// 异步调用已结束。
            Debug.Assert(ar.IsCompleted);
        }


        
public delegate int SumDelegate(int x, int y);

        
public int Sum(int x, int y)
        
{
            
return x + y;
        }

实际上,这个方式跟EndInvoke是完全相同的。

这下我们应该明白刚才所说的“并不是每次都会阻塞”了吧?没错:当ar.IsCompleted为True时,就会直接返回函数执行结果,否则才会调用WaitHandle的WaitOne来阻塞线程。

通过Delegate对象,我们可以使得我们的类更方便的支持异步方法。就好像刚才的类里面,我们有个Sum方法,然后通过定义一个可以接受这个函数的Delegate,然后用户就可以使用这个Delegate、AsyncCallback、IAsyncResult等对象来实现异步了。

那么我们可不可以为客户封装的更简单一点呢?就好像FileStream类,就有Read、BeginRead、EndRead三个方法,非常简单好用。很明显的,FileStream对象是封装了对Delegate对象的BeginInvoke、EndInvoke方法的调用。那么我们怎样去实现这样的效果呢?

下面,我们利用实现一个支持异步调用的一个类,这个类有个用于同步执行的Sum函数,和一个异步执行的BeginSum、EndSum函数:

    public class MyClass1
    
{
        
private delegate int SumDelegate(int a, int b);

        
private SumDelegate _sumHandler;

        
public MyClass1()
        
{
            
this._sumHandler = new SumDelegate(this.Sum);
        }


        
public int Sum(int a, int b)
        
{
            
return a + b;
        }


        
public IAsyncResult BeginSum(int a, int b, AsyncCallback callback, object stateObject)
        
{
            
return this._sumHandler.BeginInvoke(a, b, callback, stateObject);
        }


        
public int EndSum(IAsyncResult asyncResult)
        
{
            
return this._sumHandler.EndInvoke(asyncResult);
        }

    }


注意这个类的内部,声明了一个私有的委托类型“SumDelegate”,以及一个类型为SumDelegate的私有变量。我们把对这个委托的BeginInvoke、EndInvoke的调用,分别封装在了BeginSum、EndSum中。这样,用户在异步调用Sum方法时,就不用为了封装Sum函数而声明一个新的委托了。

posted on 2008-04-06 20:09 没有昵称 阅读(2549) 评论(15)  编辑 收藏 网摘 所属分类: .Net Framework

评论

#1楼  2008-04-06 22:58 侯垒      
好东西。解决了我的问题。
  回复  引用  查看    

#2楼  2008-04-07 07:21 Justin      
这个也不错
  回复  引用  查看    

#3楼  2008-04-07 14:33 李涛      
继续看,写的很基础,不过很好,有的东西没讲明白
  回复  引用  查看    

#4楼  2008-04-07 15:54 yellowyu      
在异步回调中可以是有返回参数的,但如果是返回多个参数(不便组成结构体),而这时又有并发问题的存在,请问有什么好的解决方法(主要是并发)

期待楼主
  回复  引用  查看    

#5楼  2008-04-07 15:56 yellowyu      
我一般都是用一个静态的OUT 的参数,尽管还没出现过问题,也有想过LOCK,但还是期待楼主的解答,嘿嘿,谢谢
  回复  引用  查看    

#6楼 [楼主] 2008-04-07 16:26 没有昵称      
--引用--------------------------------------------------
yellowyu: 在异步回调中可以是有返回参数的,但如果是返回多个参数(不便组成结构体),而这时又有并发问题的存在,请问有什么好的解决方法(主要是并发)

期待楼主
--------------------------------------------------------
public delegate int SumDelegate(int x, int y, out int a, out int b);

public class Class1
{
[STAThread]
static void Main(string[] args)
{
SumDelegate handle = new SumDelegate(Sum);
int a, b;
IAsyncResult ar = handle.BeginInvoke(1, 2, out a, out b, null, null);
int value = handle.EndInvoke(out a, out b, ar);
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(value);
Debug.Assert(a == 1);
Debug.Assert(b == 2);
Debug.Assert(value == 3);
}

static int Sum(int x, int y, out int a, out int b)
{
a = x;
b = y;
return x + y;
}
}

是这样的需求么?
另外,应该没有啥“不便组成结构体”的需求吧。这个“组成结构体”,你可以看作是一个Adapter模式的应用。如果不想暴露这个结构体,可以声明未私有的,然后在接口上包装一下。
  回复  引用  查看    

#7楼  2008-04-07 20:34 yellowyu      
@没有昵称
谢谢,我的确是这样的,
但 int a, b会不会存在还未传出其值就被更改的可能性呢!(我一般用静态的)

嘿嘿!或许是我表达有问题吧!嘿嘿,我说的不便组成结构体是想说两个不沾边的变量,就好比一个ID,传进去经过一小段时间的数据库查询而返回需要的信息,嘿嘿!我的不方便组合即是这个意思,或许是我表达出现问题,不好意思
不过主要问题还是上面这个,会不会我传入a的值进去运算,这时是一个比较久的过程(不久也少用到这了,嘿嘿)而这时却被其他继续使用的异步改变,


看到楼主使用断言,也一直不明白为啥使用断言,能一起说说吗?

表达或者想法愚蠢之外还请楼主见谅,谢谢

  回复  引用  查看    

#8楼 [楼主] 2008-04-07 22:00 没有昵称      
to 楼上:
不会产生这种问题的。out参数实际上是在函数执行完毕之后,才会把这个输出参数的值传递到函数外部的。
比如下面这个函数:
void Sum(int x, int y, out value)
{
value = x + y;
// 这里有一堆的代码,执行了很长时间
}
int a;
Sum(1, 2, out a);

这里,给a赋值的操作实际上是在整个函数执行完毕之后才会进行的,而不是在Sum函数的第一行执行完之后a马上就等于3了。

这个要说清楚就要讨论函数的执行原理了。又是一大篇呵呵。。。

ref参数也一样。你可以写个代码测试一下,^_^
  回复  引用  查看    

#9楼 [楼主] 2008-04-07 22:02 没有昵称      
public delegate int SumDelegate(int x, int y, out int a, ref int b);

public class Class1
{
static int nA, nB;

[STAThread]
static void Main(string[] args)
{
SumDelegate handle = new SumDelegate(Sum);
IAsyncResult ar = handle.BeginInvoke(1, 2, out nA, ref nB, null, null);
int value = handle.EndInvoke(out nA, ref nB, ar);
Debug.Assert(nA == 1);
Debug.Assert(nB == 2);
Debug.Assert(value == 3);
}

static int Sum(int x, int y, out int a, ref int b)
{
a = x;
b = y;

Console.WriteLine("a={0}, b={1}", nA, nB);

return x + y;
}

这是测试代码。你run一下就知道了。

如果要知道为啥,那就要去看一下函数的执行原理了。

变量名字起的有点乱哈哈,命名也不规范。凑合着用,平时不要这样命名呵呵。。。
  回复  引用  查看    

#10楼  2008-04-07 23:22 笑疯^_^      
期待有更好的文章出现
  回复  引用  查看    

#11楼  2008-04-08 11:10 yellowyu      
谢谢,我试过了,OUT与REF传出来的都是在那函数里执行的值

今晚回去我再试下异步回调的,而不采用你这样的EndInvoke,嘿嘿!我感觉这样的顺序打印出来还是顺序的(其实ENDINVOKE是阻塞在那里等着函数体执行完成),而我脑中想的是一种乱序的

谢谢!愚见之处还请见谅,小弟还是新手,嘿嘿


  回复  引用  查看    

#12楼 [楼主] 2008-04-08 11:25 没有昵称      
@yellowyu

注意这句话:Console.WriteLine("a={0}, b={1}", nA, nB); ,是在线程函数里面的。所以,这个测试跟同步还是异步是没有关系的。

他证明了,只有当这个函数返回之后,out 参数和ref参数的值才会被修改。

函数返回值、out参数、ref参数都是通过弹堆栈的方式实现的,函数内部会把out参数、ref参数、返回值放在函数堆栈的一个“返回点”上,等到函数执行完毕了之后,系统再把函数堆栈的返回点上的东西复制给外面的变量,然后销毁函数的堆栈。
  回复  引用  查看    

#13楼  2008-04-08 11:51 yellowyu      
没有看到你的回复,但我还是做了这么的一个测试程序,嘿嘿,是根据你的改过来的,不好意思哦
打印结果是

经过时:na:1 nB:1 nc:1
函数体内:a=1, b=1
经过时:na:0 nB:0 nc:0
函数体内:a=0, b=0
完成回调:na:0 nB:0 nc:0
完成回调:na:0 nB:0 nc:0

而我从头至尾想说的可能即是我上面的这种情况,比如我传入一个题目的ID,然后经过去数据库的查找,返回来时,ID:1已经变为0了,但从楼主上面的DEMO,我也获得了一些想法,谢谢分享

public delegate int SumDelegate(int x, int y, out int a, ref int b);

public class Class1
{
static int nA = 11, nB = 12, nc = 13;

[STAThread]
static void Main(string[] args)
{
SumDelegate handle = new SumDelegate(Sum);
int i = 2;
while (i > 0)
{
i--;
nA = i * i;
nB = i * i;
nc = i * i;
IAsyncResult ar = handle.BeginInvoke(nc, 2, out nA, ref nB, new AsyncCallback(consolesum), null);
Console.WriteLine("经过时:na:" + nA.ToString() + " nB:" + nB.ToString() + " nc:" + nc.ToString());
}
Console.ReadLine();
}

static void consolesum(IAsyncResult ar)
{
SumDelegate state = (SumDelegate)ar.AsyncState;
Console.WriteLine("完成回调:na:" + nA.ToString() + " nB:" + nB.ToString() + " nc:" + nc.ToString());

}

static int Sum(int x, int y, out int a, ref int b)
{
a = b;
Console.WriteLine("函数体内:a={0}, b={1}", nA, nB);
Thread.Sleep(1000);
return x + y;
}

  回复  引用  查看    

#14楼  2008-04-08 11:56 yellowyu      
谢谢你的解答,我再好好想想,有什么新想法再交流,谢谢 :)
  回复  引用  查看    


标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-04-07 01:40 编辑过
Google站内搜索

China-pub 计算机图书网上专卖店!6.5万品种 2-8折!
近千种 9-95 新二手计算图书火热销售中!
开发者征途系统新作:《设计模式——基于C#的工程化实现及扩展》




相关链接: