如果逻辑控制流在时间上是堆叠,那末它们就是并发的(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.728行,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类,那末f1定不安全,也没有办法去修正,只能改变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。并输出。
i1定是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!!