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