學習TPL(三)

上集回顧html

   上集討論了TPL的線程安全問題,以及很粗淺的討論了一些關於TPL的性能問題。這一集中,暫時拋開這些,直接來討論一下TPL帶來的一個很是強大的新功能——異步撤銷。c#

應用場景安全

   還記得線程池吧,利用線程池,按順序每秒輸出一個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。ide

新版的撤銷性能

   在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中的任務,就會自動並行的執行:

p_w_picpath

   選擇部分「是」,即:

p_w_picpath

   最後一個步驟0選否,就能夠得到以下結果:

p_w_picpath

   有沒有發現回滾的步驟和執行成功的步驟的順序有點不同?

   沒錯,由於執行這些步驟的時候是併發的,因此在撤銷的時候也是併發的,因此看到的結果是亂序的。能夠和前面一個純順序的執行方式比較一下,順序執行的撤銷是順序的,並行執行的撤銷是並行的,是否是很神奇?

思考

   看到這裏,是否還感受少了點什麼?

   若是撤銷失敗了哪?

   若是任務有子任務哪?




   確實,這方面值得思考的問題還有很多,可是,因爲接觸TPL的時間還不是不少,因此這方面還有待進一步學習。


http://www.cnblogs.com/vwxyzh/archive/2010/04/21/1716735.html

相關文章
相關標籤/搜索