上集回顾

    上集讨论了TPL的线程安全问题,以及很粗浅的讨论了一些关于TPL的性能问题。这一集中,暂时抛开这些,直接来讨论一下TPL带来的一个非常强大的新功能——异步撤销。

应用场景

    还记得线程池吧,利用线程池,按顺序每秒输出一个0-9数字:

   1:              ThreadPool.QueueUserWorkItem(_ =>
   2:                  {
   3:                      for (int i = 0; i < 10; i++)
   4:                      {
   5:                          Console.WriteLine(i);
   6:                          Thread.Sleep(1000);
   7:                      }
   8:                  });

    但是,如果还要有取消功能哪?

    为了取消,我们不得不把代码写成这样:

   1:              bool isCancelled = false;
   2:              ThreadPool.QueueUserWorkItem(_ =>
   3:                  {
   4:                      for (int i = 0; i < 10; i++)
   5:                      {
   6:                          Console.WriteLine(i);
   7:                          if (isCancelled)
   8:                              break;
   9:                          Thread.Sleep(1000);
  10:                      }
  11:                  });

    认为很好吗?不,一点也不好,isCancelled会被多个线程访问,所以,为了保证CLR不会优化isCancelled,所以需要额外添加volatile关键字,给CLR一个Hint。

新版的撤销

    在4.0中,同样的操作可以被这样实现:

   1:              CancellationTokenSource cts = new CancellationTokenSource();
   2:              ThreadPool.QueueUserWorkItem(obj =>
   3:                  {
   4:                      CancellationToken token = (CancellationToken)obj;
   5:                      for (int i = 0; i < 10; i++)
   6:                      {
   7:                          Console.WriteLine(i);
   8:                          if (token.IsCancellationRequested)
   9:                              break;
  10:                          Thread.Sleep(1000);
  11:                      }
  12:                  }, cts.Token);
  13:              cts.Cancel();

    看起来没什么不同,只不过是把一个bool,修改成了一个类而已。

    至少现在看起来是这样,当时如果说撤销工作没有这么简单的,需要一个Rollback的动作,又会变成怎么样哪?

    此时,就会发现原来的方式非常难以搞定(至少让人犯错误),而使用新的撤销机制,可以写成这样:

   1:              CancellationTokenSource cts = new CancellationTokenSource();
   2:              ThreadPool.QueueUserWorkItem(obj =>
   3:                  {
   4:                      CancellationToken token = (CancellationToken)obj;
   5:                      for (int i = 0; i < 10; i++)
   6:                      {
   7:                          Console.WriteLine(i);
   8:                          if (token.IsCancellationRequested)
   9:                          {
  10:                              token.Register(() =>
  11:                                  {
  12:                                      for (int j = i; j >= 0; j--)
  13:                                      {
  14:                                          Console.WriteLine("撤销" + j);
  15:                                      }
  16:                                  });
  17:                              break;
  18:                          }
  19:                          Thread.Sleep(100);
  20:                      }
  21:                  }, cts.Token);
  22:              Thread.Sleep(500);
  23:              cts.Cancel();
  24:              Thread.Sleep(100);

    执行结果是:

0
1
2
3
4
5

撤销5

撤销4
撤销3
撤销2
撤销1
撤销0
请按任意键继续. . .

    利用CancellationToken.Register方法,可以在撤消时,额外执行一段撤销代码,因此,实现这种撤销时,实现就变的相当非常的简单。

    ThreadPool尚且可以这样玩,那么是不是该思考一下TPL和这种撤销的结合哪?

可撤销的并发任务

    先从最简单的任务开始吧,例如现在有个任务有10个步骤(抛开并发,现在只说顺序执行),每一步都可能失败,然后要求回滚,当然,理想状态下应该是3步都成功,然后提交,利用Task类,可以这样实现一个阶段式的任务:

   1:              using (var cancellation = new CancellationTokenSource())
   2:              using (var mres = new ManualResetEventSlim(false))
   3:              {
   4:                  // 添加一个rollbacked任务
   5:                  cancellation.Token.Register(() =>
   6:                  {
   7:                      Console.WriteLine("安装失败,并且成功回滚!");
   8:                      mres.Set();
   9:                  });
  10:                  Task[] tasks = new Task[10];
  11:                  // 添加一个Welcome任务
  12:                  var lastTask = Task.Factory.StartNew(() =>
  13:                  {
  14:                      Console.WriteLine("欢迎使用模拟安装向导!");
  15:                  });
  16:                  for (int i = 0; i < 10; i++)
  17:                  {
  18:                      // 知道c#闭包的语法准则的话,一定知道这句话的作用
  19:                      int j = i;
  20:                      tasks[j] = lastTask.ContinueWith(_ =>
  21:                      {
  22:                          // 直接用MessageBox了,偷懒了,呵呵
  23:                          if (MessageBox.Show("是否已经成功执行步骤" + j, "Test", MessageBoxButtons.YesNo) == DialogResult.Yes)
  24:                          {
  25:                              Console.WriteLine("执行步骤" + j + "已经成功。");
  26:                              // 为每次成功执行任务,添加对应的Rollback任务
  27:                              cancellation.Token.Register(() =>
  28:                              {
  29:                                  Console.WriteLine("回滚步骤" + j + "。");
  30:                              });
  31:                          }
  32:                          else
  33:                          {
  34:                              cancellation.Cancel();
  35:                          }
  36:                      }, cancellation.Token);
  37:                      lastTask = tasks[j];
  38:                  }
  39:                  // 添加一个completed任务
  40:                  var completedTask = lastTask.ContinueWith(_ =>
  41:                  {
  42:                      Console.WriteLine("安装成功!");
  43:                      mres.Set();
  44:                  }, cancellation.Token);
  45:                  mres.Wait();
  46:              }

    运行一个看看,全部步骤点Yes的结果如下:

欢迎使用模拟安装向导!
执行步骤0已经成功。
执行步骤1已经成功。
执行步骤2已经成功。
执行步骤3已经成功。
执行步骤4已经成功。
执行步骤5已经成功。
执行步骤6已经成功。
执行步骤7已经成功。
执行步骤8已经成功。
执行步骤9已经成功。
安装成功!
请按任意键继续. . .

    第0-5步点Yes,第6步点No的结果如下:

欢迎使用模拟安装向导!
执行步骤0已经成功。
执行步骤1已经成功。
执行步骤2已经成功。
执行步骤3已经成功。
执行步骤4已经成功。
执行步骤5已经成功。
回滚步骤5。
回滚步骤4。
回滚步骤3。
回滚步骤2。
回滚步骤1。
回滚步骤0。
安装失败,并且成功回滚!
请按任意键继续. . .

    是不是有点像那么回事情。但是,用着Task去不去做多任务并发,是不是感觉有点浪费?好,那么把刚才的10个任务修改成并行的看看,10个任务并行执行,如果全完成,则算成功,任何一个失败,就需要将之前的操作回滚。乍看起来有点难,不过,可以很简单的把之前的代码修改一下:

   1:              using (var cancellation = new CancellationTokenSource())
   2:              using (var mres = new ManualResetEventSlim(false))
   3:              {
   4:                  // 添加一个rollbacked任务
   5:                  cancellation.Token.Register(() =>
   6:                  {
   7:                      Console.WriteLine("安装失败,并且成功回滚!");
   8:                      mres.Set();
   9:                  });
  10:                  Task[] tasks = new Task[10];
  11:                  // 添加一个Welcome任务
  12:                  var welcomeTask = Task.Factory.StartNew(() =>
  13:                  {
  14:                      Console.WriteLine("欢迎使用模拟安装向导!");
  15:                  });
  16:                  for (int i = 0; i < 10; i++)
  17:                  {
  18:                      // 知道c#闭包的语法准则的话,一定知道这句话的作用
  19:                      int j = i;
  20:                      tasks[j] = welcomeTask.ContinueWith(_ =>
  21:                      {
  22:                          // 直接用MessageBox了,偷懒了,呵呵
  23:                          if (MessageBox.Show("是否已经成功执行步骤" + j, "Test", MessageBoxButtons.YesNo) == DialogResult.Yes)
  24:                          {
  25:                              Console.WriteLine("执行步骤" + j + "已经成功。");
  26:                              // 为每次成功执行任务,添加对应的Rollback任务
  27:                              cancellation.Token.Register(() =>
  28:                              {
  29:                                  Console.WriteLine("回滚步骤" + j + "。");
  30:                              });
  31:                          }
  32:                          else
  33:                          {
  34:                              cancellation.Cancel();
  35:                          }
  36:                      }, cancellation.Token);
  37:                  }
  38:                  // 添加一个congratulation任务
  39:                  var congratulationTask = Task.Factory.ContinueWhenAll(tasks, _ =>
  40:                  {
  41:                      Console.WriteLine("安装成功!");
  42:                      mres.Set();
  43:                  }, cancellation.Token);
  44:                  mres.Wait();
  45:              }

    看看修改了什么:

  • 删除lastTask = tasks[j];,因此tasks中的任务的前置任务都是welcome任务
  • 把原来congratulation任务的前置任务修改为tasks中的所有任务

    这样tasks中的任务,就会自动并行的执行:

image

    选择部分“是”,即:

image

    最后一个步骤0选否,就可以获得如下结果:

image

    有没有发现回滚的步骤和执行成功的步骤的顺序有点不一样?

    没错,因为执行这些步骤的时候是并发的,所以在撤销的时候也是并发的,所以看到的结果是乱序的。可以和前面一个纯顺序的执行方式比较一下,顺序执行的撤销是顺序的,并行执行的撤销是并行的,是不是很神奇?

思考

    看到这里,是否还感觉少了点什么?

    如果撤销失败了哪?

    如果任务有子任务哪?

 

 

 

    确实,这方面值得思考的问题还有不少,但是,由于接触TPL的时间还不是很多,所以这方面还有待进一步学习。

posted on 2010-04-21 00:08  Zhenway  阅读(1454)  评论(1编辑  收藏  举报