上集回顾
上集讨论了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
撤销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中的任务,就会自动并行的执行:
选择部分“是”,即:
最后一个步骤0选否,就可以获得如下结果:
有没有发现回滚的步骤和执行成功的步骤的顺序有点不一样?
没错,因为执行这些步骤的时候是并发的,所以在撤销的时候也是并发的,所以看到的结果是乱序的。可以和前面一个纯顺序的执行方式比较一下,顺序执行的撤销是顺序的,并行执行的撤销是并行的,是不是很神奇?
思考
看到这里,是否还感觉少了点什么?
如果撤销失败了哪?
如果任务有子任务哪?
确实,这方面值得思考的问题还有不少,但是,由于接触TPL的时间还不是很多,所以这方面还有待进一步学习。
浙公网安备 33010602011771号