信息安全系统设计基础第十三周学习总结

并发编程

并发可以看做是一种操作系统在内核用来运行多个应用程序的机制,也可以应用程序中扮演重要角色

·访问慢速I/O设备:交替执行I/O请求和其他有用工作来使用并发
·与人交互:和计算机交互的人同样要求计算机有同时执行多个任务的能力
·推迟工作以降低延迟:推迟其他操作和并发的执行,利用并发来执行操作的延迟
·服务多个网络客户端:创建并发服务器,为每个客户端创建单独的逻辑刘,避免独占
·在多核机器上进行并行计算:被划分成并行流的应用程序在多核机器上运行的比在单核快

构造并发程序的方法

·进程
·I/O多路复用
·线程

12.1 基于进程的并发编程

构造并发进程最简单的方法就是用进程:在父进程中接受客户端连接的请求然后创建一个新的子进程来为每个新的客户端提供服务

12.1.1基于进程的并发服务器

12.1.2关于进程的优劣

父子进程共享信息状态:共享文件表,单步共享用户地址空间。优点是可以防止覆盖进程的虚拟存储器;缺点是使用显式的进程间通讯,速度较慢

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

基本思路:使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。

select函数有两个输入:一个称为读集合的描述符字符结合和该读集合的基数

12.2.1基于I/O多路复用的并发事件驱动服务器

不严格的说一个状态机就是一组状态,输入事件和转移

流程:

select函数检测到输入事件
add_client函数创建新状态机
check_clients函数执行状态转移(在课本的例题中是回送输入行),并且完成时删除该状态机。

函数:

init_pool:初始化客户端池
add_client:添加一个新客户端到火丁客户端池中
check_clients:回送来自每个准备好的已连接描述符的一个文本行

12.2.2 I/O多路复用技术的优劣

1.优点

相较基于进程的设计,给了程序员更多的对程序程序的控制
运行在单一进程上下文中,所以每个逻辑流都可以访问该进程的全部地址空间,共享数据容易实现
可以使用GDB调试
高效

2.缺点

编码复杂
不能充分利用多核处理器

12.3基于线程的并发编程

线程:就是运行在进程上下文中的逻辑流。混合了以上两种方法

每个进程都有自己的线程上下文,包括:一个唯一的整数线程ID——TID、栈、栈指针、程序计数器、通用目的寄存器、条件码。

12.3.1线程执行模型

主线程:在每个进程开始生命周期时都是单一线程——主线程,与其他进程的区别仅有:它总是进程中第一个运行的线程。

对等线程:某时刻主线程创建,之后两个线程并发运行。

每个对等线程都能读写相同的共享数据。

主线程切换到对等线程的原因:

·主线程执行一个慢速系统调用,如read或sleep
·被系统的间隔计时器中断

12.3.2 posix线程

POSIX线程是在C程序中处理线程的一个标准接口

12.3.3创建线程

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

创建一个新的线程,带着一个输入变量arg,在新线程的上下文运行线程例程f。

attr默认为NULL

参数tid中包含新创建线程的ID

12.3.4终止线程

终止线程的几个方式:

隐式终止:顶层的线程例程返回

显示终止:调用pthread_exit函数

#include<pthread.h>
void pthread_exit(void *thread_return)

某个对等线程调用Unix的exit函数,会终止进程与其相关线程

另一个对等线程通过以当前线程ID作为参数调用pthread_cancle来终止当前线程

12.3.5 回收已终止线程的资源

pthread_join函数:

#include <pthread.h>
int pthread_join(pthread_t tid,void **thrad_return);

这个函数会阻塞,知道线程tid终止,将线程例程返回的(void*)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有存储器资源

12.3.6 分离线程

在任何一个时间点上,一个线程是可结合或者分离的
可结合的线程

能够被其他线程收回其资源和杀死
被收回钱,它的存储器资源没有被释放
每个可结合线程要么被其他线程显式的收回,要么通过调用pthread_detach函数被分离

分离的线程

不能被其他线程回收或杀死
存储器资源在它终止时由系统自动释放

12.3.7 初始化线程

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));

once_control是一个全局或者静态变量

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

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

12.4.1 线程存储器模型

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

12.4.2将变量映射到存储器

根据存储器类型被映射到虚拟存储器中

1、全局变量

定义在函数之外
运行时,虚拟存储器的读写区域只包括每个全局变量的一个实例,任何线程都可以引用

2、本地自动变量

定义在函数内部但是没有static属性的变量
运行时,每个线程的站都包括他自己的所有本地自动变量的实例

3、本地静态变量

定义在函数内部并且有static属性的变量
运行时,和全局变量一样

12.4.3 共享变量

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

12.5 用信号量同步线程

共享线程十分方便但是会带来同步错误的可能性,一般而言,没有办法预测操作系统是否将为你的线程选择一个正确的顺序。

12.5.1 进度图

进度图:是将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,原点对应于没有任何线程完成一条指令的初始状态。

进度图将指令执行模式化为一种状态到另一种状态的转换。转换被表示为一条从一点到相邻点的有向边

互斥访问:临界区不应该和其他进程的临界区交替使用

不安全区:两个临界区的交集形成的状态空间区域

12.5.2 信号量

信号量:是具有非负整数值的全局变量,只能由两种特殊的操作来处理:P和V。

注意

每个信号量在使用之前必须初始化
V的定义中没有定义等待形成被重新启动的顺序,唯一的要求是V必须只能启动一个正在等待的线程

12.5.3 使用信号量来实现互斥

基本思想:将每个共享变量(或者一组相关的共享变量)与一个信号量s(初始为1)联系起来,然后用P和V操作将相应的临界区包围起来。用这种方式保护共享变量的信号叫做二元信号量。

概念:

互斥锁:以提供互斥为目的的二元信号量
加锁:对一个互斥锁执行P操作
解锁;对一个互斥锁执行V操作
计数信号量:被用作一组可用资源的计数器的信号量
禁止区:由于信号量的不变性,没有实际可能的轨迹能够包含禁止区中的状态。

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

这部分的内容在操作系统中有学过,比较了解

1、生产者和消费者

2、读者和写者

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

12.6 使用线程提高并行性

所有程序的集合能够划分成不相交的顺序程序集合和并发程序集合。写顺序程序只有一条逻辑流。并发程序有多条逻辑流。并行程序是一个运行早多个处理器上的并发程序。因此,并行程序的集合是并发程序集合的真子集。

12.7 其他并发问题

12.7.1 线程安全

一个函数被称为线程安全,当且仅当被多个并发线程反复调用时会一直产生正确的结果
不安全函数类:

1、不保护共享变量的函数

利用像P和V这样的同步操作来保护共享的变量
优点:在调用抽程序中不需要做任何修改
缺点:同步操作将减慢程序执行的时间

2、保持跨越多个调用的状态的函数

一个伪随机数产生器是这类线程不安全函数的简单例子

3、返回指向静态变量的指针的函数
某些函数将计算结果放在一个静态变量中,然后返回一个指向这个变量的指针,如果并发线程中调用这些函数,那么可能会发生灾难

处理方法:

第一种:重写函数,使的调用者传递存放结果的变量的地址,修改函数的源代码
第二种:加锁—拷贝,将线程的不安全函数与互斥锁联系起来

4、调用线程不安全函数的函数

12.7.2 可重入性

当他们被多个线程调用时不会引用任何共享数据

1、显式可重入的:
所有函数参数都是传值传递,没有指针,并且所有的数据引用都是本地的自动栈变量,没有引用静态或全剧变量。

2、隐式可重入的:
调用线程小心的传递指向非共享数据的指针。

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

unix系统通过大多数线程不安全函数的可重入版本,可重入版本的名字以_r为后缀结尾。

12.7.4 竞争

1、 竞争发生的原因:
一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点。也就是说,程序员假定线程会按照某种特殊的轨迹穿过执行状态空间,忘了一条准则规定:线程化的程序必须对任何可行的轨迹线都正确工作。

2、消除方法:
动态的为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针

12.7.5 锁死

竞争和锁死都在操作系统中有学过

原文地址:https://www.cnblogs.com/javajy/p/5024552.html