Evil 域

当Evil遇上先知

导航

选好 Async 函数的返回类型

Posted on 2011-08-15 10:13  Saar  阅读(3813)  评论(4编辑  收藏  举报

C# 5.0功能之Async一瞥中,简单的介绍了Async CTP的使用,我们一起领略了下一版本的C#可能给我们带来的强大而简单的编写异步执行的代码的方法。

文中提到一个异步方法的返回值有三个选项:

  • void
  • Task
  • Task<T>

什么时候使用哪一种返回类型,是有讲究的。一不小心使用不当,会让代码产生意想不到的结果。为了避免在将同步代码改成异步代码时出现返回类型选择不恰当的情况,给大家介绍ASync选择返回类型的三法则

image

(图片来自Bing搜索)

(还是申明一下:本文的例子基于Async CTP SP1 Refresh完成。由于 Async还处于CTP阶段,很多东西还在讨论,因此,也许到正式发布的时候,细节还会变动。)

 

假设有一个学生类,包括学号ID姓名Name属性:

   1:      public class Student
   2:      {
   3:          public int ID { get; set; }
   4:          public string Name { get; set; }
   5:   
   6:          public override string ToString()
   7:          {
   8:              return string.Format("ID: {0}, Name: {1}.", ID, Name);
   9:          }
  10:      }

然后,有一组学生记录的StudentRepository:

   1:      public class StudentRepository
   2:      {
   3:          ICollection<Student> storage;
   4:   
   5:          public ICollection<Student> GetStudents()
   6:          {
   7:              var studentCollection = CreateStudents();
   8:              UpdateNameUnknownStudent(studentCollection);
   9:              return studentCollection;
  10:          }
  11:   
  12:          private ICollection<Student> CreateStudents()
  13:          {
  14:              Thread.Sleep(2000);
  15:              storage = new Collection<Student>()
  16:              {
  17:                  new Student(){ ID=1 },
  18:                  new Student(){ ID=2 }
  19:              };
  20:              return storage;
  21:          }
  22:   
  23:          private void UpdateNameUnknownStudent(ICollection<Student> studentCollection)
  24:          {
  25:              foreach (var student in studentCollection)
  26:              {
  27:                  Thread.Sleep(1000); // Someoperation time like db access or so.
  28:                  student.Name = student.Name ?? "Unknown";
  29:              }
  30:          }
  31:      }

其中,12行的CreateStudents方法模拟了学生对象的创建,它返回一个学生集合;

第23行UpdateNameUnkownStudent方法遍历学生集合,并且,如果学生的姓名为空,将学生姓名设置成“Unknown”

第5行GetStudents()作为公开的接口,先后调用CreateStudnets和UpdateNameUnknownStudent方法,并将结果返回。

为了模拟现实代码中例如数据库等操作的处理时间,在第14行和第27行分别加了延时。

 

然后,创建一个WPF UI,调用上面的StudentRepository类,把结果存放到一个Label中。

界面如下:

image

Demo按钮的事件处理代码如下:

   1:          private void btnDemo_Click(object sender, RoutedEventArgs e)
   2:          {
   3:              StudentRepository repository = new StudentRepository();
   4:              var studentCollection = repository.GetStudents();
   5:              StringBuilder builder = new StringBuilder(studentCollection.Count);
   6:              foreach (var student in studentCollection)
   7:              {
   8:                  builder.AppendLine(student.ToString());
   9:              }
  10:              lblResult.Content = builder.ToString();
  11:          }

执行代码,虽然代码能够得到正确结果,但是,在点击Demo按钮后,界面出现了几秒钟的死锁。为了提供更好的用户体验,我们决定把学生创建和为空名学生记录添加Unknown的方法改写成异步方法。

image

(图片来自Bing搜索)

 

首先看CreateStudents方法,它返回一个ICollection<Student>的集合。

  • 法则一:对于需要返回对象的方法,我们添加async关键字后,改为返回Task<T>,也即,改为:
   1:          private async Task<ICollection<Student>> CreateStudentsAsync()
   2:          {
   3:              await TaskEx.Delay(2000);
   4:              storage = new Collection<Student>()
   5:              {
   6:                  new Student(){ ID=1 },
   7:                  new Student(){ ID=2 }
   8:              };
   9:              return storage;
  10:          }

 

接下来,我们看UpdateNameUnknownStudent方法,由于它不返回结果,我们直接加上async:

 
   1:          private async void UpdateNameUnknownStudentAsync(ICollection<Student> studentCollection)
   2:          {
   3:              foreach (var student in studentCollection)
   4:              {
   5:                  await TaskEx.Delay(1000); // Someoperation time like db access or so.
   6:                  student.Name = student.Name ?? "Unknown";
   7:              }
   8:          }

相应的GetStudent方法和Demo按钮也加上async关键字和await等待结果后,重新编译运行。点击Demo按钮。我们发现,界面是不死锁了,但是结果出错了(就说会产生意想不到的结果吧):

ID: 1, Name: .
ID: 2, Name: .

 

显然,UpdateNameUnknownStudentAsync没有运行。让我们来仔细的看一下执行流

首先,Demo按钮Click事件调用者调用GetStudentAsync,为理清调用层次,我们记btnDemo_Click方法为L1方法

   1:          private async void btnDemo_Click(object sender, RoutedEventArgs e)
   2:          {
   3:              StudentRepository repository = new StudentRepository();
   4:              var studentCollection = await repository.GetStudentsAsync();
   5:              StringBuilder builder = new StringBuilder(studentCollection.Count);
   6:              foreach (var student in studentCollection)
   7:              {
   8:                  builder.AppendLine(student.ToString());
   9:              }
  10:              lblResult.Content = builder.ToString();
  11:          }

当执行到第4行时,遇到await关键字,调用GetStudentsAsync方法(记为L2方法),并且进入等待状态,等待L2结果

GetStudentAsync(L2)此时代码如下:

   1:          public async Task<ICollection<Student>> GetStudentsAsync()
   2:          {
   3:              var studentCollection = await CreateStudentsAsync();
   4:              UpdateNameUnknownStudentAsync(studentCollection); // We have a problem here …
   5:              return studentCollection;
   6:          }
当它执行第3行代码,调用CreateStudnetsAsync以后,遇到await关键字,等待CreateStudentAsync的完成。
CretaeStudent方法完成后,L2方法继续执行到调用UpdateNameUnknownStudentAsync方法(记为L3方法):
   1:          private async void UpdateNameUnknownStudentAsync(ICollection<Student> studentCollection)
   2:          {
   3:              foreach (var student in studentCollection)
   4:              {
   5:                  await TaskEx.Delay(1000); // Someoperation time like db access or so.
   6:                  student.Name = student.Name ?? "Unknown";
   7:              }
   8:          }
L3执行到第5行时,遇到了await,当前线程开始等待TaskEx.Delay完成;同时,它会检查它的调用者(L2)是否有代码可以在当前线程上执行。由于在L2中,并没有await L3方法,因此,当L3中await一个结果时,L2所在进程会执行下一个语句:return studentCollection。
L1中的await等到了L2的return,进而进行下一语句……然而,此时此刻,添加Unknown的M3其实还没有完成。前面看到的残缺的结果就这样被显示了出来。
 
引入这个问题的原因是,由于在有async关键字的情况下,void或者返回Task都不需要在代码中显式的使用return,我们在改写UpdateNameStudentAsync方法时,没有仔细考虑应当使用void还是Task作为返回类型。因此,到底是返回Task还是保持void是一个在刚开始使用Async时经常遇到的问题。
明白了代码执行流以后,判断方法就不难了:
  • 法则二、当一个方法属于触发后不用理会什么时候完成的方法,可以直接使用void例如事件处理函数(Event Handler);
  • 法则三、当虽然不需要返回结果,但却需要知道是否执行完成的方法时,返回一个Task,例如示例中的UpdateNameUnknownStudnetAsync方法。
 
在把示例中UpdateNameUnknownStudentAsync方法改成如下形式,并且在GetStudentAsync方法中await它以后,我们便可以得到预期的结果了:
   1:          private async Task UpdateNameUnknownStudentAsync(ICollection<Student> studentCollection)
   2:          {
   3:              foreach (var student in studentCollection)
   4:              {
   5:                  await TaskEx.Delay(1000); // Someoperation time like db access or so.
   6:                  student.Name = student.Name ?? "Unknown";
   7:              }
   8:          }
 
image
 
写在最后
Async/await的引入虽然为提供了我们书写异步执行代码的捷径,但是,要用好这把双刃剑,得在理解Async代码的执行流程的基础上不断总结多练多用啊。
 
 

资源下载:

* Sync 版源代码

* Async 版源代码

* Async CTP SP1 Refresh.