概述

学习目标:

  • 理解线程概念和并发特征,分辨线程与进程的区别与联系
  • 掌握多线程应用编程技术,掌握线程间数据传递基本方法
  • 掌握共享变量识别方法,理解多线程访问共享变量可能带来的问题
  • 理解临界资源、临界区、线程互斥、线程同步基本概念
  • 理解保证临界区互斥执行的基本思想
  • 掌握用信号量和P/V操作来解决互斥、同步问题的编程方法
  • 掌握生产者/消费者、读者/写者问题两个经典同步问题的编程方法,并用以分析和解决实际应用
  • 了解AND型信号量、信号量集、条件变量和管程等同步机制
  • 理解线程安全、可重入型、线程竞争基本概念
  • 掌握利用多线程并发编程基本方法
  • 掌握并发程序性能分析基本方法

6.1 线程概念

6.1.1 什么是线程

传统应用程序特点与不足

启动一个传统C语言程序,产生一个进程,进程从main()函数开始,沿着程序流程往下执行,不严格地说,可认为每次仅执行一条语句(或一条指令),前一条语句(或指令)完成,后一条语句(或指令)才能开始。不足之处:

  • 一个进程只有一个代码执行序列(或控制流),只能接受一个CPU(或CPU核)为其服务,不便发挥多CPU或多核系统强大的计算能力
  • 进程每段时间只能执行一项活动,若有多项需要并发执行的活动,传统程序结构很难进行有效协调。比如程序在等待用户输入的同时,还要从消息队列接收消息,而此时用户输入尚未完成,消息还未到来。很难同时协调和管理多项并发活动,尤其是需要等待某种事件发生的活动。

什么是线程

采用多线程机制可以很好地解决前述问题。

  • 什么是线程:线程定义为进程内一个执行单元或一个可调度实体,每个执行单元可执行进程的一段程序代码(如函数)。在一个进程内可创建多个线程,这些线程都是并发逻辑流,每个线程可执行一项独立的活动或功能。 如Web服务器一个线程在给某浏览器产生网页时,另一线程就可侦听其他浏览器请求
  • 线程共享进程资源:属于同一进程的多个线程位于同一个地址空间内,共享进程拥有的资源,包括代码、数据(全局变量)、堆和打开文件等,线程需栈、程序计数器等少量资源,线程又称轻量级进程(LWP)

线程结构和状态

  • 线程结构:每个线程拥有一个线程控制块TCB(Thread Control Block)和一个线程栈,TCB用于存放线程相关属性,栈用于给函数调用、局部变量分配内存
  • TCB内容:与线程相关的属性放在线程控制块(TCB)内,TCB包括线程ID(Thread TID)、线程状态、栈指针、通用寄存器、程序计数器和标志寄存器
  • 线程比进程开销小:因线程工作所需的大部分资源和属性在进程中,线程需维护的资源、属性很少,线程创建、销毁所需开销很低(甚至低于进程开销的10%)
  • 线程基本状态及其转换

6.1.2 线程执行模型

  • 线程创建:进程启动时,先执行main函数,这个执行体为主线程(main thread);主线程在某个时刻可创建新的线程,称为对等线程(peer thread);此后各线程也可以根据需要创建更多对等线程
  • 线程间关系:线程之间无父子关系,都是对等的,一个进程相关的线程组成一个对等线程池(pool),独立于其他进程创建的线程
  • 好处:一个线程可以终止任何其他对等线程,或者等待任意其他对等线程终止
  • 线程间并发关系:各线程可因sleep、read等慢速系统调用,走走停停,交错往前推进

6.1.3 多线程应用

  • 让程序同时做多件事情。示例:在编辑文档的同时对文档中的单词个数进行实时统计。
  • 将一个耗时的进程任务划分成很多小任务,同时在多个CPU或核上执行。如大矩阵运算、海量数据检索
  • 管理多项并发活动。
  1. 示例1:对一个混杂输入、计算和输出的应用程序,输入、计算、输出各安排一个线程来做
  2. 示例2:通过多线程让进程同时等待多个事件,如网络连接、消息
  3. 示例3:网络服务器同时处理多个客户连接

6.1.4 第一个线程

  • pthread创建一个对等线程,对等线程的任务是peertask
  • 之后主线程与对等线程一起往下执行,由于每个线程执行输出一行文本后都usleep,让出CPU,控制两个线程来回切换
  • 对等线程peertask的return语句终止之后,主线程调用pthread_join函数等待对等线程终止,最后调用exit(0)终止整个程序

6.2 多线程并发特性与编程方法

6.2.1 Pthreads线程API

Pthreads提供了phtread_create、pthread_exit、pthread_join、pthread_detach、pthread_cancel等API函数,用于创建、退出、回收、分离和取消线程。

1. 创建线程

  • 创建线程

typedef void *(*Func) (void *arg);

int pthread_create(pthread_t *tid, pthread_attr_t *attr, Func fun,void *arg);

  • 获得线程TID: pthread_t pthread_self(void);

2. 终止线程

  • 根据tid撤销某个线程: void pthread_cancel(pthread_t tid);
  • 线程自己终止: void pthread_exit(void *thread_return);   //thread_return存放线程终止状态的指针

3. 等待线程终止,回收其资源

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

4. 分离线程

  • 线程缺省情况下是可结合的(joinable)或者是分离的(detached)。
  • 一个可结合的线程能够被其他线程杀死并收回其资源
  • 一个分离的线程是不能被其他线程回收或杀死的。它的存储器资源在其终止时由系统自动释放
  • 应用示例:一个高性能Web服务器可能在每次收到Web浏览器的连接请求时都创建一个新的对等线程。因为每个连接都是由一个单独的线程独立处理的,所以对于服务器而言,就没有必要显式地等待每个对等线程终止,可将线程分离,让其终止时自动释放资源
  • 分离线程:int  pthread_detach(pthread_t tid);

5. 初始化线程

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

#include<pthread.h>pthread_once_t once_control = PTHREAD_ONCE_INIT;int pthread_once(pthread_once_t  *once_control, void (*init_routine)(*void));
//成功返回0,失败返回非0值
  • once_control变量是一个全局或者静态变量,总是被初始化为PTHREAD_ONCE_INIT。
  • 第一个线程用参数once_control调用pthread_once时,它调用init_routine执行某些初始化工作
  • 接下来其他线程以once_control为参数的pthread_once调用,将不做任何事情
  • 可用于动态初始化多个线程共享的全局变量时

6.2.2 多线程并发特性

示例

进程结构分析

进程并发逻辑流分析

  • 进程pthread2有三个并发逻辑流:main线程、t1线程、t2线程
  • 线程t1的两个操作A1、A2与线程t2的两个操作B1、B2、B3并发执行,不同的执行顺序有C(5,2) 种之多。
  • 操作t1:A1和t2:B2都需读变量x的值,访问相同的内存单元,可不严格地认为这两个操作会错开执行
  • 虽然主线程的Pthread_join函数调用与对等线程并发执行,但会等到相应的线程结束后返回,因此主线程的printf语句并不与线程t1、t2并发 程序中不存在对共享变量并发读写情况(并发读没关系),程序执行结果是唯一的。

6.2.3 线程间数据传递

主线程与对等线程间数据传递的方式

  • 利用pthread_create函数的第四个参数
  • 通过全局变量
  • 通过pthread_exit函数的参数

6.3 多线程程序中的共享变量

共享变量:一个变量是共享的,当且仅当多个线程引用这个变量的某个实例。

6.3.1 进程的用户地址空间结构

6.3.2 变量类型与运行实例

  • 全局变量:是定义在函数之外的变量,只有一个实例,映射到可读写数据区域(data段、bss段)。任何线程可以引用,最典型的共享变量
  • 本地自动变量:定义在函数内部没有static属性的变量,函数被某个线程调用时,该函数所有本地实例变量在该线程堆栈中有一个运行实例,若多个线程执行同一个函数(或例程),该例程中的变量就拥有多个运行实例。
  • 本地静态变量:定义在函数内部有static属性的变量。即使函数被多个线程调用,也仅有一个运行实例,位于可读写区域。

6.3.3 共享变量的识别

示例:

#include “wrapper.h"
#define N 2
void *thread(void *vargp);
char **ptr;                             int main()
{int i;  pthread_t tid;char *msgs[N] = {"Hello from foo",  "Hello from bar"   }; ptr = msgs; for (i = 0; i < N; i++)  Pthread_create(&tid, NULL, thread, (void *)i); Pthread_exit(NULL);
}
void *thread(void *vargp)
{int myid = (int)vargp;static int cnt = 0;  printf("[%d]: %s (cnt=%d)\n", myid, ptr[myid], ++cnt); return NULL;
}

一个变量是共享的,当且仅当它的一个实例被一个以上的线程引用

  • cnt是共享的,因它仅有一个变量实例,被t0,t1两个线程引用
  • myid不共享,因为线程t0引用myid.t0, 线程t1引用实例myid.t1,每个实例仅有一个线程引用
  • msgs是共享的。因为它通过全局指针ptr,被两个线程t0、t1引用

6.4 线程同步与互斥

6.4.1 变量共享带来的同步错误

  • 出错原因:导致运行结果不正确的根本原因是不同线程对共享变量cnt的操作指令被交错执行。
  • 应对措施:如果能借助某种设施能保证,一个线程中操作共享变量的指令全部执行完成后,才允许另外一个线程执行操作同一全局变量的指令序列,(也就说,操作共享变量的代码段必须互斥执行),那么结果都是正确的
  • 线程互斥:这种限制要求称为线程互斥,而实现线程互斥的设施,称为同步机制。
  • 进程互斥:如果多个进程需要操作共享资源或共享变量,这些操作共享资源或共享变量的代码也需要互斥执行。

6.4.2 临界资源、临界区、进程(线程)互斥问题

临界资源、临界区

  • 临界资源:被多个并发流所共用,但一段时间内一个逻辑流(进程或线程)需要独占使用的资源,如全局变量tickets。
  • 临界区:各逻辑流操作访问临界资源的代码段,一般是一个完整的语句块。

临界区执行的三种模式

  • 模式1:临界区1执行结束后,开始执行临界区2
  • 模式2:临界区2执行结束后,开始执行临界区1
  • 模式3:临界区1已经开始执行,在其结束前开始了临界区2

模式1、模式2都保证了临界区互斥执行,临界资源互斥访问,不会影响程序执行结果正确性,OK。模式3导致了临界资源的并发操作,可能导致程序执行结果出错,ERROR。

多临界区、多临界资源情况

  • 一个临界区是对共享资源的一次完整操作,如果某段代码可划分为多临界资源的多次完整操作,可视为多个临界区。
  • 不同临界资源对应的代码有时可划属不同临界区,划分原则是不影响程序正确性。
  • 两个并发流中操作同一临界资源的临界区必须互斥执行,但操作不同临界资源的临界区可并发执行。

6.4.3 用信号量与P/V操作解决资源调度问题

基本思想:为每种临界资源设置一种许可权(或称令牌、互斥锁),数量为一个。在每个逻辑流临界区代码前增加一段“进入区”代码,用于获取许可权(互斥锁),在临界区后增加一段“退出区”代码,归还临界资源许可权。

简单锁机制

用一个各逻辑流共用的特殊锁变量x来标识临界区的忙闲状态,x=1为”开锁”状态,表示资源空闲,x=0为”上锁”状态,表示资源繁忙。

执行请求上锁操作lock时,锁状态测试与上锁两个子操作是分开执行的,中间插入了其他线程的锁状态测试操作。如果能将上锁状态测试与上锁两个子操作放到一条指令中完成,二者九不会执行了。

用测试设置指令来设计同步机制

测试设置指令语义(x为表示临界资源忙闲状态的标识变量,初值为False,表示该临界资源空闲)

这是一种正确的实现方案,但未进入临界区的线程循环进入测试状态,浪费了宝贵的CPU时间,所以一般仅适合临界区代码比较简单的场合,在操作系统内核中运用较多。如果临界区需要较长的CPU时间,还需寻找更高效的同步机制。

信号量与P、V操作同步机制

  • 信号量(Semaphores):是荷兰学者Dijkstra提出的一种卓有成效的进程同步设施(概念上),其本质是一种封装了加1和减1原语操作、可防止反复测试资源可用状态的计数器。
  • 基本思想:当临界资源空闲时,让请求进程进入临界区,而临界资源被占用时,强迫请求进程阻塞等待,避免浪费CPU时间;并要求从临界区退出的进程唤醒因请求临界资源而被阻塞的一个进程,使其进入临界区。
  • 信号量描述:
typedef  struct  _semaphore {int        value;           //信号量值,表示可用资源数
WaitQueue  L;      //阻塞队列,等待资源的进程队列
} semaphore;
  • 信号量操作:信号量值仅能通过P、V原语操作来安全加1、减1

6.4.4 用信号量及P/V操作解决资源调度问题

用信号量与P/V操作解决临界区互斥执行问题

设置同步的四条原则:

  • 互斥,任何时候最多允许一个线程进入临界区
  • 有限等待,当一个线程在等待队列中被挂起时,应保证在有限的时间内能进入临界区
  • 空闲请进,若无线程在临界区执行,一个请求线程立即进入临界区
  • 让权等待,临界资源忙时,请求线程立即挂起,释放处理器,避免循环测试。

用信号量和P、V操作解决资源分配问题

用信号量和P、V操作解决同步问题

同步:对并发线程操作的执行顺序进行控制,才能保证运行结果的正确性,比如通过共享变量传递数据,就必须保证写操作在前、读操作在后的顺序执行

(1)单向同步

问题描述:ActionA必须在ActionB开始前结束

编程思路:

  • 设想ActionA产生某种虚拟资源(如可用数据)给ActionB获取使用,该资源初始不存在
  • 定义信号量sem管理虚拟资源,其初值sem=0
  • 在产生资源的ActionA后执行signal(sem),使资源数加1,获取资源的ActionB前调用wait(sem),使资源减1

(2)双向同步

  • 双向同步模型: 线程A产生数据,将数据放入缓冲区(全局变量),线程B从缓冲区读出数据,进行处理
  • 线程A的操作“buffer=v1”必须在线程B的操作“v2=buffer”前执行,保证线程B总能读取到新鲜的数据;
  • 线程A的第二次操作“buffer=v1”,必须在线程B第一次“v2=buffer”操作之后,以保证不覆盖buffer中尚未取走的数据。
  • 依此类推。

(3)算法设计

  • 分析思路:假设存在两种虚拟资源:空槽位(无数据的单元)、数据项目(含数据的单元),执行操作”buffer=v1”前要获取空槽位,执行后产生数据项目;执行操作” v2=buffer”前要获取数据项目,执行后清空缓冲区,产生空槽位。将同步问题变成一种资源分配问题,用信号量方法来解决
  • 信号量:定义两个信号量avail、ready分别表示空槽位数、数据项目数
  • 信号量初值:由于开始时缓冲区为空,这两个信号量初值分别为1、0, avail=1, ready=0
  • P、V操作:在buf=v1前调用wait(avail)将空槽位数减1,获得空槽位,之后调用signal(ready)使数据项目数加1;在v2=buf前执行wait(ready)将可用项目数减1,其后执行signal(avail)将空槽位数减1。

6.4.5 用Pthreads同步机制实现线程的互斥与同步

  • Linux系统支持Posix threads(简称Pthreads)线程规范,Pthreads提供四种同步设施:信号量、互斥量、读写锁、条件变量
  • Pthreads规范实现的信号量设施是Pthreads信号量,类型为sem_t,初始化及对应的P操作、V操作函数分别为sem_init、sem_wait、sem_post。

  • 用pthreads信号量编写线程互斥同步程序方法:
  • 先写出同步、互斥算法,
  • 然后用Linux C和信号量实现算法,用pthread函数来实现线程,
  • 最后用pthread信号量与函数来替换信号量定义与P/V操作

用Pthreads信号量实现进程互斥

  • 第一步:识别共享变量与临界区,增加同步描述代码

  • 第2步:将同步描述代码,替换成Pthreads信号量定义与操作函数

  • 第3步,改进方法1:缩小P、V操作加锁的程序范围

  • 第4步,改进方法2,改写程序,将while循环中共享变量移入循环体

用Pthreads信号量编写线程同步程序(以stat1.c)为例

  • Step1:增加同步描述代码

  • Step2:用pthreads信号量与函数替换同步描述代码

6.4.6 共享变量的类型与同步编程小结

1.读写操作不并发

  • 各线程对共享变量并发访问,不会影响程序执行结果的正确性,不需进行同步互斥干预

2. 读与读并发

  • 这类变量的存在不会影响到程序正确性,不需要对并发读操作进行干预

3. 读/写与读/写并发

  • 如果各线程都对共享变量指向读、写操作,先读后写,根据变量的旧值计算新值,然后写回,属于互斥

4.读与写并发

  • 一个线程写共享变量,一个线程读共享变量,需在读线程和写线程间同步

6.5 经典同步问题

6.5.1 生产者/消费者问题

问题描述

  • 有一群生产者线程(或进程)不断地生产产品(数据资料),并提供给一群消费者线程(或进程)去消费
  • 在它们之间设置有n个缓冲区的缓冲池buf[n],生产者进程可将它所生产的产品(数据资料)放入一个缓冲区中,消费者进程可从一个缓冲区取得一个产品(数据资料)消费
  • 所有的生产者进程和消费者进程以异步方式并发运行,它们之间必须保持同步,即不允许消费者进程到一个空缓冲区去取产品,也不允许生产者进程向一个已存有消息、尚未被取走数据资料的缓冲区投放数据资料。

代码

6.5.2 读者/写者问题

问题描述

有两组并发进程: 读者和写者,共享一组数据区,用信号量与PV操作节这些进程同步问题。要求:

  • 1)允许多个读者同时执行读操作;
  • 2)不允许读者、写者同时操作;
  • 3)不允许多个写者同时操作。

设计思路

  • 将数据区看成一种共享变量或临界资源,各个写者个体与整个读者集体竞争对共享资源使用权,因此可设置一个初值为1的互斥信号量wmutex来表示共享数据区使用权。
  • 写者操作为临界区,其前后分别添wait(wmutex)和signal(wmutex)
  • 第一个读者去竞争共享数据区的使用权,执行wait(wmutex),最后一个读者归还数据区使用权,执行signal(wmutex)
  • 需要一个变量count记录读者数,所有读者线程对该共享变量count做并发读写操作,需设置一个互斥信号量mutex,对count的读写操作加锁

代码

Semaphore mutex=1, wmutex=1;
int count=0;Writer_i()   //写者线程i
{while (true){wait(wmutex);写者操作signal(wmutex);}
}
Reader_i()   //读者线程i
{while (true){wait(mutex);count++;if(count==1)   //第一个读者wait(wmutex);singal(mutex);读者操作wait(mutex);count--;if(count==0)  //最后一个读者singal(wmutex);  signal(mutex);}
}

6.8 使用多线程提高并行性

6.8.1 顺序程序、并发程序和并行程序

顺序、并发和并行程序之间的集合关系

  • 顺序程序只有一条逻辑流
  • 并发程序有多条并发流
  • 并行程序是一个运行在多个处理器上的并发程序,并行程序也是并发程序

并行程序运行效率分析

6.8.2 并行程序应用示例

性能分析

  • 四核处理器执行情况

  • 并行程序的加速比(speedup)

(绝对加速比:T1程序顺序执行版本的执行时间)

(相对加速比: T1是程序并行版本在一个核上的执行时间)

  • 效率(efficiency): 处理器运行效率

 一般超过90%的效率是非常好的

6.8.3 使用线程管理多个并发活动

聊天程序threadapp2.c,在两个终端窗口A、B并发执行,都从标准输入读取输入,发送给对方,同时接收来自对方的信息,显示出来,双方通过两个管道文件通信

代码:

 运行结果:

Linux学习06——线程控制与同步互斥相关推荐

  1. Linux学习--rsync+inotify实现自动同步

    Linux学习–rsync+inotify实现自动同步 rsync remote synchronization(远程同步) rsync 的最大特点是会检查发送方和接收方已有的文件,仅传输有变动的部分 ...

  2. Linux c线程间的同步----互斥锁、条件变量、信号量

    线程 一个进程中的所有线程共享为进程分配的地址空间.所以进程地址空间中的代码段和数据段都是共享的. 如果定义一个函数在各个线程中都可以调用,定义一个全部变量,在各个线程中都可以访问到. 各线程共享资源 ...

  3. Linux学习之系统编程篇:互斥锁(pthread_mutex_init / lock / trylock / unlock / destroy)

    一.主要函数介绍 (1)定义锁 : pthread_mutex_t mutex; //互斥锁 数据类pthread_mutex_t (2)初始化锁: int pthread_mutex_init(pt ...

  4. Linux学习~树莓派gpio控制

    WiringPi 是应用于树莓派平台的 GPIO 控制库函数,WiringPi 遵守 GUN Lv3.wiringPi 使用 C 或者 C++ 开发并且可以被其他语言包转,例如 Python.ruby ...

  5. Linux学习之线程封装四:基于接口的封装

    业务逻辑提供者类"CLThreadFunctionProvider" 头文件: View Code #ifndef CLTHREADFUNCTIONPROVIDER_H#defin ...

  6. Linux学习笔记-线程的自然终止

    线程的自然终止 线程主函数退出时,该线程自然终止.例如,下面的线程运行10秒后终止 ... void* Thread_Main(void* context) {for(int i=0; i<10 ...

  7. APUE读书笔记-12线程控制-04同步属性

    转载于:https://blog.51cto.com/quietheart/818811

  8. windows多线程同步互斥--总结

    2019独角兽企业重金招聘Python工程师标准>>> 秒杀多线程面试题系列 参考JustDoIT -- 大部分内容 <Windows核心编程>线程同步对象速查表 对象 ...

  9. c++ linux 线程等待与唤醒_Linux线程同步(互斥量、信号量、条件变量、生产消费者模型)...

    为什么要线程同步? 线程间有很多共享资源,都对一个共享数据读写操作,线程操作共享资源的先后顺序不确定,可能会造成数据的冲突 看一个例子 两个线程屏行对全局变量count++ (采用一个val值作为中间 ...

最新文章

  1. Educational Codeforces Round 13 E. Another Sith Tournament 状压dp
  2. kubernetes目录挂载
  3. linux下c语言读取roed文件,如何在Linux系统上安装Android4.4.docx
  4. Intent, Bundle, ListView的简单使用
  5. 桌面的计算机被删掉了怎么调出来,误删了电脑桌面图标怎么办——一波超简单的操作,分分钟搞定它...
  6. 拼多多组织架构大变动:黄峥不再担任公司CEO
  7. 获取Linux命令源代码的方法【ZT】
  8. 小网站服务器空间,小型网站空间服务器
  9. # 3 网页实现吃豆子动画
  10. 阅读器android工程,一种简单的纯粹——全球首款 EINK屏 安卓手机 BOOX E43 工程机测试体验...
  11. 京东深圳手Q微信事业部测试工程师面试总结
  12. [SP]梦网masterSP模式下的sp生存
  13. 刚刚过去的六一,OPPO Find新机让一些“大孩子”忍不住落泪!
  14. 基于SpringBoot + Vue的个人博客系统07——文章列表和文章详情
  15. 如何计算EEG信号的香农熵Shannon entropy(附Matlab程序)
  16. 入门编程指南:如何从零开始学习编程?
  17. 利用Python构建股票交易策略 !
  18. Java中 List、Set、Map 之间的区别
  19. 计算机论文答辩2分钟演讲稿,论文答辩三分钟自述
  20. 清华EMBA课程系列思考之六 -- 比較文明视野下的中华领导智慧、企业管理与经济解析...

热门文章

  1. 荧光标记转铁蛋白-(FITC, cy3, cy5, cy7, 香豆素, 罗丹明)
  2. 考勤不是非得按指纹 刷脸操作更亲民!
  3. 【gitlab-runner】gitlab-runner安装注册到https的gitlab
  4. 查询自动售货机中商品的价格
  5. 实例解析导购电商APP快速开启变现,收益提升184%
  6. c语言表达式1 4 2.75,东师C程序设计20秋在线作业1 2【标准答案】
  7. 长春牙齿矫正日记第二篇-----------洗牙以及口腔扫描
  8. Node.js TLSSocket 库里涉及到的证书链的概念简介
  9. quartus ii中的dff元件(D触发器)prn引脚的含义
  10. 【tika】tika介绍