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、死锁

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

  • 相关阅读:
    Codeforces Round #251 (Div. 2) A
    topcoder SRM 623 DIV2 CatAndRat
    topcoder SRM 623 DIV2 CatchTheBeatEasy
    topcoder SRM 622 DIV2 FibonacciDiv2
    topcoder SRM 622 DIV2 BoxesDiv2
    Leetcode Linked List Cycle II
    leetcode Linked List Cycle
    Leetcode Search Insert Position
    关于vim插件
    Codeforces Round #248 (Div. 2) B. Kuriyama Mirai's Stones
  • 原文地址:https://www.cnblogs.com/tymjava/p/5024202.html
Copyright © 2011-2022 走看看