zoukankan      html  css  js  c++  java
  • 20135220谈愈敏--信息安全系统设计基础第十一周学习总结

    第十二章 并发编程

    如果逻辑流在时间上重叠,那么他们就是并发的,硬件异常处理程序、进程和UNIX信号处理程序都是熟悉的例子。并发现象不仅在内核中存在,在应用级别的程序中也存在。

    访问慢速的I/O设备
    
    与人交互
    
    通过推迟工作以降低延迟
    
    服务多个网络客户端
    
    在多核机器上进行并行计算
    

    操作系统提供给应用程序的三种构造并发程序的方法

    进程
    
    I/O多路复用
    
    线程
    

    12.1 基于进程的并发编程

    在接受连接请求之后,服务器派生出一个子进程,这个子进程获得服务器描述表完整的拷贝。子进程关闭它的拷贝中监听描述符3,父进程关闭它的已连接描述符4的拷贝,因为不需要这些描述符了。

    程序实例:

    • 因为通常服务器会运行很长时间,所以需要一个SIGCHLD处理程序,来回收僵死进程。因为当SIGCHLD执行时,信号是阻塞的,而UNIX信号是不排队的,所以SIGCHLD必须准备好回收多个僵死进程。

    • 另外注意,循环中的父进程和子进程关闭各自需要关闭的描述符。

    关于进程的优劣:

    进程能够共享文件表,但不共享用户地址空间。
    

    12.2 基于I/O多路复用的并发编程

    1、面对困境——服务器必须响应两个互相独立的I/O事件:
    1)网络客户端发起的连接请求  
    2)用户在键盘上键入的命令 
    

    解决的办法是I/O多路复用技术。基本思想是,使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。

    select函数如下:

    程序示例如下:

    使用select函数的过程如下:

    第一步,初始化fd_set集,19~22行;
    
    第二步,调用select,25行;
    
    第三步,根据fd_set集合现在的值,判断是哪种I/O事件,26~31行。
    
    2、基于I/O多路复用的并发事件驱动服务器

    I/O多路复用可以用做并发事件驱动程序的基础,在事件驱动程序中,流是因为某种事件而前进的,一般概念是把逻辑流模型化为状态机。一个状态机就是一组状态、输入事件和转移。

    并发事件驱动程序中echo服务器中逻辑流的状态机,如下图所示:

    12.3 基于线程的并发编程

    1、线程

    线程就是运行在进程上下文中的逻辑流。线程由内核自动调度,每个线程都有它自己的线程上下文。

    2、线程执行模型

    多线程的执行模型在某些方面和多进程的执行模型相似。每个进程开始生命周期时都是单一线程,这个线程称为主线程。在某一时刻,主线程创建一个对等线程,从在此刻开始,两个线程就并发地运行。

    3、Posix线程

    创建线程:

    获取自身ID:

    终止线程:

    有以下四种方式终止线程:

    1、当顶层的线程例程返回时,线程会隐式终止;
    
    2、线程调用pthread_exit函数,线程会显示终止;
    如果主线程调用pthread_exit,它会等到所有其他对等线程终止,然后再终止主线程和整个线程,返回值为thread_return;
    
    3、某个对等线程调用exut函数,则函数终止进程和所有与该进程相关的线程;
    
    4、另一个对等线程调用以当前ID为参数的函数ptherad_cancel来终止当前线程。
    

    回收已终止线程的资源:

    pthread_join函数会终止,直到线程tid终止。和wait不同,该函数只能回收指定id的线程,不能回收任意线程。

    分离线程:

    一个可结合的线程能够被其他线程回收其资源和杀死,在被其他线程回收之前,它的存储其资源是没有被释放的;相反,一个分离的线程是不能被其他线程回收或杀死的。它的存储器资源是在它终止时系统自动释放的。默认情况下,线程被创建成可结合的。但现实程序中,有很好的理由要使用分离线程。

    初始化线程:

    该函数用来初始化多个线程共享的全局变量。

    一个基于线程的并发服务器:

    以上程序可能会出错,因为在对等线程的赋值语句和主线程的accept的语句见引入了竞争——如果赋值语句在下一个accept之前完成,则不会出错;如果赋值语句是在accept之后完成,那么对等线程的局部变量connfd就得到下一次连接的描述符。

    • 解决办法:必须将每个accept返回的描述符分配到它自己的动态分配的存储器块。(21~23行)

    32行:动态内存空间释放,释放那个指向动态内存的指针即可,不一定非要是malloc当时生成的指针。

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

    每个线程都有它自己独自的线程上下文,包括:

    线程ID
    栈
    栈指针
    程序计数器
    条件码
    通用目的寄存器值。
    

    每个线程和其他线程一起共享进程上下文的剩余部分。

    寄存器是从不共享的,而虚拟存储器总是共享的。
    

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

    全局变量
    本地自动变量(不共享)
    本地静态变量
    

    12.5 用信号量同步线程

    共享变量引入了同步错误。

    进度图:

    轨迹线示例:

    临界区(不安全区):

    信号量:是用信号量解决同步问题,信号量s是具有非负整数值的全局变量,有两种特殊的操作来处理(P和V):

    • P(s):如果s非零,那么P将s减1,并且立即返回。如果s为0,那么就挂起这个线程,直到s变为非零;

    • V(s):V操作将s加1。

    使用信号量实现互斥:

    利用信号量调度共享资源:在这种场景中,一个线程用信号量操作来通知另一个线程,程序状态中的某个条件已经为真了。两个经典应用:

    1、生产者——消费者问题

    要求:必须保证对缓冲区的访问是互斥的;还需要调度对缓冲区的访问,即,如果缓冲区是满的(没有空的槽位),那么生产者必须等待直到有一个空的槽位为止,如果缓冲区是空的(即没有可取的项目),那么消费者必须等待直到有一个项目变为可用。

    注释:513行,缓冲区初始化,主要是对缓冲区结构体进行相关操作;1619行,释放缓冲区存储空间;2229行,生产(有空槽的话,在空槽中插入内容);324行,消费(去除某个槽中的内容,使该槽为空)

    2、读者——写者问题

    修改对象的线程叫做写者;只读对象的线程叫做读者。写着必须拥有对对象的独占访问,而读者可以和无限多个其他读者共享对象。读者——写者问题基本分为两类:第一类,读者优先,要求不要让读者等待,除非已经把使用对象的权限赋予了一个写者。换句话说,读者不会因为有一个写者等待而等待;第二类,写者优先,要求一定能写者准备好可以写,它就会尽可能地完成它的写操作。同第一类问题不同,在一个写者后到达的读者必须等待,即使这个写者也是在等待。以下程序给出了第一类读者——写者问题的解答:

    注释:信号量w控制对访问共享对象的临界区的访问。信号量mutex保护对共享变量readcnt的访问,readcnt统计当前临界区的读者数量。每当一个写者进入临界区,它就对互斥锁w加锁,每当它离开临界区时,对w解锁,这就保证了任意时刻临界区最多有一个写者;另一方面,只有第一个进入临界区的读者对w加锁,而只有最后一个离开临界区的读者对w解锁。

    综合:基于预线程的并发服务器

    之前介绍的基于线程的并发服务器,需要为每个客户端新建一个新线程,导致不小的代价。一个基于预线程化的服务器通过使用如下图所示的生产者——消费者模型来降低这种开销。服务器是由一个主线程和一组工作组线程构成的。主线程不断地接受来自客户端的连接请求,并将得到的连接描述符放在一个有限缓冲区中。每一个工作组线程反复地从共享缓冲区中取出描述符,为客户端服务,然后等待下一个描述符。

    程序示例如下图:

    注释:2627行,产生工作组线程;2932行,接受客户端的连接请求,并把这些描述符放到缓冲区;35~43行,每个线程所要完成的工作

    程序示例如下图:

    注释:19行,初始化线程共享的全局变量。

    初始化有两种方式:

    一种是它要求主线程显示地调用一个初始化函数;
    
    第二种是,在此显示的,当第一次有某个线程调用echo_cnt函数时,使用pthread_once函数去调用初始化函数。
    

    其他并发问题

    1、线程安全

    四个(不相交的)线程不安全函数类:

    1、不保护共享变量的函数
    
    2、保持跨越多个调用的状态的函数
    
    3、返回指向静态变量的指针的函数
    
    4、调用线程不安全函数的函数
    
    2、可重入性

    可重入函数是线程安全函数的一个真子集,它不访问任何共享数据。可重入安全函数通常比不可重入函数更有效,因为它们不需要任何同步原语。

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

    4、竞争

    当程序员错误地假设逻辑流该如何调度时,就会发生竞争。为了消除竞争,通常我们会动态地分配内存空间。

    5、死锁

    当一个流等待一个永远不会发生的事件时,就会发生死锁。

  • 相关阅读:
    如何垂直居中一个浮动元素?
    微信小程序vant-search获取不到输入的搜索内容踩坑
    关于微信小程序无法使用css3过度属性transition以及微信小程序如何利用api进行画面过度的展示
    Spring Shedule Task之注解实现 (两次启动Schedule Task 的解决方案)
    学习站点记录
    Spring 通过配置文件注入 properties文件
    Liunx下安装jdk
    Mybatis使用generator自动生成映射配置文件信息
    Tomcat容器虚拟路径设置
    Spring init-method和destroy-method 的使用
  • 原文地址:https://www.cnblogs.com/tymjava/p/5024202.html
Copyright © 2011-2022 走看看