如果逻辑控制流在时间上是堆叠,那末它们就是并发的(concurrent)
。这类常见的现象称为并发(concurrency)
。
我们主要将并发
看作是1种操作系统内核用来运行多个利用程序的机制。
但是,并发
不单单局限于内核。它也能够在利用程序中扮演重要的角色。
例如
Unix
信号处理程序如何允许利用响应异步事件 ctrl-c
其他情况
访问慢速I/O
装备
I/O
装备(例如磁盘)的数据到达时,内核会运行其他进程,使CPU保持繁忙。与人交互
通过推延工作以下降延迟
并发
来下降某些操作的延迟使用利用级并发的利用程序称为并发程序(concurrent program)
.
操作系统提供3种基本的构造并发程序的方法:
进程
每一个逻辑控制流 都是1个进程
由于进程
有独立的虚拟地址空间
通讯
,控制流必须使用某种显式的进程间通讯(interprocess communication,IPC)
进制I/O
多路复用(暂时不太懂) 逻辑流
。逻辑流
被模型化为状态机
,数据到达文件描写符后,主程序显式地从1个状态转换到另外一个状态。线程
是运行在1个单1进程上下文中的逻辑流
,有内核调度。 进程
1样由内核进行调度。I/O
多路复用1流1样同享1个虚拟地址空间。1个构造并发服务器的自然方法就是,在父进程中接收客户端连接要求,然后创建1个新的子进程来为每一个新客户端提供服务。
用Signal(SIGCHLD,sigchld_handler)
回收僵死进程。
8.5.7
28
行,33
行 父子进程各自关闭他们不需要的拷贝。
由于文件表项的援用计数,直到父进程关闭它的描写符,才算结束1次连接
对在父,子进程间同享状态信息,进程有1个非常清晰的模型
。
进程
具有独立的虚拟地址空间即是 优点,也是 缺点。
优点
:1个进程
不可能不谨慎覆盖另外一个进程的虚拟存储空间。
缺点
:独立的地址空间使得进程间同享信息也很困难。
必须使用显式的IPC
(进程间通讯)机制。
常常还比较慢
IPC
的开消都很大。假定要编写1个echo服务器
。
服务器
既能响应客户端
的要求exit
).因此,服务器
必须要响应两个相互独立的I/O
事件
不管先等待那个事件都不是理想的,解决办法之1是就是使用I/O多路复用技术
。
select
函数,要求内核挂起进程,只有1个或多个I/O
事件产生后,才将控制返回给利用程序。线程(thread)
就是运行在进程上下文中的逻辑流。
内核
调度。每一个线程都有它自己的线程上下文(thread context)
.
线程ID(Thread ID,TID)
.所有运行在该进程里的线程
同享该进程的全部虚拟地址空间。
每一个进程开始生命周期时都是单1线程,这个线程称为主线程(main thread)
。
对等线程(peer thread)
。 read
或sleep
间隔计时器
中断。对等线程
。对等线程
履行1段时间,将控制传递回主线程。在某些方面,线程
履行是不同等于进程的。
线程
的上下文切换的开消比进程
的小很多,快很多线程
不是依照严格的父子层次来组织。 线程池(pool)
。 线程池
概念的主要影响是对等线程
终止。Posix线程
(Pthreads
)是在C程序中处理线程的1个标准接口。
Unix
系统可用这是我们第1个线程化的代码,仔细解析。
线程的代码和本地数据被封装在1个线程例程(thread routine)
中。
2
行代码所示:每一个线程例程
都以1个通用指针作为输入,并返回1个通用指针。如果想传递多个参数给线程例程
指针
。如果想要线程例程
返回多个参数。
指针
。tid
寄存对等线程的线程ID
。
主线程调用pthread_create
函数创建1个新的对等线程(第7
行)。
pthread_create
的调用返回时,主线程和新创建的对等线程同时运行。通过调用pthread_join
,主线程等待对等线程
的终止。
对等线程
输出Hello,world
。
主线程
终止。线程通过调用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个新的线程。
arg
,在新线程的上下文中运行线程例程f
.能用attr
参数改变新创建线程的默许属性。
NULL
作为attr
的参数。pthread_create
返回时,参数tid
包括新创建线程的ID
。
pthread_self
函数来取得它自己的线程ID
。1个线程是以以下方式之1来终止
的
线程例程
返回时,线程会隐式地终止
。通过调用pthread_exit
函数,线程会显示地终止
。
pthread_exit
. thread_return
原型以下
#include<pthread.h>
void pthread_exit(void *thread_return)
//成功返回0,出错返回非0
某个对等线程调用Unix
的exit
函数,函数终止进程和所有与该进程有关的线程
。
对等线程
通过以当前线程ID为参数调用pthread_cancle
函数来终止当前线程。
原型
#include<pthread.h>
void pthread_cancle(pthread_t tid);
//成功返回0,出错返回非0
线程通过调用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个线程的结束。
Stevens
在书中指出这是1个设计毛病。在任何1个时间点上,线程是可结合的(joinable)
或 是分离的(detached)
。
1个可结合的线程
能够被其他线程收回其资源或杀死。
1个分离的线程
是不能被其他线程收回其资源或杀死。
pthread_detach
函数分离可结合线程tid
。
#include<pthread.h>
int pthread_detach(pthread_t tid);
返回:若成功则返回0,若出错则返回非零。
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
。
第2次,第3次以参数once_control
调用pthread_once
时,啥事也不产生。
当你需要动态初始化多个线程同享的全局变量时,pthread_once
函数是很有用的。
注意使用malloc
动态给1个connfdp
,否则可能两个线程援用同1个connfdp
的地址。
竞争
为在线程例程
中避免存储器泄漏,使用分离线程
。
主线程
中malloc
的变量。为了解1个C程序中的1个变量是不是同享,有1些基本的问题要解答
基础存储器模型
是甚么?变量实例
是如何映照到存储器的?为了使同享讨论具体化,使用下图的程序作为示例。
示例程序由1个创建两个对等线程
的主线程组成。主线程传递1个唯1的ID
给每一个对等线程,每一个对等线程利用这个ID
输出1个个性化的信息,和调用该线程例程
的总次数。
线程化的C程序中的变量根据它们的存储类型被映照到虚拟存储器:
全局变量
全局变量
是定义在函数以外的变量。 读/写区域
包括每一个全局变量的1个实例。线程
都可以援用。5
行声明的ptr
。本地自动变量
本地自动变量
就是定义在函数内部但是没有static
属性的变量。 栈
包括它自己的所有本地自动变量的实例。本地静态变量
本地静态变量
是定义在函数内部有static
属性的变量。 读/写区域
。25
行的cnt
.我们说1个变量v
是同享
的,当期仅当它的1个实例被1个以上的线程
援用。
例如:
cnt
是同享的myid
不是同享的msgs
这类本地自动变量也能被同享是很重要的。同享变量10分方便,但是他们也引入了同步毛病(synchronization error)
的可能性。
斟酌下图的程序。
到底哪里出错了呢?这个毛病10分隐晦
,必须通过研究计数器循环
时的汇编代码才能看出。
当badcnt.c
中的两个对等线程在1个单处理器上并发履行
,机器指令以某种顺序1个接1个地完成。同1个程序每次运行的顺序都可能不同,这些顺序
中有1些将会产生正确结果,但是其他的不会。这就是同步毛病
关键点
: 1般而言,你没有办法预测操作系统是不是将为你的线程选择1个正确的顺序。
cnt
正确的顺序和毛病的顺序(正确结果cnt=2
,毛病结果cnt=1
)我们可以借助于1种叫做进度图(progress graph)
的方法来阐明这些正确和不正确的指令顺序的概念。将在接下来介绍。
进度图(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条安全轨迹线
Edsger Dijksta
,并发编程领域的先锋任务,提出了1种经典的解决同步不同履行线程问题
的方法
这类方法是基于1种叫做信号量(semaphore)
的特殊类型变量。
信号量s
是具有非负整数值的全局变量。
只能由两种特殊的操作来处理,这两种操作称为P
和V
P(s),Proberen,测试
s
是非零的,那末P操作
将s
减1,并且立即返回。s
为零,那末就挂起这个线程,直到s
变成非零。 V
操作会重启这个线程。P操作
将s
减1,并将控制返回给调用者。V(s),Verhogen,增加
V操作
将s
加1.P操作
等待s
变成非零。 V操作
随机会重启这些线程中的1个。s
减去1,完成它的P操作
。重点,P操作
和V操作
都是不可分割的,也就是本身确保了是1个带有安全轨迹的操作。(所以又叫原语
)
cnt++
的操作。加1
这个操作中,加载,加1,存储信号量进程是不可分割的。P
和V
的定义确保了1个正在运行的程序绝不可能进入这样1种状态,也就是不可能有负值。
这个属性叫做信号量不变性(semaphore invariant)
,为控制并发程序的轨迹线提供了强有力的工具。
信号量
提供了1种很方便的方法来确保对同享变量的互斥访问。
基本的思想是
同享变量
(或1组相干的同享变量) 与1个信号量s(初始为
)`联系起来。P(s)
和V(s)
操作相应的临界区包围起来。以这类方式保护同享变量的信号量叫做2元信号量(binary semaphore)
以提供互斥为目的的2元信号量
常常也称为互斥锁(mutex)
。
P操作
叫做互斥锁加锁
。V操作
叫做互斥锁解锁
。占用这个互斥锁
。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
的更新包围P
和V
操作,从而保护了它们。
for( i = 0 ;i < niters ;i++) {
P(&mutex);
cnt++;
V(&mutex);
}
除提供互斥
外,信号量的另外一个重要作用是调度对同享资源的访问。
两个经典而有用的例子。
图给出了生产者消费者问题
生产者线程
反复地生成新的项目
,并把它们插入到缓冲区中。消费者线程
不断地从缓冲区取出这些项目
,然后消费使用它们。由于插入和取出项目都触及更新同享变量
互斥的
缓冲区
的访问。 我们将开发1个简单的包,叫做SBUF
,用来构造生产者-消费者程序。
SBUF
操作类型为sbuf_t
的有限缓冲区。
项目
寄存在1个动态分配的n
项整数数组(buf
)中。front
和rear
索引值记录该队列的第1项和最后1项。mutex
信号量提供互斥的缓冲区访问slots
和items
信号量分别记录空槽位和可用项目的数量。以下给出SBUF
函数的实现:
sbuf_init
函数进行初始。 front
和rear
表示1个空的缓冲区。sbuf_deinit
函数是当利用程序使用完缓冲区时,释放缓冲区存储。sbuf_insert
sbuf_remove
读者-写着
问题是互斥问题的1个概括。
1组并发的线程要访问同1个数据对象。
写者
读者
写者
必须具有对对象的独占访问。
读者
可以和无穷多个其他读者同享对象。读者-写者
问题有几个变种,都是基于读者和写者的优先级
第1类读者-写者问题
读者
优先,要求不要让读者等待,除非已把1个使用权限赋予了1个写者
。读者
不会由于有1个写者
在等待而等待。第2类读者-写者问题(?)
写者
优先,要求1但1个写者准备好可以写,它就会尽量地完成它的写操作。给出第1类读者-写者问题答案。
信号量w
控制对访问同享对象的临街区的访问。
读者
w
只对第1个读者上锁w
对最后1个走的读者解锁写者
w
上锁w
解锁mutex
保护对同享变量readcnt
的访问。 readcnt
统计当前临界区的读者数量。所有读者-写者
答案都有可能致使饥饿
为每一个新的客户端创建新的线程,有很多的代价。
1个基于预线程化
的服务器利用生产者-消费者模型构造1个更高效力的方式。
主要用于多核CPU的算法。
比如:利用并行来完成n路递归
互斥
和生产者-消费者同步
的技术,只是并提问题的冰山1角。
同步问题
从根本来讲是很难的问题。
这章我们以线程
为例讨论。
同步问题
在任何并发流
操作同享资源时都会出现。 信号
时,回收进程时的竞争
。1个函数被称为线程安全的(thread-safe)
,当且仅当被多个并发线程反复地调用时,它会1直产生正确的结果。否则就是线程不安全的(thread-unsafe)
我们能够定义出4个
(不相交)线程不安全函数类:
P,V
这样的同步操作来保护同享的变量第 2 类 : 保持逾越多个调用状态的函数
1个伪随机数生成器
是这类线程不安全的例子。
由于产生的结果依赖于上1个next
的值。
seed
不管运行多少次,都是一样的结果。线程不安全
的解决方案: 重写
static
,而是依托调用者在参数中传递状态信息。第 3 类 :返回指向静态变量的指针的函数( 有点类似第1类 )
解放方案
指针
。加锁-拷贝技术:
用上面的原理写1个线程不安全函数
的包装函数来实现线程安全。
第 4 类 : 调用线程不安全函数的函数。
f
调用线程不安全函数g
。那末f
可能不安全。 g
是第2类,那末f
1定不安全,也没有办法去修正,只能改变g
.g
是第1,3类,可以用加锁-拷贝技术来解决。有1类重要的线程安全函数
,叫做可重入函数(reentrant function)
其特点在于它们有这样1种属性。
同享数据
。被分为两类
隐式可重入
参数可以有指针
是不是可重入,同时取决于调用者
,和被调用者
。
可重入函数
比较高效是由于不需要同步操作。
认识到可重入性有时即是调用者
也是被调用者
的属性。
被调用者
的单独属性。大多数Unix
函数,包括大部份定义在标准C库的函数(malloc
,free
,realloc
,printf
和scanf
)都是线程安全的。
asctime
,ctime
,localtime
函数是在不同时间和数据格式相互来回转换时常常使用的函数。
gethostbyname
,gethostbyaddr
,inet_ntoa
函数是常常用的网络编程函数。
strtok
函数是1个过时了的同来分析字符串的函数。
Unix
系统提供大多数线程不安全函数的可重入版本。
_r
后缀结尾。gethostbyname_r
。当1个程序的正确性依赖于1个线程要在另外一个线程到达y
点之前到达它的控制流中的x
点,就会产生竞争
。
竞争
产生的理由是由于程序员假定某种特殊的轨迹线穿过履行状态空间。例子:
程序10分简单。
主线程创建了4个对等线程
,并传递1个指向循环变量i
的指针作为线程的ID
。并输出。
i
1定是4个不同的。所以会想固然觉得会输出4个不同的ID
。对等线程
给myid
赋值结束后,i
才会自增。i++
,和对等线程myid=*((int *)vargp)
的 竞争
。解决方案:用1个临时地址保存i
信号量
引入了1种潜伏的使人讨厌的运行时毛病,叫做死锁 (deadlock
)。
进度图
对理解死锁是1个无价的工具。
死锁的区域d
是1个只能进,不能出的区域。
制止区
,能进去。制止区
了。如果制止区不堆叠,1定不会产生死锁
。
死锁
。死锁是1个相当困难的问题,由于它总是不可预测的。
死锁
区域。使用2元信号量来实现互斥,可以利用1下有效的规则。
互斥锁加锁顺序规则
:如果对程序中每对互斥锁(s,t)
,每一个占用s
和t
的线程都依照相同的顺序对它们加锁,那末这个程序就是无死锁的。
GGGGGGGGGGG,暂时告1段落了!!!!!!!!!!!!!!ddd!!