免费的cms视频网站模板,东方购物商城,无锡百姓网推广代理商,中国建设银行下载安装目录
11.1 与并发相关的错误类型
11.1.1 不必要的阻塞
11.1.2 条件竞争
11.2 定位并发错误的技术
11.2.1 代码审阅——发现潜在的错误
11.2.2 通过测试定位并发相关的错误
11.2.3 可测试性设计
11.2.4 多线程测试技术
11.2.5 构建多线程测试代码
11.2.6 测试多线程代…
目录
11.1 与并发相关的错误类型
11.1.1 不必要的阻塞
11.1.2 条件竞争
11.2 定位并发错误的技术
11.2.1 代码审阅——发现潜在的错误
11.2.2 通过测试定位并发相关的错误
11.2.3 可测试性设计
11.2.4 多线程测试技术
11.2.5 构建多线程测试代码
11.2.6 测试多线程代码性能 参考《C 并发编程实战》 著Anthony Williams 译 吴天明
11.1 与并发相关的错误类型
有一些错误与使用并发直接相关本章重点关注这些错误。通常并发相关的错误通常有两大类 不必要的阻塞 条件竞争
11.1.1 不必要的阻塞
这个主题可以分成以下几个问题 死锁——如在第3章所见死锁的情况下两个线程会互相等待。当线程产生死锁应该完成的任务就会持续搁置。举个例子来说一些线程是负责对用户界面操作的线程在死锁的情况下用户界面就会无响应。另一些例子中界面接口会保持响应不过有些任务就无法完成比如查询无结果返回或文档未打印。 活锁——与死锁的情况类似。不同的地方在于线程不是阻塞等待而是在循环中持续检查例如自旋锁。比较严重的情况下其表现和死锁一样(应用不会做任何处理停止响应)CPU的使用率还居高不下因为线程还在循环中被检查而不是阻塞等待。不太严重的情况下因为使用随机调度活锁的问题还是可以解决的。 I/O阻塞或外部输入——当线程被外部输入所阻塞线程也就不能做其他事情了(即使等待输入的情况永远不会发生)。因此被外部输入所阻塞就会让人不太高兴因为可能有其他线程正在等待这个线程完成某些任务。
11.1.2 条件竞争
条件竞争经常会产生以下几种类型的错误 数据竞争——因为未同步访问一块共享内存将会导致代码产生未定义行为。第5章已经介绍了数据竞争也了解了C的内存模型。数据竞争通常发生在错误的使用原子操作上做同步线程的时候或没使用互斥量保护共享数据的时候。 破坏不变量——主要表现为悬空指针(因为其他线程已经将要访问的数据删除了)随机存储错误(因为局部更新导致线程读取了不一样的数据)以及双重释放(比如当两个线程对同一个队列同时执行pop操作想要删除同一个关联数据)等等。不变量被破坏可以看作为“基于数据”的问题。当独立线程需要以一定顺序执行某些操作时错误的同步会导致条件竞争比如顺序被破坏。 生命周期问题——虽然这类问题也能归结为破坏了不变量不过这里将其作为一个单独的类别给出。这里的问题是线程会访问不存在的数据这可能是因为数据被删除或销毁了或者转移到其他对象中去了。生命周期问题通常是在一个线程引用了局部变量在线程还没有完成前局部变量的“死期”就已经到了不过这个问题并不止存在这种情况下。当手动调用join()等待线程完成工作需要保证异常抛出的时候join()还会等待其他未完成工作的线程。这是线程中基本异常安全的应用。
11.2 定位并发错误的技术
11.2.1 代码审阅——发现潜在的错误
如果让你的同事来审阅代码他/她肯定对你的代码不是很熟悉。因此他/她会从不同的角度来看你的代码然后指出你没有注意的事情。如果你的同事都没有空你可以叫朋友或传到网络上让网友审阅(注意别传一些机密代码上去)。实在没有人审阅不要着急。对于初学者可以将代码放置一段时间——先去做应用的另外的部分或是阅读一本书籍亦或出去溜达溜达。在休息之后当再集中注意力做某些事情(潜意识会考虑很多问题)。同样当你做完其他事情回头再看这段代码就会有些陌生——你可能会从另一个角度来看你自己以前写的代码。 另一种方式就是自己审阅。可以向别人详细的介绍你所写的功能可能并不是一个真正的人——可能要对玩具熊或橡皮鸡来进行解释并且我个人觉得写一些比较详细的注释是非常有益的。在解释过程中会考虑每一行过后会发生什么事情有哪些数据被访问了等等。问自己关于代码的问题并且向自己解释这些问题。我觉得这是种非常有效的技巧——通过自问自答对每个问题认真考虑这些问题往往都会揭示一些问题也会有益于任何形式的代码审阅。
审阅多线程代码需要考虑的问题 并发访问时哪些数据需要保护 如何确定访问数据受到了保护 是否会有多个线程同时访问这段代码 这个线程获取了哪个互斥量
其他线程可能获取哪些互斥量 两个线程间的操作是否有依赖关系如何满足这种关系 这个线程加载的数据还是合法数据吗数据是否被其他线程修改过 当假设其他线程可以对数据进行修改这将意味着什么并且怎么确保这样的事情不会发生 我最喜欢最后一个问题因为它让我去考虑线程之间的关系。通过假设一个bug和一行代码相关联你就可以扮演侦探来追踪bug出现的原因。为了让你自己确定代码里面没有bug需要考虑代码运行的各种情况。数据被多个互斥量所保护时这种方式尤其有用比如使用线程安全队列(第6章)可以对队头和队尾使用独立的互斥量就是为了确保在持有一个互斥量的时候访问是安全的必须保持有其他互斥量的线程不能同时访问同一元素。还需要特别关注的是对公共数据的显式处理使用一个指针或引用的方式来获取数据。 倒数第二个问题也很重要因为这里很容易产生错误先释放再获取一个互斥量的前提是其他线程可能会修改共享数据。虽然很明显但当互斥锁不是立即可见——可能因为是内部对象——就会不知不觉的掉入陷阱中。第6章已经了解到这种情况是怎么引起条件竞争的以及如何给细粒度线程安全数据结构带来麻烦的。不过非线程安全栈将top()和pop()操作分开是有意义的当多线程并发的访问这个栈问题会马上出现因为在两个操作的调用间内部互斥锁已经被释放并且另一个线程对栈进行了修改。解决方案就是将两个操作合并就能用同一个锁来对操作的执行进行保护就消除了条件竞争的问题。
11.2.2 通过测试定位并发相关的错误
11.2.3 可测试性设计
测试多线程代码很困难所以需要将其变得简单一些。很重要的一件事就是设计代码时考虑其的可测试性。可测试的单线程代码设计已经说烂了而且其中许多建议现在依旧适用。通常如果代码满足一下几点就很容易进行测试
每个函数和类的关系都很清楚。 函数短小精悍。 测试用例可以完全控制被测试代码周边的环境。 执行特定操作的代码应该集中测试而非分布式测试。
需要在完成编写后考虑如何进行测试。 以上这些在多线程代码中依旧适用。实际上我会认为对多线程代码的可测试性要比单线程的更为重要因为多线程的情况更加复杂。最后一个因素尤为重要即使不在写完代码后去写测试用例这也是一个很好的建议能让你在写代码之前想想应该怎么去测试它——用什么作为输入什么情况看起来会让结果变得糟糕以及如何激发代码中潜在的问题等等。
并发代码测试的一种最好的方式去并发化测试。如果代码在线程间的通讯路径上出现问就可以让一个已通讯的单线程进行执行这样会减小问题的难度。在对数据进行访问的应用进行测试时可以使用单线程的方式进行。这样线程通讯和对特定数据块进行访问时只有一个线程就达到了更容易测试的目的。
例如当应用设计为一个多线程状态机时可以将其分为若干块。将每个逻辑状态分开就能保证对于每个可能的输入事件、转换或其他操作的结果是正确的这就是单线程测试的技巧测试用例提供的输入事件将来自于其他线程。之后核心状态机和消息路由的代码就能保证时间能以正确的顺序正确的传递给可单独测试的线程上不过对于多并发线程需要为测试专门设计简单的逻辑状态。 或者如果将代码分割成多个块(比如读共享数据/变换数据/更新共享数据)就能使用单线程来测试变换数据的部分。麻烦的多线程测试问题转换成单线程测试读和更新共享数据就会简单许多。某些库会用其内部变量存储状态时需要小心当多线程使用同一库中的函数这个状态就会被共享。这的确是一个问题并且问题不会马上出现在访问共享数据的代码中。不过随着你对这个库的熟悉就会清楚这样的情况会在什么时候出现。之后可以适当的加一些保护和同步或使用B计划——让多线程安全并发访问的功能。
11.2.4 多线程测试技术
蛮力测试 代码有问题的时候就要求蛮力测试一定能看到这个错误。这就意味着代码要运行很多遍可能会有很多线程在同一时间运行。要是有bug出现只能线程出现特殊调度的时候代码运行次数的增加就意味着bug出现的次数会增多。 当有几次代码测试通过你可能会对代码的正确性有一些信心。如果连续运行10次都通过你就会更有信心。如果你运行十亿次都通过了那么你就会认为这段代码没有问题了。 自信的来源是每次测试的结果。如果你的测试粒度很细就像测试之前的线程安全队列那么蛮力测试会让你对这段代码持有高度的自信。另一方面当测试对象体积较大的时候调度序列将会很长即使运行了十亿次测试用例也不让你对这段代码产生什么信心。
组合仿真测试 名字比较口语化我需要解释一下这个测试是什么意思使用一种特殊的软件用来模拟代码运行的真实情况。你应该知道这种软件能让一台物理机上运行多个虚拟环境或系统环境而硬件环境则由监控软件来完成。除了环境是模拟的以外模拟软件会记录对数据序列访问上锁以及对每个线程的原子操作。然后使用C内存模型的规则重复的运行从而识别条件竞争和死锁。 虽然这种组合测试可以保证所有与系统相关的问题都会被找到不过过于零碎的程序将会在这种测试中耗费太长时间因为组合数目和执行的操作数量将会随线程的增多呈指数增长态势。这个测试最好留给需要细粒度测试的代码段而非整个应用。另一个缺点就是代码对操作的处理往往会依赖与模拟软件的可用性。 所以测试需要在正常情况下运行很多次不过这样可能会错过一些问题也可以在一些特殊情况下运行多次不过这样更像是为了验证某些问题。
使用专用库对代码进行测试 虽然这个选择不会像组合仿真的方式提供彻底的检查不过可以通过特别实现的库(使用同步原语)来发现一些问题比如互斥量锁和条件变量。例如访问某块公共数据的时候就要将指定的互斥量上锁。数据被访问后发现一些互斥量已经上锁就需要确定相关的互斥量是否被访问线程锁住如果没有测试库将报告这个错误。当需要测试库对某块代码进行检查时可以对相应的共享数据进行标记。 多个互斥量同时被一个线程持有时测试库也会对锁的序列进行记录。如果其他线程以不同的顺序进行上锁即使在运行的时候测试用例没有发生死锁测试库都会将这个行为记录为“有潜在死锁”可能。 测试多线程代码时另一种库可能会用到以线程原语实现的库比如互斥量和条件变量当多线程代码在等待或是被条件变量通过notify_one()提醒的某个线程测试者可以通过线程获取到锁。就可以让你来安排一些特殊的情况以验证代码是否会在这些特定的环境下产生期望的结果。 C标准库实现中某些测试工具已经存在于标准库中没有实现的测试工具可以基于标准库进行实现。
11.2.5 构建多线程测试代码
在特定时间内需要安排一系列线程同时去执行指定的代码段。两个线程的情况就很容易扩展到多个线程。 首先需要知道每个测试的不同之处 环境布置代码必须首先执行 线程设置代码需要在每个线程上执行 线程上执行的代码需要有并发性 并发执行结束后后续代码需要对代码的状态进行断言检查
了解了各个代码块就需要保证所有事情按计划进行。一种方式是使用一组使用承诺值来表示是否准备好然后让std::promise等待(复制)一个std::promise std::shared_future来表示就绪状态。每个线程主线程会等待每个线程上的承诺值设置后才按下“开始”键。这就能保证每个线程能够同时开始并且在准备代码执行完成后并发代码就可以开始执行了任何线程的特定设置都需要在设置承诺值前完成。最终主线程会等待所有线程完成并且检查其最终状态。还需要格外关心异常所有线程在准备好的情况下再按下“开始”键否则未准备好的线程就不会运行。
清单11.1 对一个队列并发调用push()和pop()的测试用例
1.void test_concurrent_push_and_pop_on_empty_queue()
2.{
threadsafe_queueint q;
3.
// 1
4.
5.std::promisevoid go,push_ready,pop_ready;
6.std::shared_futurevoid ready(go.get_future());
// 2
// 3
7.
8.std::futurevoid push_done;
9.std::futureint pop_done;
// 4
10.
11.try
12.{
push_donestd::async(std::launch::async,
13.
// 5
14.[q,ready,push_ready]()
15.{
16.push_ready.set_value();
17.ready.wait();
q.push(42);
18.
}
19.
);
20.
pop_donestd::async(std::launch::async,
21.
// 6
22.[q,ready,pop_ready]()
23.{
24.pop_ready.set_value();
25.ready.wait();
26.return q.pop();
// 7
}
27.
);
28.
29.push_ready.get_future().wait();
30.pop_ready.get_future().wait();
31.go.set_value();
// 9
33.push_done.get();
// 10
34.assert(pop_done.get()42);
// 8
32.
assert(q.empty());
35.
36.}
37.catch(...)
38.{
go.set_value();
39.
// 12
throw;
40.
}
41.
42.
// 11
}
11.2.6 测试多线程代码性能 选择以并发的方式开发应用就是为了能够使用日益增长的处理器数量通过处理器数量的增加来提升应用的执行效率。因此确定性能是否有真正的提高就很重要了(就像其他优化一样)。 并发效率中有个特别的问题——可扩展性——你希望代码能很快的运行24次或在24芯的机器上对数据进行24(或更多)次处理或其他等价情况。如8.4.2节中所述当有重要的代码以单线程方式运行时就会限制性能的提高。因此在做测试之前回顾一下代码的设计结构是很有必要的这样就能判断代码在24芯的机器上时性能会不会提高24倍或是因为有串行部分的存在最大的加速比只有3。 对数据访问时处理器之间会有竞争会对性能有很大的影响。需要合理的权衡性能和处理器的数量处理器数量太少就会等待很久处理器过多又会因为竞争的原因等待很久。 因此在对应的系统上通过不同的配置检查多线程的性能就很有必要这样可以得到一张性能图。最起码(如果条件允许)需要在一个单处理器的系统上和一个多处理核芯的系统上进行测试。