欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

.NET中的async和await关键字使用及Task异步调用实例

程序员文章站 2024-02-22 08:28:34
其实早在.net 4.5的时候m$就在.net中引入了async和await关键字(vb为async和await)来简化异步调用的编程模式。我也早就体验过了,现在写一篇日志...

其实早在.net 4.5的时候m$就在.net中引入了async和await关键字(vb为async和await)来简化异步调用的编程模式。我也早就体验过了,现在写一篇日志来记录一下顺便凑日志数量(以后面试之前可以用这个“复习”一下)。

(一)传统的异步调用

在比较“古老”的c#程序中经常可以看到iasyncresult、begininvoke之类的异步调用“踪迹”。先来简单的复习一下吧。

假如我们有一个方法生成字符串,而生成这个字符串需要10秒中的时间:

复制代码 代码如下:

public class wastetimeobject
{
    public string getslowstring(int begin, int length)
    {
        stringbuilder sb = new stringbuilder();

        for (int i = begin; i < begin + length; i++)
        {
            sb.append(wastetime(i) + " ");
        }

        return sb.tostring();
    }

    private string wastetime(int current)
    {
        system.threading.thread.sleep(1000);
        return current.tostring();
    }
}

我们再做一个窗口,用来请求这个方法并把字符串显示到文本框中。使用同步调用肯定会把ui线程阻塞掉,要想不把ui阻塞掉就要另起一个线程了。基本的步骤如下:

创建一个异步调用的委托:

复制代码 代码如下:

public delegate string getslowstringdelegate(int begin, int length);

然后呢,再异步调用这个委托:

复制代码 代码如下:

private void button1_click(object sender, eventargs e)
{
    wastetimeobject ad = new wastetimeobject();
    getslowstringdelegate d = ad.getslowstring;

    textbox1.text = "requesting string, please wait...";

    iasyncresult ar = d.begininvoke(1, 10, taskcomplete, d);
}

这里的begininvoke会在原来的基础上再附加两个参数:表示执行完毕后的回调方法asynccallback,最后一个参数可以是任何对象,以便从回调方法中访问它。不过一般情况都是传递的委托实例,以便获取调用的结果。

当然我们也可以不用回调方法,这样就只好不断地循环查询是否执行完成了。

然后我们就要编写asynccallback这个回调方法了,它接受一个iasyncresult类型的对象表示异步调用的结果:

复制代码 代码如下:

private void taskcomplete(iasyncresult ar)
{
    if (ar == null) return;
    getslowstringdelegate d = ar.asyncstate as getslowstringdelegate;
    if (d == null) throw new exception("invalue object type");
    string result = d.endinvoke(ar);
    this.invoke(new action(() => updatetextresult(result)));
}

调用委托实例的endinvoke方法并传入iasyncresult类型的对象用以获取getslowstring的返回结果。

回调方法是委托线程调用的,因此它不能直接访问ui,所以我们使用窗体的invoke方法在主线程中显示结果。如果委托方法抛出异常,将会在endinvoke时抛出。

(二)使用task类型

可以看到使用传统的办法编写异步调用很麻烦,特别是如果这种调用很多,那么我们的程序就会变成很复杂,逻辑很乱。

.net 4.5提供的新的异步变成模式就很好地解决了这个问题(其实本质上应该是.net自动实现了很多操作),使编写异步代码和同步调用一样逻辑清晰。

首先来看看微软的例子:

复制代码 代码如下:

private async task sumpagesizesasync()
{
    // to use the httpclient type in desktop apps, you must include a using directive and add a
    // reference for the system.net.http namespace.
    httpclient client = new httpclient();

    // equivalently, now that you see how it works, you can write the same thing in a single line.
    byte[] urlcontents = await client.getbytearrayasync(url);
    // . . .
}

可以看出,使用await关键字后,.net会自动把返回结果包装在一个task类型的对象中。对于这个示例,方法是没有返回结果的。而对有返回结果的方法,就要使用task<t>了:

复制代码 代码如下:

public async task<string> waitasynchronouslyasync()
{
    await task.delay(10000);
    return "finished";
}

总而言之,使用await表达式时,控制会返回到调用此方法的线程中;在await等待的方法执行完毕后,控制会自动返回到下面的语句中。发生异常时,异常会在await表达式中抛出。

对于我们这个例子,我们编写的代码如下:

复制代码 代码如下:

private async void button1_click(object sender, eventargs e)
{
    textbox1.text = "requesting string, please wait...";

    wastetimeobject ad = new wastetimeobject();

    string result = await task.run(() => ad.getslowstring(1, 10));

    //update ui to display the result
    textbox1.text = result;
}

我们使用task类新建一个工作线程并执行。当然我们也可以像m$给的例子那样改造一下getslowstring,这样就不需要加上task.run了。(基本上,这种方法都会以async后缀结尾。)

如何?原来的:创建异步委托→回调一气呵成。另外还有一点,await下面的语句是由主线程调用的,不是由新的线程调用,所以我们可以直接访问ui。

(三)取消执行和显示进度

最后一个要记录的,就是如何给异步调用添加进度条,并能让用户取消操作。界面就是下面这样:

.NET中的async和await关键字使用及Task异步调用实例

使用最终完成的代码来说明吧。首先改造getslowstring方法,使之支持取消和汇报进度:

复制代码 代码如下:

public string getslowstring(int begin, int length, iprogress<int> progress, cancellationtoken cancel)
{
    stringbuilder sb = new stringbuilder();

    for (int i = begin; i < begin + length; i++)
    {
        sb.append(wastetime(i) + " ");

        cancel.throwifcancellationrequested();

        if (progress != null)
            progress.report((int)((double)(i - begin + 1) * 100 / length));
    }

    return sb.tostring();
}

iprogress<t>类型的对象有一个report方法,执行这个方法实际上会调用自定义的更新进度的方法,这个方法(使用委托或匿名方法皆可)是在生成progress<t>对象的时候指定的:

复制代码 代码如下:

iprogress<int> progress = new progress<int>((progressvalue) => { progressbar1.value = progressvalue; });

神奇的是,这个方法是由主线程调用的,如果不是这样,它就不能更新我们界面上的控件。所以说微软提供的新机制帮我们简化了很多工作。

cancellationtoken用于指定该方法“绑定”的取消上下文,如果这个对象执行过cancel方法(用户点击了cancel按钮),那么访问throwifcancellationrequested时就会抛出operationcanceledexception类型的异常。这种机制的灵活性在于中止执行的位置是可以自行确定的,不会出现取消时自己都不知道执行到哪行代码的情况。

总而言之,单击request按钮的代码我们修改如下:

复制代码 代码如下:

private async void button1_click(object sender, eventargs e)
{

    cancelsource = new cancellationtokensource();
    iprogress<int> progress = new progress<int>((progressvalue) => { progressbar1.value = progressvalue; });

    textbox1.text = "requesting string, please wait...";
    button1.enabled = false; button2.enabled = true;

    wastetimeobject ad = new wastetimeobject();

    try
    {
        string result = await task.run(() => ad.getslowstring(1, 10, progress, cancelsource.token),
            cancelsource.token);
        //update ui to display the result
        textbox1.text = result;
        button2.enabled = false;  //disable cancel button
    }
    catch (operationcanceledexception)
    {
        textbox1.text = "you canceled the operation.";
    }

}

取消按钮的代码就很简单了:

复制代码 代码如下:

private void button2_click(object sender, eventargs e)
{
    if (cancelsource != null) cancelsource.cancel();
    button2.enabled = false;
}

至此,task机制的初步体验就到此完成。以后有机会在研究下更高阶的内容吧。