本文为《Computer Systems: A Programmer's Perspective》第12.7节—并发编程问题的读书笔记。下面开始正文。

1. 线程安全
        一个线程安全(thread-safety)的函数应满足条件:当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。相对地,若一个不是线程安全的函数被称为线程不安全(thread-unsafety)函数。
        我们能定义出四个(不相交)的线程不安全函数类:
        1)不保护共享变量的函数
        很容易理解为什么这类函数线程不安全,将这类函数改造成线程安全的也相对容易:利用类似于信号量的P/V操作或操作系统支持的其它同步操作来保护共享变量。这种改造方法的优点是在调用这类函数的上层程序中不需要做如何修改,缺点是同步操作会减慢程序的执行时间。
        2)保持跨越多个调用的状态的函数
        例如下面的这段伪随机数生成器代码:

    unsigned int next = 1;/* rand - return pseudo-random integer on 0 - 32767 */int rand(void){next = next * 1103515245 + 12345;return (unsigned int)(next / 65536) % 32768;}/* srand - set seed for rand() */void srand(unsigned int seed){next = seed;}

上面的代码中,srand()是线程不安全的,因为当前调用结果依赖于前次调用的中间结果。当调用srand()为rand()设置好随机种子后,单线程反复调用rand(),能够预期得到一个可重复的随机数字序列。然而,如果多线程调用rand(),这种假设就不再成立了。
        将类似于rand()这样的函数改造为线程安全函数的唯一方法是重写它,使得其不再使用任何static或global数据,而是依靠调用者在参数中传递状态信息。这样做的缺点是:程序员需要被迫修改调用程序中的代码,在大型程序中,可能会修改成百上千个调用位置,很容易引起bug。
        3)返回指向静态变量的指针的函数
        某些函数,如ctime和gethostbyname,将计算结果放到一个static变量中,然后返回指向这个变量的指针。如果从并发线程中调用这类函数,将可能引发灾难,因为正在被一个线程使用的结果会被另一个线程悄悄覆盖。
        有两种方法来处理这类线程不安全函数。一种选择是重写函数,由调用者来传递存放结果的变量地址,这消除了所有的共享数据,但需要程序员修改函数的源代码和上层的调用代码(很多时候,修改源代码是不现实的,例如标准库函数或第三方提供的库)。第二种选择是使用加锁-拷贝(lock-and-copy)技术基本思想是将线程不安全函数用互斥锁保护起来,即在每个调用位置,程序的执行路径为:加互斥锁->调用线程不安全函数->将函数返回结果拷贝到调用者提供的私有存储变量中->解互斥锁。为尽可能减少对调用者的修改,我们可以定义一个线程安全的包装函数,由它来执行加锁拷贝,上层调用者通过调用这个包装函数来取代所有对线程不安全函数的调用,从而实现线程安全。
        下面给出利用lock-and-copy技术实现的一个线程安全版本的ctime:

    char * ctime_ts(const time_t * timep, char * privatep){char * sharedp;P(&mutex);sharedp = ctime(timep);strcpy(privatep, sharedp);  // copy string from shared to private  V(&mutex);return privatep; }

博主按:上面提到的定义一个与原函数具有相同接口参数和返回值的包装函数来执行复杂操作的思路,最初是由 Richard Stevens在其经典力作《Unix Network Programming》中引入的,读过这本经典书籍的同学应该不会感到陌生。只可惜大师意外早逝,令人唏嘘啊。
        4)调用线程不安全函数的函数
        若函数f调用线程不安全函数g,那么f就是线程不安全的吗?
        答案是:不一定。如果g是上面提到的第2类函数,即依赖与跨越多次调用的状态,则f也是线程不安全的,而且除了重写g外,没有什么改造办法。然而,若g是第1类或第3类函数,那么只有我们用一个互斥锁来保护调用位置和任何得到的共享数据,f仍然可能是线程安全的。上面给出的利用加速-拷贝技术实现的ctime_ts代码就是一个例子,在该示例中,我们使用lock-and-copy编写了一个线程安全函数,它调用了一个线程不安全的函数。

2. 可重入性
        有一类重要的线程安全函数,叫做可重入函数(reentrant function),它具有如下属性:当它们被多个线程调用时,不会引用任何共享数据。
        下图给出了可重入函数、线程安全函数和线程不安全函数之间的集合关系:
        
        可重入函数通常要比不可重入的线程安全函数高效,因为它们不需要同步操作。进一步讲,将上面提到的第2类函数改造为线程安全函数的唯一方法就是将其重写为可重入的。下面的代码展示了这一点,其关键思想是用调用者传递的指针取代静态的next变量。

    /* rand_r - a reentrant pseudo-random integer on 0 - 32767 */int rand_r(unsigned int * nextp){*nextp = *nextp * 1003515245 + 12345;return (unsigned int)(*nextp / 65536) % 32168;}

检查某个函数的代码并先验地断定它是可重入的,这可能吗?
        答案是:不一定。
        若所有的函数参数都是值传递,且所有的数据引用都是本地自动栈变量,则函数就是显式可重入的(explicitly reentrant),也即,无论它是被如何调用的,我们都可以断定它是可重入的。
        若将显式可重入函数的某些参数改为指针传递,我们就得到了一个隐式可重入(implicitly reentrant)函数,也即,如果调用线程小心地传递指向非共享数据的指针,那么它是可重入的。

3. 在线程化的程序中使用已存在的库函数
        所有定义在标准C库中的函数都是线程安全的。大多数Unix函数也都是线程安全的,只有一小部分是例外,这些函数如下图所列。
       
        Unix系统提供大多数线程不安全函数的可重入版本。可重入版本的名字总是以"_r"后缀结尾。例如,gethostbyname的可重入版本是gethostbyname_r。我们应尽可能使用这些函数。

4. 竞争
        当一个程序的正确性依赖与一个线程要在另一个线程到达y点之前到达它的控制流中的x点时,就会发生竞争(race)。通常发生竞争是因为程序员假定线程将按照某种特殊的轨迹线穿过执行状态空间,而忘记了另一条准则规定:线程化的程序必须对任何可行的轨迹都正确工作。
        要理解多线程中的竞争问题,可参考下面的代码:

    #define N 4void * thread(void * vargp);int main(){pthread_t tid[N];int i;for(i = 0; i < N; ++i) {pthread_create(&tid[i], NULL, thread, &i);}       for(i = 0; i < N; ++i) {pthread_join(tid[N], NULL);}exit(0);}/* thread routine */void * thread(void * vargp){int myid = *((int *)vargp);printf("Hello from thread %d \n", myid);return NULL;}

上面的代码中,thread routine中打印的tid可能会产生非预期的结果。 问题是由每个对等线程和主线程之间的竞争引起的:主线程通过for循环创建对等线程时,它传递了一个指向本地栈变量i的指针。在此时,竞争出现在下次循环体调用pthread_create和thread()函数第1行参数的间接引用和赋值之间。如果对等线程在主线程执行下个循环的pthread_create前就执行了thread()的第1行,那么myid就得到了正确的ID;否则,它的值就是其它线程的ID。 令人惊慌的是,我们是否得到正确的答案依赖于内核是如何调动线程执行的。一种可能的情况是:在某些版本的操作系统中,错误的执行结果可能会暴露给程序员,而在另一些系统中,它可能总是能"正确"工作,让程序员"幸福地"错觉不到程序的严重错误。
        博主按:对新手来说,这个bug非常隐蔽,请务必引起重视。
        下面是消除竞争后的示例代码:

    #define N 4void * thread(void * vargp);int main(){pthread_t tid[N];int i, *ptr;for(i = 0; i < N; ++i) {ptr = malloc(sizeof(int));*ptr = i;pthread_create(&tid[i], NULL, thread, ptr);}       for(i = 0; i < N; ++i) {pthread_join(tid[N], NULL);}exit(0);}/* thread routine */void * thread(void * vargp){int myid = *((int *)vargp);free(vargp);printf("Hello from thread %d \n", myid);return NULL;}

5. 死锁
        在并发编程中,死锁(deadlock)是一个让程序员头疼的问题。关于死锁,操作系统方面的权威专家Andrew S. Tanenbaum在《Modern Operating Systems》一书中用整整一章来介绍,足见死锁在系统编程方面的重要性。大部分死锁都与资源相关,本文引用该书中对死锁的规范定义:
        A set of processes is deadlocked if each process in the set is waiting for an event that only another process in the set can cause.        
        中文翻译:如果一个进程集合中的每个进程都在等待只能由该进程集合中的其它进程才能引发的事件,那么,该进程集合就是死锁的。
        程序死锁有很多原因,要避免死锁一般而言是很困难的。然而,当使用二元信号量来实现互斥时,避免死锁的规则变得相对比较简单:
        互斥锁加锁顺序规则:若对于程序中每对互斥锁(s, t),每个同时占用s和t的线程都按照相同的顺序对它们加锁那么这个程序就是无死锁的。
        关于死锁的更详细的讨论(如资源死锁的条件,死锁建模,死锁检测等细节)超出了本笔记的范畴。感兴趣的同学,建议阅读《Modern Operating Systems》或其它操作系统教材的相关章节。^_^

【参考资料】
1. <Computer Systems: A Programmer's Perspective>. chapter 12 - Concurrent Programming with Threads
2. mcu cs online material: www.cs.cmu.edu/~kgao/course/15213/notes/week16.ppt‎
3. <Modern Operating Systems, 2nd Edition>. chapter 3 - DeadLock

================ EOF ===============

转载于:https://www.cnblogs.com/xinyuyuanm/p/3206234.html

【读书笔记】并发编程需要注意的几个典型问题相关推荐

  1. Java并发编程之美读书笔记-并发编程基础2

    2019独角兽企业重金招聘Python工程师标准>>> 1.线程的通知与等待 Java中的Object类是所有类的父亲,鉴于继承机制,Java把所有类都需要的方法放到了Object类 ...

  2. 剑指offer(第二版)读书笔记以及编程题目python版答案(二)

    剑指offer(第二版)读书笔记以及编程题目python版答案(二) 题目五:青蛙跳台阶 github地址: https://github.com/ciecus/leetcode_answers/tr ...

  3. Core Java 8 读书笔记-Networking编程

    Core Java 8 读书笔记-Networking编程 作者:老九-技术大黍 原文:Core Java 8th Edition 社交:知乎 公众号:老九学堂(新人有惊喜) 特别声明:原创不易,未经 ...

  4. Java并发编程实战笔记—— 并发编程1

    1.如何创建并运行java线程 创建一个线程可以继承java的Thread类,或者实现Runnabe接口. public class thread {static class MyThread1 ex ...

  5. C++笔记-并发编程 异步任务(async)

    转自 https://www.cnblogs.com/diysoul/p/5937075.html 参考:https://zh.cppreference.com/w/cpp/thread/lock_g ...

  6. OnJava8读书笔记(java编程思想)--集合Collections

    本篇博文参考on Java8中文版编写 本编博文参考java编程思想第四版编写 文章目录 概述 一.泛型和类型安全的集合 二.基本概念 三.添加元素组(Adding Groups of Element ...

  7. 小白的第一本python书_读书笔记:编程小白的第一本python入门书

    书名:编程小白的第一本python入门书 作者:侯爵 出版社/出处:图灵社区 年份:2016年 封面: 感想: 本书短小精悍,精华部分在于给编程小白打了鸡血的同时输出了一种"高效学习法的思想 ...

  8. 读书笔记————Python编程快速上手

    学习笔记 文章目录 基础 整型.浮点型和字符串数据类型 字符串连接和复制 变量命名规则 `print()`函数 `input()`函数 `len()`函数 `str() float() int()`函 ...

  9. 读书笔记: Unix编程实践教程

    目录 第1章 Unix系统编程概述 第2章 用户.文件操作与联机帮助:编写who命令 第3章 目录与文件属性:编写ls 第4章 文件系统:编写pwd 第5章 连接控制:学习stty 第6章 为用户编程 ...

最新文章

  1. cakephp对数据库的增删改查
  2. ZOJ 2587 Unique Attack
  3. C++回声服务器_4-UDP connect版本客户端
  4. 光伏领跑者火热前行 可靠性护航“长跑”
  5. linux访问nfs端口号,linux nfs配置及访问控制
  6. oracle实例的概念组成,oracle体系结构的两个基本概念:数据库和实例
  7. GitHub标星2600,从零开始的深度学习实用教程 | PyTorch官方推荐
  8. qt实现仓库物料管理(小工具)
  9. 斑马Zebra 110Xi4 打印机驱动
  10. linux终端设置es副本数,elasticsearch之修改shards数
  11. 帝国时代(1)--献给曾经的游戏
  12. We're sorry but vue_blog doesn't work properly without JavaScript enabled. Please enable it to.....
  13. 问答间了解ISO27701隐私信息管理体系
  14. 解决SQL适配器连接到字符集为US7ASCII的Oracle数据库的中文乱码问题
  15. Java基础练习——吃货联盟
  16. 华盛顿大学计算机专业硕士申请,华盛顿大学计算机科学与系统理学硕士研究生申请要求及申请材料要求清单...
  17. 网络克隆(Netghost8.0)图文教程
  18. scada系统web服务器,基于IEC61970的Web-SCADA系统服务器后台的设计与实现
  19. Helvetica字体的50年
  20. c8051f c语言编程,C8051F SPI接口读写c程序

热门文章

  1. 环信SDK 踩坑记webIM篇(一)
  2. Web前端笔记-vue cli中使用echarts加载geo地图
  3. python开发项目架构图_我的第一个python web开发框架(8)——项目结构与RESTful接口风格说明...
  4. 二进制指数类型退避算法
  5. 计组之存储系统:6、Cache-主存映射方式(全相连映射、直接映射、组相连映射)
  6. (王道408考研操作系统)第四章文件管理-第二节3:减少延迟时间的方法
  7. (计算机组成原理)第五章中央处理器-第二节:指令执行过程(取指周期、间址周期、执行周期和中断周期)
  8. 第一章第一节:C++简介与学习方法
  9. Linux网络编程--sendfile零拷贝高效率发送文件
  10. Qt 自定义界面(实现无边框、可移动)