程序员人生 网站导航

[CSAPP笔记][第十二章并发编程]

栏目:框架设计时间:2016-06-12 08:56:52

第102章 并发编程

如果逻辑控制流在时间上是堆叠,那末它们就是并发的(concurrent)。这类常见的现象称为并发(concurrency)

  • 硬件异常处理程序,进程和Unix信号处理程序都是大家熟习的例子。

我们主要将并发看作是1种操作系统内核用来运行多个利用程序的机制。

  • 但是,并发不单单局限于内核。它也能够在利用程序中扮演重要的角色。

    • 例如

      • Unix信号处理程序如何允许利用响应异步事件
        • 例如:用户键入ctrl-c
        • 程序访问虚拟存储器的1个未定义的区域
    • 其他情况

      • 访问慢速I/O装备

        • 当1个利用程序正在等待来自慢速I/O装备(例如磁盘)的数据到达时,内核会运行其他进程,使CPU保持繁忙。
      • 与人交互

        • 和计算机交互的人要求计算机有同时履行多个任务的能力。
      • 通过推延工作以下降延迟

        • 有时,利用程序能够通过推延其他操作和并发履行它们,利用并发来下降某些操作的延迟
      • 服务多个网络客户端
        • 1个慢速的客户端可能会致使服务器谢绝为所有客户端提供服务。
      • 在多核机器上进行并行运算

使用利用级并发的利用程序称为并发程序(concurrent program).

  • 操作系统提供3种基本的构造并发程序的方法:

    • 进程

      • 每一个逻辑控制流 都是1个进程

        • 由内核来调度和保护。
      • 由于进程有独立的虚拟地址空间

        • 和其他进程通讯,控制流必须使用某种显式的进程间通讯(interprocess communication,IPC)进制
    • I/O多路复用(暂时不太懂)
      • 利用程序在1个进程的上下文中显示地调度它们自己的逻辑流
      • 逻辑流被模型化为状态机,数据到达文件描写符后,主程序显式地从1个状态转换到另外一个状态。
      • 由于程序是1个单独的进程,所以所有的流都同享同1个地址空间。
    • 线程
      • 线程是运行在1个单1进程上下文中的逻辑流,有内核调度。
        • 进程1样由内核进行调度。
        • 而像I/O多路复用1流1样同享1个虚拟地址空间。

12.1 基于进程的的并发编程

1个构造并发服务器的自然方法就是,在父进程中接收客户端连接要求,然后创建1个新的子进程来为每一个新客户端提供服务。

  • 服务器正在监听1个监听描写符(描写符3)上的连接要求
  • 服务器接收客户端1的连接要求
  • 并返回1个已连接描写符(描写符4)。

  • 子进程取得服务器描写符表的完全拷贝(描写符3,4)
  • 子进程关闭它的拷贝中的监听描写符3
  • 服务器关闭描写符表中的描写符4

  • 以后新的客户端又类似之前两个步骤。

12.1.1 基于进程的并发服务器

  • Signal(SIGCHLD,sigchld_handler)回收僵死进程。

    • 具体细节见8.5.7
  • 28行,33行 父子进程各自关闭他们不需要的拷贝。

  • 由于文件表项的援用计数,直到父进程关闭它的描写符,才算结束1次连接

12.1.2 关于进程的优劣

对在父,子进程间同享状态信息,进程有1个非常清晰的模型

  • 同享文件表,但是不同享用户地址空间
  • 进程具有独立的虚拟地址空间即是 优点,也是 缺点

    • 优点:1个进程不可能不谨慎覆盖另外一个进程的虚拟存储空间。

      • 消除许多使人迷惑的毛病。
    • 缺点:独立的地址空间使得进程间同享信息也很困难。

      • 必须使用显式的IPC(进程间通讯)机制。

      • 常常还比较

        • 进程控制IPC的开消都很大。

12.2 基于I/O多路复用的并发编程(暂时跳过)

假定要编写1个echo服务器

  • 服务器既能响应客户端的要求
  • 也能对用户从标准输入输出的交互命令做出反应(如exit).

因此,服务器必须要响应两个相互独立的I/O事件

  • 网络客户端发起连接
  • 用户在键盘键入命令行。

不管先等待那个事件都不是理想的,解决办法之1是就是使用I/O多路复用技术

  • 基本的思路
    • 使用select函数,要求内核挂起进程,只有1个或多个I/O事件产生后,才将控制返回给利用程序。

12.3 基于线程的并发编程

线程(thread) 就是运行在进程上下文中的逻辑流。

  • 线程由内核调度。
  • 每一个线程都有它自己的线程上下文(thread context).

    • 包括1个唯1的整数线程ID(Thread ID,TID).
    • 栈和栈指针
    • 程序计数器
    • 通用目的寄存器和条件码
  • 所有运行在该进程里的线程同享该进程的全部虚拟地址空间。

    • 同享 包括代码,数据,堆,同享库和打开的文件。

12.3.1 线程履行模型

  • 每一个进程开始生命周期时都是单1线程,这个线程称为主线程(main thread)

    • 某时刻,主线程创建1个对等线程(peer thread)
      • 当主线程履行1个慢速系统调用,例如readsleep
      • 或被系统的间隔计时器中断。
      • 控制就会通过上下文切换传递到对等线程
      • 对等线程履行1段时间,将控制传递回主线程。
  • 在某些方面,线程履行是不同等于进程的。

    • 线程的上下文切换的开消比进程的小很多,快很多
    • 线程不是依照严格的父子层次来组织。
      • 和1个进程相干的线程组成1个线程池(pool)
        • 线程池概念的主要影响是
        • 1个线程可以杀死它的任何对等线程,或等待任意对等线程终止。
        • 每一个对等线程都能读写相同的同享数据。

12.3.2 Posix 线程

Posix线程 (Pthreads)是在C程序中处理线程的1个标准接口。

  • 在大多数Unix系统可用
  • 允许程序创建,杀死和回收线程,与对等线程安全同享数据,还可以通知对等线程系统状态的变化。

这是我们第1个线程化的代码,仔细解析。

  • 线程的代码和本地数据被封装在1个线程例程(thread routine)中。

    • 如第2行代码所示:每一个线程例程都以1个通用指针作为输入,并返回1个通用指针。
    • 如果想传递多个参数给线程例程

      • 你应当将参数放到1个结构中。
      • 并传递1个指向该结构的指针
    • 如果想要线程例程返回多个参数。

      • 也能够返回1个指向结构的指针
  • tid寄存对等线程的线程ID

  • 主线程调用pthread_create函数创建1个新的对等线程(第7行)。

    • 当对pthread_create的调用返回时,主线程和新创建的对等线程同时运行。
  • 通过调用pthread_join,主线程等待对等线程的终止。

  • 对等线程输出Hello,world

  • 主线程终止。

12.3.3 创建线程

线程通过调用pthread_create函数来创建其他线程。

#include<phread.h>
typedef void *(func)(void *);

int phread_create(pthread_t *tid,pthread_attr_t *attr,fun *f,void *arg)

                    //若成功则返回0,出错则为非0

pthread_create函数创建1个新的线程。

  • 带着1个输入变量arg,在新线程的上下文中运行线程例程f.
  • 能用attr参数改变新创建线程的默许属性。

    • 改变这些属性超过我们的学习范围。
    • 我们总是用NULL作为attr的参数。
  • pthread_create返回时,参数tid包括新创建线程的ID

    • 通过调用pthread_self函数来取得它自己的线程ID

12.3.4 终止线程

1个线程是以以下方式之1来终止

  • 当顶层的线程例程返回时,线程会隐式地终止
  • 通过调用pthread_exit函数,线程会显示地终止

    • 如果主线程调用pthread_exit.
      • 等待所有其他对等线程终止,然后终止主线程和其他全部进程。
      • 返回值为thread_return
    • 原型以下

      #include<pthread.h>
      
      void pthread_exit(void *thread_return)
          //成功返回0,出错返回非0
      
  • 某个对等线程调用Unixexit函数,函数终止进程和所有与该进程有关的线程

  • 对等线程通过以当前线程ID为参数调用pthread_cancle函数来终止当前线程。

    • 原型

      #include<pthread.h>
      
      void pthread_cancle(pthread_t tid);
          //成功返回0,出错返回非0
      

12.3.5 回收已终止的资源

线程通过调用pthread_join函数等待其他进程终止

#include<pthread.h>

int pthread_join(pthread_t tid,void **thread_return);

            //返回,成功则为0,出错为非0
  • pthread_join函数会阻塞,知道线程tid终止,将线程返回的(void *)指针赋值给thread_return所指向的位置,然后回收已终止线程占用的存储器资源。

  • pthread_join不像wait函数1样等待任意1个线程的结束。

    • 使得用不那末直观的方式,检测1个进程的终止。
    • Stevens在书中指出这是1个设计毛病。

12.3.6 分离线程

在任何1个时间点上,线程是可结合的(joinable)或 是分离的(detached)

  • 1个可结合的线程能够被其他线程收回其资源或杀死。

    • 在被其他线程回收之前,它的存储器资源是没有被释放的。
  • 1个分离的线程是不能被其他线程收回其资源或杀死。

    • 系统自动释放资源。

pthread_detach函数分离可结合线程tid

#include<pthread.h>

int pthread_detach(pthread_t tid);

            返回:若成功则返回0,若出错则返回非零。

12.3.7 初始化线程

pthread_once函数允许你初始化与线程例程相干的状态。

#include<pthread.h>

pthread_once_t once_control = PTHREAD_INIT;

int phread_once(phread_once_t *once_control,void (*init_routine)(void));
  • once_control变量是1个全局或静态变量,总是被初始化为PTHREAD_ONCE_INIT.
  • 当你第1次用参数once_control调用pthread_once时,它调用init_routine

    • 这是1个没有输入参数,也不返回甚么的函数。
  • 第2次,第3次以参数once_control调用pthread_once时,啥事也不产生。

    • 意思时仅仅第1次调用时有效果。
  • 当你需要动态初始化多个线程同享的全局变量时,pthread_once函数是很有用的。

12.3.8 1个基于线程的并发服务器


  • 注意使用malloc动态给1个connfdp,否则可能两个线程援用同1个connfdp的地址。

    • 这叫做竞争
  • 为在线程例程中避免存储器泄漏,使用分离线程

  • 还要注意释放在主线程malloc的变量。

12.4 多线程程序中的同享变量

为了解1个C程序中的1个变量是不是同享,有1些基本的问题要解答

  • 线程的基础存储器模型是甚么?
  • 根据这个模型,变量实例是如何映照到存储器的?
  • 有多少线程援用这些实例?

为了使同享讨论具体化,使用下图的程序作为示例。

示例程序由1个创建两个对等线程的主线程组成。主线程传递1个唯1的ID给每一个对等线程,每一个对等线程利用这个ID输出1个个性化的信息,和调用该线程例程的总次数。

12.4.1 线程存储器模型

12.4.2 将变量映照到存储器

线程化的C程序中的变量根据它们的存储类型被映照到虚拟存储器:

  • 全局变量

    • 全局变量是定义在函数以外的变量。
      • 在运行时,虚拟存储器中的读/写区域包括每一个全局变量的1个实例。
      • 任何线程都可以援用。
      • 例如,第5行声明的ptr
  • 本地自动变量

    • 本地自动变量就是定义在函数内部但是没有static属性的变量。
      • 在运行时,每一个线程的包括它自己的所有本地自动变量的实例。
  • 本地静态变量

    • 本地静态变量是定义在函数内部有static属性的变量。
      • 和全局变量1样,存储在虚拟存储器的读/写区域
      • 例如:第25行的cnt.

12.4.3 同享变量

我们说1个变量v同享的,当期仅当它的1个实例被1个以上的线程援用。

例如:

  • cnt 是同享的
  • myid 不是同享的
  • 认识到msgs这类本地自动变量也能被同享是很重要的。

12.5 用信号量同步线程

同享变量10分方便,但是他们也引入了同步毛病(synchronization error)的可能性。

斟酌下图的程序。

到底哪里出错了呢?这个毛病10分隐晦,必须通过研究计数器循环时的汇编代码才能看出。

badcnt.c中的两个对等线程在1个单处理器上并发履行,机器指令以某种顺序1个接1个地完成。同1个程序每次运行的顺序都可能不同,这些顺序中有1些将会产生正确结果,但是其他的不会。这就是同步毛病

关键点: 1般而言,你没有办法预测操作系统是不是将为你的线程选择1个正确的顺序

  • 下图,就是cnt正确的顺序和毛病的顺序(正确结果cnt=2,毛病结果cnt=1)

我们可以借助于1种叫做进度图(progress graph)的方法来阐明这些正确和不正确的指令顺序的概念。将在接下来介绍。

12.5.1 进度图

进度图(process graph)n个并发进程的履行模型化为1条n维笛卡尔空间的轨迹线

  • 每条轴k对应于k的进度。

  • 每一个点(I1,I2,I3,I4...,In)代表线程k(k=1,...,n)已完成到了Ik这条指令的状态。

  • 图的原点对应于没有任何线程完成这1条指令的初始状态


进度图将指令履行模型化为从1个状态到另外一个状态的转换(transition)

  • 转换指从1点到相邻1点的有向边。
    • 合法的转换是向各个轴的正半轴走。

临界区

对线程i,操作同享变量cnt内容的指令(Li,Ui,Si)构成了1个(关于同享变量cnt的)临界区(critical section)。(必须确保指令要这样履行)

  • 这个临界区不应当和其他线程的临界区交替履行。(这1段的指令不能交叉)。

  • 我们要确保每一个线程在履行它的临界区中的指令时,具有对同享变量的互斥的访问(mutually exclusive access)

    • 通常这类现象叫做互斥(mutual exclusion)

不安全区

在进程图中,两个临界区的交集情势称为不安全区(unsafe region)

  • 不安全区边沿的不算不安全区的1部份。

安全轨迹线,不安全轨迹线

  • 绕过不安全区的轨迹线叫做安全轨迹线
    • 能正确更新计数器
  • 接触到不安全的轨迹线叫做不安全轨迹线

我们必须以某种方式同步线程,使它们总是有1条安全轨迹线

  • 1个经典的方法,就是基于信号量的思想。

12.5.2 信号量

Edsger Dijksta,并发编程领域的先锋任务,提出了1种经典的解决同步不同履行线程问题的方法

这类方法是基于1种叫做信号量(semaphore)的特殊类型变量。

  • 信号量s是具有非负整数值的全局变量。

  • 只能由两种特殊的操作来处理,这两种操作称为PV

    • P(s),Proberen,测试

      • 如果s是非零的,那末P操作s减1,并且立即返回。
      • 如果s为零,那末就挂起这个线程,直到s变成非零。
        • 而1个V操作会重启这个线程。
        • 在重启以后,P操作s减1,并将控制返回给调用者。
    • V(s),Verhogen,增加

      • V操作s加1.
      • 如果有任何线程阻塞在P操作等待s变成非零。
        • 那末V操作随机会重启这些线程中的1个。
        • 然后将s减去1,完成它的P操作
    • 重点P操作V操作都是不可分割的,也就是本身确保了是1个带有安全轨迹的操作。(所以又叫原语)

      • 对照,上文中的cnt++的操作。
      • 例如,加1这个操作中,加载,加1,存储信号量进程是不可分割的。

PV的定义确保了1个正在运行的程序绝不可能进入这样1种状态,也就是不可能有负值。
这个属性叫做信号量不变性(semaphore invariant),为控制并发程序的轨迹线提供了强有力的工具。

12.5.3 使用信号量来实现互斥

信号量提供了1种很方便的方法来确保对同享变量的互斥访问。

基本的思想是

  • 将每一个同享变量(或1组相干的同享变量) 与1个信号量s(初始为)`联系起来。
  • 然后用P(s)V(s)操作相应的临界区包围起来。

以这类方式保护同享变量的信号量叫做2元信号量(binary semaphore)

  • 由于它的值总是0或1。

以提供互斥为目的的2元信号量常常也称为互斥锁(mutex)

  • 在1个互斥锁上履行P操作叫做互斥锁加锁
  • 在1个互斥锁上履行V操作叫做互斥锁解锁
  • 对1个互斥锁加了锁还没有解锁的线程称为占用这个互斥锁

1个被用作1组可用资源的计数器的信号量称为计数信号量

关键思想:

  • P操作V操作的结合创建了1组状态,叫做制止区(forbidden regin),其中s<0
    • 由于信号量的不变形,不可能有轨迹线进入这个区域
    • 而且制止区包括了不安全区的任何部份。
      • 使得,每条可行的轨迹线都是安全的。

代码上的实现

正确切现上文中的cnt的线程同步。

  • 第1步:声明1个信号量 mutex

    volatile int cnt = 0 ; 
    sem_t mutex;
    
  • 第2步:主线程中初始化

    Sem_init(&mutex,0,1);
    
  • 第3步,在线程例程中对同享变量cnt的更新包围PV操作,从而保护了它们。

    for( i = 0 ;i < niters ;i++) {
        P(&mutex);
        cnt++;
        V(&mutex);
    }
    

12.5.4 利用信号量来调度同享资源

除提供互斥外,信号量的另外一个重要作用是调度对同享资源的访问

  • 在这类场景中,1个线程用信号量操作来通知另外一个线程,程序状态中的某个条件为真了。
  • 两个经典而有用的例子。

    • 生产者 - 消费者 问题
    • 读者 - 写者 问题

1.生产者和消费者

图给出了生产者消费者问题

  • 生产者线程反复地生成新的项目,并把它们插入到缓冲区中。
  • 消费者线程不断地从缓冲区取出这些项目,然后消费使用它们。
  • 也可能有多个的变种。

由于插入和取出项目都触及更新同享变量

  • 所以我们必须保证对缓冲区的访问是互斥的
  • 还需要调度对缓冲区的访问。
    • 如果缓冲区是满的,那末生产者必须等待直到有1个槽位变成可用。
    • 如果缓冲区是空的,那末消费者必须等待知道有1个项目变成可用。

我们将开发1个简单的包,叫做SBUF,用来构造生产者-消费者程序。

SBUF操作类型为sbuf_t的有限缓冲区。

  • 项目寄存在1个动态分配的n项整数数组(buf)中。
  • frontrear索引值记录该队列的第1项和最后1项。
  • 3个信号量同步对缓冲区的访问。
    • mutex信号量提供互斥的缓冲区访问
    • slotsitems信号量分别记录空槽位和可用项目的数量。

以下给出SBUF函数的实现:

  • sbuf_init函数进行初始。
    • 为缓冲辨别配堆存储器
    • 设置frontrear表示1个空的缓冲区。
    • 并为3个信号量赋初值。

  • sbuf_deinit函数是当利用程序使用完缓冲区时,释放缓冲区存储。
  • sbuf_insert

    • 等待1个可用的槽位
    • 对互斥锁加锁,添加项目,对互斥锁解锁
    • 然后宣布有1个新项目可用。
  • sbuf_remove

    • 等待1个可用的项目
    • 对互斥锁加锁,取出项目,对互斥锁解锁
    • 然后宣布有1个新槽位可用。

2.读者-写者问题

读者-写着问题是互斥问题的1个概括。

  • 1组并发的线程要访问同1个数据对象。

    • 修改对象的线程叫做写者
    • 只读对象的线程叫做读者
  • 写者必须具有对对象的独占访问。

  • 读者可以和无穷多个其他读者同享对象。
  • 1般来讲有没有数个并发的读者和写者。

读者-写者问题有几个变种,都是基于读者和写者的优先级

  • 第1类读者-写者问题

    • 读者优先,要求不要让读者等待,除非已把1个使用权限赋予了1个写者
    • 换句话说,读者不会由于有1个写者在等待而等待。
  • 第2类读者-写者问题(?)

    • 写者优先,要求1但1个写者准备好可以写,它就会尽量地完成它的写操作。
    • 同第1类不同,在1个写者到达后的读者必须等待,即便这个写者也是在等待。

给出第1类读者-写者问题答案。

  • 这个的优先级很弱,由于1个离开临界区的写者可能重启1个在等待的写者(随机重启)
    • 很有可能1群写者使得1个读者饥饿

  • 信号量w控制对访问同享对象的临街区的访问。

    • 读者

      • w只对第1个读者上锁
      • w对最后1个走的读者解锁
    • 写者

      • 写者只要进入临界区就对w上锁
      • 写者只要离开临界区就对w解锁
  • 信号量mutex保护对同享变量readcnt的访问。
    • readcnt统计当前临界区的读者数量。

所有读者-写者答案都有可能致使饥饿

  • 饥饿就是1个线程无穷期地阻塞,没法进展。

12.5.5 基于预线程化的并发服务器

为每一个新的客户端创建新的线程,有很多的代价。

1个基于预线程化服务器利用生产者-消费者模型构造1个更高效力的方式。

  • 生产者: 主线程
  • 消费者: 对等线程

12.6 使用线程提高并行性(暂略)

主要用于多核CPU的算法。

比如:利用并行来完成n路递归

12.7 其他并提问题

互斥生产者-消费者同步的技术,只是并提问题的冰山1角。

同步问题从根本来讲是很难的问题。

这章我们以线程为例讨论。

  • 但是要知道同步问题在任何并发流操作同享资源时都会出现。
    • 比如之前学信号时,回收进程时的竞争

12.7.1 线程安全

1个函数被称为线程安全的(thread-safe),当且仅当被多个并发线程反复地调用时,它会1直产生正确的结果。否则就是线程不安全的(thread-unsafe)

我们能够定义出4个(不相交)线程不安全函数类:

  • 第 1 类 : 不保护同享变量的函数。
    • 解决方案,利用P,V这样的同步操作来保护同享的变量
  • 第 2 类 : 保持逾越多个调用状态的函数

    • 1个伪随机数生成器是这类线程不安全的例子。

    • 由于产生的结果依赖于上1个next的值。

      • 在单线程中,用同1个seed不管运行多少次,都是一样的结果。
      • 多线程中,这类情况就不会出现了,所以是线程不安全
    • 解决方案: 重写

      • 使得它不能依赖static,而是依托调用者在参数中传递状态信息。
      • 缺点: 需要在曾成千上百个不同的调用位置,修改。10分麻烦。
  • 第 3 类 :返回指向静态变量的指针的函数( 有点类似第1类 )

    • 危害:我们在并发线程中调用这些函数,可能产生灾害。
      • 由于1个正在被1个线程使用的变量,可能偷偷被另外一个线程悄悄覆盖。
    • 解放方案

      • 重写函数:让调用者传递寄存的结果的指针
      • 加锁-拷贝技术:

        • 在1个调用位置,互斥锁加锁。
        • 调用线程不安全函数,将函数返回的结构拷贝到1个私有的存储器。
        • 然后互斥锁,解锁。
      • 用上面的原理写1个线程不安全函数的包装函数来实现线程安全。

        • 以ctime为例子

  • 第 4 类 : 调用线程不安全函数的函数

    • 如果函数f调用线程不安全函数g。那末f可能不安全。
      • 如果g是第2类,那末f1定不安全,也没有办法去修正,只能改变g.
      • 如果g是第1,3类,可以用加锁-拷贝技术来解决。

12.7.2 可重入性

有1类重要的线程安全函数,叫做可重入函数(reentrant function)

  • 其特点在于它们有这样1种属性。

    • 当它们被多个线程调用时,不会援用任何同享数据
  • 被分为两类

    • 显示可重入
      • 参数都是值传递
      • 变量都是本地自动栈变量
    • 隐式可重入

      • 参数可以有指针

        • 但是不允许调用者传入指向同享数据的指针。
      • 是不是可重入,同时取决于调用者,和被调用者

  • 可重入函数比较高效是由于不需要同步操作。

  • 认识到可重入性有时即是调用者也是被调用者的属性。

    • 其实不是被调用者的单独属性。

12.7.3 在线程化的程序中使用已存在的库函数

大多数Unix函数,包括大部份定义在标准C库的函数(malloc,free,realloc,printfscanf)都是线程安全的。

部份线程不安全

  • asctime,ctime,localtime函数是在不同时间和数据格式相互来回转换时常常使用的函数。

  • gethostbyname,gethostbyaddr,inet_ntoa函数是常常用的网络编程函数。

  • strtok函数是1个过时了的同来分析字符串的函数。


Unix系统提供大多数线程不安全函数的可重入版本。

  • 可重入的版本总是以 _r后缀结尾。
  • 例,gethostbyname_r

12.7.4 竞争

当1个程序的正确性依赖于1个线程要在另外一个线程到达y点之前到达它的控制流中的x点,就会产生竞争

  • 通常,竞争产生的理由是由于程序员假定某种特殊的轨迹线穿过履行状态空间。

例子:

程序10分简单。

主线程创建了4个对等线程,并传递1个指向循环变量i的指针作为线程的ID。并输出。

  • 1般而言,循环变量i1定是4个不同的。所以会想固然觉得会输出4个不同的ID

  • 但是从结果来看,明显是毛病的,有两个3,为何?
    • 由于我们想固然的觉得对等线程myid赋值结束后,i才会自增。
    • 竞争来源于 主线程中i++,和对等线程myid=*((int *)vargp)竞争

解决方案:用1个临时地址保存i

12.7.5 死锁

信号量引入了1种潜伏的使人讨厌的运行时毛病,叫做死锁 (deadlock)。

  • 指的是1组线程被阻塞,等待1个永久不为真的条件。

进度图对理解死锁是1个无价的工具。

  • 死锁的区域d是1个只能进不能出的区域。

    • 位置是合法的,其实不是制止区,能进去。
    • 但是会发现不管向上,还是右,都只剩下制止区了。
  • 如果制止区不堆叠,1定不会产生死锁

    • 否则,可能产生死锁
  • 死锁是1个相当困难的问题,由于它总是不可预测的。

    • 荣幸的话,会绕开死锁区域。
    • 毛病还不会重复,轨迹不同。

特殊解

使用2元信号量来实现互斥,可以利用1下有效的规则。

互斥锁加锁顺序规则:如果对程序中每对互斥锁(s,t),每一个占用st的线程都依照相同的顺序对它们加锁,那末这个程序就是无死锁的。


GGGGGGGGGGG,暂时告1段落了!!!!!!!!!!!!!!ddd!!

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐