zoukankan      html  css  js  c++  java
  • 学习TPL(三)

    上集回顾

        上集讨论了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的时间还不是很多,所以这方面还有待进一步学习。

  • 相关阅读:
    Head First设计模式-单例模式
    mahout算法源码分析之Itembased Collaborative Filtering(四)共生矩阵乘法
    “非常PHP学习网”(www.veryphp.cn)一期上线
    IOS深入学习(19)之View object
    POJ 1005(累加)
    Plan04.学习与提升
    一个python
    【tcl脚本】改变输出字符格式
    为什么寄存器比内存快?
    我觉得epoll和select最大的区别
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/1716735.html
Copyright © 2011-2022 走看看