注意事项:
1. volatile 关键字并非每种编译器都支持, 要写通用的代码只能够采用C++标准的关键字.
2. 任何被线程读取的值, 即使有同步, 也只有以指针形式访问其内容才能够获取最新值, 或者是以非inline的函数调用才能够获取最新值, inline会导致编译器有可能优化成不是每次访问变量地址的数据, 而是直接从上次一在寄存器里面值.
3. 线程处理过程当中, 每一步执行之前, 必须得要假设另外的线程会把处理代码都跑一遍甚至跑好几遍, 以此来排除非同步的错误, 否则将会出现运行一段时期, 随机出现各种不同的错误.
4. 多线程编写, 均必须以全速来测试, 所以不能用printf等的排错, 用上printf, 就有可能把潜在的错误给掩盖起来, 导致问题不能被发现.
5. 多线程编写, 必须以资源访问划分来编写模型, 并对模型进行测试, 按业务模型来直接写, 将会导致整个程序不稳定, 这是非常危险的, 因为这样的BUG几乎不可能修复.
6. 采用临界区并非百分百安全, 线程安全都是必须用精确的逻辑来进行推断, 不能估算大概时间, 也不能用自认为的流程来推断, 必须采用电脑的运行逻辑来推断, 即: 除非被锁或没信号, 否则就能够运行N遍的逻辑, 更不能够用Sleep....
7. 资源划分, 是多线程编写的重点, 即同一个资源会被多少个线程同时访问, 哪种访问方式需要同步, 哪种不需要同步, 必须清楚.
8. 不要盲目使用多线程, 不能够并行的情况, 误用多线程只会带来缓慢.
9. 存在多个线程大并发对某个资源进行读和写的操作, 绝大部分会是资源划分上的错误. 采用单线程的方式, 效率不会比多个线程低下.
高效误区和方案:
1. 误区: 存在大并发的写, 而资源是采用临界区来进行保护, 这种方式只会导致多个线程被挤压成单线程运行, 跟自动扶梯原理一样, 一个梯级只能够进入一个人, 再多人一起拥进去, 速度还只会是一次从出口出一个.
__方案: 采用各自写入, 在取出的时候再归总才能达到实际的写并发. 写之间的竞争缩减成写和读一对一的竞争, 而只有在读到某一个线程的写入时, 才会跟这个写竞争, 其他写并不影响. (假如只读, 则完全不存在竞争关系), 并且不论是全局的写读顺序还是单一写的顺序均可以保证.
2. 误区: 存在大并发的读, 而资源在读取的时候进行删除, 这种方式也同样会导致上述的情况, 因为删除就等于包括了写.
__方案: 要实现不会重复读取已经读取的内容, 可采用Interlock的模式进行, 假如读取资源是数组, Interlock出来的值就是下标值, 直接访问即可, 假如读取出来的并非数组而是链表式, 可采用Interlock的模式生成下标, 然后在链表搜索相对位置, 或者是从上一位置前往下一位置进行读取, 较简单的方式可采用Interlock模式, 读取之前判断返回值, 为1以上代表已经读取过, 继续往链表的下一个位置进行读取. 然后删除则可以循环链表头, 判断当前结点是否已经满足有N个线程读取过了, 来进行删除, 或者说是读取全部完成后, 才删除链表.
3. 误区: 读写不频繁时, 认为采用临界区就会百分百安全. 原子链表是必须根据实际情况, 把重要的写入, 读取和删除容入业务逻辑才会正确的. 假如一份数据需要写10次才完成, 一个线程写到一半的时候, 另一个线程发现错误而执行了清空操作, 那么另一个写线程并不可能得知情况, 而读线程由于没有立即得到通知而读取了脏数据.
__方案: 原子链表可以由多线程读写, 但只能由主控制线程进行清空. 正确的错误停止方式是主控制线程设置错误, 然后等待其他线程停止去原子链表的访问后, 才进行删除数据.
4. 误区: 认为临界区安全, 效率非常高. 应该多使用. 没错, 临界区的API调用很快, 能够有效保护代码段. 但就算代码段粒度再少, 只要有临界区的模式, 就会容易变成瓶颈. 因为临界区的实质是把并行转变成串行来实现同步的. 并且假如临界区当中, 还存在任何的Wait来等待信号量的话, 就极有可能死锁.
__方案: 必须深刻理解好各种同步对象适用的场合. 临界区, 只适用于并行转换成串行化的场合, 其他场合是不适用的. 多线程存在几种关系: 抢占(即先到先得), 流水(即下一线程由上一线程唤醒做事), 并行(即每个线程互不影响), 抢占的关系下, 采用了临界区, 会导致串行化, 效率严重影响. 虽然有TryEnter模式, 但想想工作线程在访问资源的时候, 是的确想去得到资源进行处理, 而不是得不到又去处理其他事, 再回过来头询问能不能得到, 这样有可能导致其中某些线程一直得不到资源进行处理, 而白白浪费CPU资源. 在流水关系下, 采用了临界区, 不但得不到想要的效果, 还会是错误的. 因为临界区是不能够保证进入顺序的. 并行关系下, 需要临界区保护的, 就是并行入口和出口, 即串行获取资源后, 串行输出. 这样会导致一个问题, 就是对资源的处理时快时慢. 并行速度越高, 这个时快时慢的效果越明显, 临界区是不能够保证进入的次序的, 即有可能第一个线程获取第一个资源, 由于输出时一直不能Enter, 而是由其他线程获取, 这样变成第一个进入的资源反而变成处理速度最慢的. 这些采用临界区的结果绝大部分都不是编写时想要的结果, 但采用临界区带来这种结果是必然的. 虽然不是不安全, 但得不到需要的效果, 还不如单线程来做. 而抢占, 流水, 并行这些模式如何能够正确, 比较公平的方式来进行处理, 上述也有解决方法. 抢占, 采用Interlock的模式, Interlock的汇编代码, 其实只有一个XADD. 流水, 采用的是Event, Mutex等信号模式(较成熟的流水模型网上有的), 并行, 可采用主线程分配资源的方式进行, 这样可降低并发读的冲突, 也可采用Interlock的模式完全避开并发读, 输出时, 采用上述的1方式即可.
多线程DEBUG:
1. 有BUG是很麻烦的, 因为很难查出原因, 特别是按业务逻辑而不是功能逻辑来写的多线程的时候, 更加会查死人. 按业务逻辑的方式来写多线程, 可以说是设计上的错误, 不论业务还是功能, 其抽象出来的结果都是输入, 处理, 输出这三种, 而业务, 总是由功能组合而成, 从业务角度来写多线程, 程序不可能健壮. 而按功能写的时候, 检测BUG假如为内存出错, 那肯定是写的人功底不足, 重查所有代码是必须的. 假如是逻辑错误, 只能够采用上述的一条: 某个线程在走下一步之前, 其他线程按代码的逻辑在脑袋里面来跑上几遍, 看看运行到哪里会造成不同步导致错误即较为容易找到缺口. 千万别写LOG, printf等操作进行DEBUG, 这就会导致当前代码的时间逻辑改变, 导致以当前代码的处理上的BUG被掩盖. 多线程有BUG, 只能够以当前的代码来进行逻辑分析是必须的.
2. 达不到预算效果, 多线程写的时候, 达不到预算效果, 可以说就是心痛了. 辛苦弄出来的功能, 却发现连单线程都不如. 这个时候必须分析所有同步机制, 查看是否会出现某些线程是浪费CPU资源, 某些线程是一直等待, 甚至某些线程根本不工作等等, 按上述的几种基本办法, 大体上可以解决较多效率问题的了. 多线程的效率, 只能从串行分解成并行来解决, 而不是追求安全能解决的, 并行不了的地方应该要用单线程, 这样才会高效率, 不须串行的地方就采用并行, 多线程的效率都是来自于此. 同步机制不需要懂得太多, 善用原子操作和信号量已经足够应付很多场合了.
以上为个人经验总结, 保留以备忘记.