返回目录

Task.Unwrap基本使用

这个扩展方法定义在TaskExtensions类型中,命名空间在System.Threading.Tasks。Unwrap会把嵌套的Task<Task>或者Task<Task<T>>的结果提取出来。

就像这样,不用Unwrap的话:

static void Main(string[] args)

{

    doo();

    Task.Delay(-1).Wait();

}

static async void doo()

{

    //运行嵌套的Task

    //Task返回Task<Task<string>>

    //第一个await后result类型为Task<string>

    var result = await Task.Run<Task<string>>(() =>

        {

            var task = Task.Run<string>(() =>

                {

                    Task.Delay(1000).Wait();

                    return "Mgen";

                });

            return task;

        });

    //第二个await后才会返回string

    Console.WriteLine(await result);

}

使用Unwrap后,结果可以直接从嵌套Task中提取出来:

static async void doo()

{

    //运行嵌套的Task

    //Task返回Task<Task<string>>

    //await后类型为Task<string>,Unwrap后result类型为string

    var result = await Task.Run<Task<string>>(() =>

        {

            var task = Task.Run<string>(() =>

                {

                    Task.Delay(1000).Wait();

                    return "Mgen";

                });

            return task;

        }).Unwrap();

    //不需要await,result已经是string

    Console.WriteLine(result);

}

返回目录

Task.Factory.StartNew和Task.Run的Unwrap操作

简单地讲,Task.Factory.StartNew和Task.Run区别之一就有Task.Run会自动执行Unwrap操作,但是Task.Factory.StartNew不会,Task.Run就是Task.Factory.StartNew的更人性化封装,而Task.Factory.StartNew则是原始的执行。(另外关于更多的区别,推荐PFX Team的一篇非常给力的文章:Task.Run vs Task.Factory.StartNew)。

通过代码来验证:

var task1 = Task.Factory.StartNew(async () => "Mgen");

var task2 = Task.Run(async () => "Mgen");

Console.WriteLine(task1.GetType());

Console.WriteLine(task2.GetType());

输出:

System.Threading.Tasks.Task`1[System.Threading.Tasks.Task`1[System.String]]

System.Threading.Tasks.UnwrapPromise`1[System.String]

可以看到

使用Task.Factory.StartNew会返回原始的Task<Task<string>>。但是Task.Run则会直接返回async Lambda的结果,中间的Unwrap操作会自动进行。

返回目录

使用案例:LINQ中的async Lambda

文章讲述到这里,或许读者在想上述情况会不会很少见?不,任何使用async Lambda的情况都可能会出现上述情况,比如最近在搞一个WinRT的项目,使用LINQ去转换一些数据,但是许多WinRT的API只有异步执行的,这类问题就会出现,下方示例:

我们来进行一个再简单不过的LINQ Select操作,把一堆int转换成string,只不过转换过程是异步的,来看代码:

static void Main(string[] args)

{

    doo();

    Task.Delay(-1).Wait();

}

static void doo()

{

    //int数据

    var ints = Enumerable.Range(110);

    //转换并输出结果

    foreach (var str in ints.Select(async i => await Int2StringAsync(i)))

        Console.WriteLine(str);

}

//异步将int转换成string

static async Task<string> Int2StringAsync(int i)

{

    return await Task.Run<string>(() => i.ToString());

}

上面代码正确吗?很多人会认为没问题的,Select方法通过一个async Lambda调用异步转换方法,并使用await异步等待结果,那么async Lambda返回string,str类型也是string,最后输出所以字符串。

但事实上程序运行后会输出一连串的“System.Threading.Tasks.Task`1[System.String]”,str变量根本不是string,而是Task<string>。上边的推断错在(上面黄字标注的)“async Lambda返回string”,async Lambda的结果并没有被await,因此它会返回Task<string>,所以Select会返回一些列的Task<string>。

最简单的解决方案是,在处理结果的时候加上await,如下:

Console.WriteLine(await str);

但是在多数情况下,我们使用LINQ会直接赋值给数据对象,这个数据对象的类型是固定的,这里需要一种更彻底的解决方案,直接在LINQ Select上解决掉。

那么还有一种解决方案就是不使用async Lambda,就不存在嵌套Task的问题,直接在Select中返回异步方法的Task的Result属性:

//int数据

var ints = Enumerable.Range(110);

//Select调用异步方法

IEnumerable<string> strs = ints.Select(i => Int2StringAsync(i).Result);

如果一定要使用async Lambda,则必须将嵌套的Task进行Unwrap。(当然这里更多的是为了讨论技术本身,实际工作中没必要这么钻牛角尖呵呵。)

结合上面讲到的知识,使用Task.Factory.StartNew需要进行一个Unwrap,然后返回Task<T>的结果作为Select方法的最终返回值,代码:

//int数据

var ints = Enumerable.Range(110);

//Select调用异步方法

IEnumerable<string> strs = ints.Select(i =>

    Task.Factory.StartNew(async () => await Int2StringAsync(i)).Unwrap().Result);

而Task.Run的话,不需要Unwrap:

IEnumerable<string> strs = ints.Select(i =>

    Task.Run(async () => await Int2StringAsync(i)).Result);

作者:Mgen

本文版权归作者所有,欢迎以网址(链接)的方式转载,不欢迎复制文章内容的方式转载,其一是为了在搜索引擎中去掉重复文章内容,其二复制后的文章往往没有提供本博客的页面格式和链接,造成文章可读性很差。望有素质人自觉遵守上述建议。

如果一定要以复制文章内容的方式转载,必须在文章开头标明作者信息和原文章链接地址。否则保留追究法律责任的权利。