标签:
如果逻辑控制流在时间上是重叠,那么它们就是并发的(concurrent)。这种常见的现象称为并发(concurrency)。
我们主要将并发看做是一种操作系统内核用来运行多个应用程序的机制。
但是,并发不仅仅局限于内核。它也可以在应用程序中扮演重要的角色。
例如
Unix信号处理程序如何允许应用响应异步事件 ctrl-c其他情况
访问慢速I/O设备
I/O设备(例如磁盘)的数据到达时,内核会运行其他进程,使CPU保持繁忙。与人交互
通过推迟工作以降低延迟
并发来降低某些操作的延迟使用应用级并发的应用程序称为并发程序(concurrent program).
操作系统提供三种基本的构造并发程序的方法:
进程
每个逻辑控制流 都是一个进程
因为进程有独立的虚拟地址空间
通信,控制流必须使用某种显式的进程间通信(interprocess communication,IPC)进制I/O多路复用(暂时不太懂) 逻辑流。逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换到另一个状态。线程是运行在一个单一进程上下文中的逻辑流,有内核调度。 进程一样由内核进行调度。I/O多路复用一流一样共享一个虚拟地址空间。一个构造并发服务器的自然方法就是,在父进程中接收客户端连接请求,然后创建一个新的子进程来为每个新客户端提供服务。

描述符3)上的连接请求客户端1的连接请求描述符4)。
描述符3,4)监听描述符3描述符4

用Signal(SIGCHLD,sigchld_handler)回收僵死进程。
8.5.728行,33行 父子进程各自关闭他们不需要的拷贝。
因为文件表项的引用计数,直到父进程关闭它的描述符,才算结束一次连接
对于在父,子进程间共享状态信息,进程有一个非常清晰的模型。
进程拥有独立的虚拟地址空间即是 优点,也是 缺点。
优点:一个进程不可能不小心覆盖另一个进程的虚拟存储空间。
缺点:独立的地址空间使得进程间共享信息也很困难。
必须使用显式的IPC(进程间通信)机制。
往往还比较慢
IPC的开销都很大。
假设要编写一个echo服务器。
服务器既能响应客户端的请求exit).因此,服务器必须要响应两个相互独立的I/O事件
无论先等待那个事件都不是理想的,解决办法之一是就是使用I/O多路复用技术。
select函数,要求内核挂起进程,只有一个或多个I/O事件发生后,才将控制返回给应用程序。线程(thread) 就是运行在进程上下文中的逻辑流。
内核调度。每个线程都有它自己的线程上下文(thread context).
线程ID(Thread ID,TID).所有运行在该进程里的线程共享该进程的整个虚拟地址空间。

每个进程开始生命周期时都是单一线程,这个线程称为主线程(main thread)。
对等线程(peer thread)。 read或sleep间隔计时器中断。对等线程。对等线程执行一段时间,将控制传递回主线程。在某些方面,线程执行是不等同于进程的。
线程的上下文切换的开销比进程的小得多,快得多线程不是按照严格的父子层次来组织。 线程池(pool)。 线程池概念的主要影响是对等线程终止。Posix线程 (Pthreads)是在C程序中处理线程的一个标准接口。
Unix系统可用
这是我们第一个线程化的代码,仔细解析。
线程的代码和本地数据被封装在一个线程例程(thread routine)中。
2行代码所示:每个线程例程都以一个通用指针作为输入,并返回一个通用指针。如果想传递多个参数给线程例程
指针。如果想要线程例程返回多个参数。
指针。tid存放对等线程的线程ID。
主线程调用pthread_create函数创建一个新的对等线程(第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函数创建一个新的线程。
arg,在新线程的上下文中运行线程例程f.能用attr参数改变新创建线程的默认属性。
NULL作为attr的参数。pthread_create返回时,参数tid包含新创建线程的ID。
pthread_self函数来获得它自己的线程ID。一个线程是以下列方式之一来终止的
线程例程返回时,线程会隐式地终止。通过调用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函数一样等待任意一个线程的结束。
Stevens在书中指出这是一个设计错误。在任何一个时间点上,线程是可结合的(joinable)或者 是分离的(detached)。
一个可结合的线程能够被其他线程收回其资源或者杀死。
一个分离的线程是不能被其他线程收回其资源或者杀死。
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变量是一个全局或者静态变量,总是被初始化为PTHREAD_ONCE_INIT.当你第一次用参数once_control调用pthread_once时,它调用init_routine。
第二次,第三次以参数once_control调用pthread_once时,啥事也不发生。
当你需要动态初始化多个线程共享的全局变量时,pthread_once函数是很有用的。

注意使用malloc动态给一个connfdp,否则可能两个线程引用同一个connfdp的地址。
竞争为在线程例程中避免存储器泄露,使用分离线程。
主线程中malloc的变量。为了解一个C程序中的一个变量是否共享,有一些基本的问题要解答
基础存储器模型是什么?变量实例是如何映射到存储器的?为了使共享讨论具体化,使用下图的程序作为示例。


示例程序由一个创建两个对等线程的主线程组成。主线程传递一个唯一的ID给每个对等线程,每个对等线程利用这个ID输出一个个性化的信息,以及调用该线程例程的总次数。

线程化的C程序中的变量根据它们的存储类型被映射到虚拟存储器:
全局变量
全局变量是定义在函数之外的变量。 读/写区域包含每个全局变量的一个实例。线程都可以引用。5行声明的ptr。本地自动变量
本地自动变量就是定义在函数内部但是没有static属性的变量。 栈包含它自己的所有本地自动变量的实例。本地静态变量
本地静态变量是定义在函数内部有static属性的变量。 读/写区域。25行的cnt.我们说一个变量v是共享的,当期仅当它的一个实例被一个以上的线程引用。
例如:
cnt 是共享的myid 不是共享的msgs这种本地自动变量也能被共享是很重要的。共享变量十分方便,但是他们也引入了同步错误(synchronization error)的可能性。
考虑下图的程序。



到底哪里出错了呢?这个错误十分隐晦,必须通过研究计数器循环时的汇编代码才能看出。


当badcnt.c中的两个对等线程在一个单处理器上并发执行,机器指令以某种顺序一个接一个地完成。同一个程序每次运行的顺序都可能不同,这些顺序中有一些将会产生正确结果,但是其他的不会。这就是同步错误
关键点: 一般而言,你没有办法预测操作系统是否将为你的线程选择一个正确的顺序。
cnt正确的顺序和错误的顺序(正确结果cnt=2,错误结果cnt=1)
我们可以借助于一种叫做进度图(progress graph)的方法来阐明这些正确和不正确的指令顺序的概念。将在接下来介绍。
进度图(process graph) 将n个并发进程的执行模型化为一条n维笛卡尔空间的轨迹线。

每条轴k对应于k的进度。
每个点(I1,I2,I3,I4...,In)代表线程k(k=1,...,n)已经完成到了Ik这条指令的状态。
图的原点对应于没有任何线程完成这一条指令的初始状态。
进度图将指令执行模型化为从一个状态到另一个状态的转换(transition)。
转换指从一点到相邻一点的有向边。 合法的转换是向各个轴的正半轴走。
对于线程i,操作共享变量cnt内容的指令(Li,Ui,Si)构成了一个(关于共享变量cnt的)临界区(critical section)。(必须确保指令要这样执行)
这个临界区不应该和其他线程的临界区交替执行。(这一段的指令不能交叉)。
我们要确保每个线程在执行它的临界区中的指令时,拥有对共享变量的互斥的访问(mutually exclusive access)。
互斥(mutual exclusion)。在进程图中,两个临界区的交集形式称为不安全区(unsafe region)。
安全轨迹线。 不安全轨迹线。我们必须以某种方式同步线程,使它们总是有一条安全轨迹线
Edsger Dijksta,并发编程领域的先锋任务,提出了一种经典的解决同步不同执行线程问题的方法
这种方法是基于一种叫做信号量(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操作随机会重启这些线程中的一个。s减去1,完成它的P操作。重点,P操作和V操作都是不可分割的,也就是自身确保了是一个带有安全轨迹的操作。(所以又叫原语)
cnt++的操作。加1这个操作中,加载,加一,存储信号量过程是不可分割的。P和V的定义确保了一个正在运行的程序绝不可能进入这样一种状态,也就是不可能有负值。
这个属性叫做信号量不变性(semaphore invariant),为控制并发程序的轨迹线提供了强有力的工具。

信号量提供了一种很方便的方法来确保对共享变量的互斥访问。
基本的思想是
共享变量(或一组相关的共享变量) 与一个信号量s(初始为)`联系起来。P(s)和V(s)操作相应的临界区包围起来。以这种方式保护共享变量的信号量叫做二元信号量(binary semaphore)
以提供互斥为目的的二元信号量常常也称为互斥锁(mutex)。
P操作叫做互斥锁加锁。V操作叫做互斥锁解锁。占用这个互斥锁。一个被用作一组可用资源的计数器的信号量称为计数信号量。

关键思想:
P操作和V操作的结合创建了一组状态,叫做禁止区(forbidden regin),其中s<0 信号量的不变形,不可能有轨迹线进入这个区域禁止区包含了不安全区的任何部分。 正确实现上文中的cnt的线程同步。
第一步:声明一个信号量 mutex
volatile int cnt = 0 ;
sem_t mutex;
第二步:主线程中初始化
Sem_init(&mutex,0,1);
第三步,在线程例程中对共享变量cnt的更新包围P和V操作,从而保护了它们。
for( i = 0 ;i < niters ;i++) {
P(&mutex);
cnt++;
V(&mutex);
}

除了提供互斥外,信号量的另一个重要作用是调度对共享资源的访问。
两个经典而有用的例子。
图给出了生产者消费者问题

生产者线程反复地生成新的项目,并把它们插入到缓冲区中。消费者线程不断地从缓冲区取出这些项目,然后消费使用它们。因为插入和取出项目都涉及更新共享变量
互斥的缓冲区的访问。 我们将开发一个简单的包,叫做SBUF,用来构造生产者-消费者程序。

SBUF操作类型为sbuf_t的有限缓冲区。
项目存放在一个动态分配的n项整数数组(buf)中。front和rear索引值记录该队列的第一项和最后一项。mutex信号量提供互斥的缓冲区访问slots和items信号量分别记录空槽位和可用项目的数量。以下给出SBUF函数的实现:

sbuf_init函数进行初始。 front和rear表示一个空的缓冲区。
sbuf_deinit函数是当应用程序使用完缓冲区时,释放缓冲区存储。sbuf_insert
sbuf_remove
读者-写着问题是互斥问题的一个概括。
一组并发的线程要访问同一个数据对象。
写者读者写者必须拥有对对象的独占访问。
读者可以和无限多个其他读者共享对象。读者-写者问题有几个变种,都是基于读者和写者的优先级
第一类读者-写者问题
读者优先,要求不要让读者等待,除非已经把一个使用权限赋予了一个写者。读者不会因为有一个写者在等待而等待。第二类读者-写者问题(?)
写者优先,要求一但一个写者准备好可以写,它就会尽可能地完成它的写操作。给出第一类读者-写者问题答案。

信号量w控制对访问共享对象的临街区的访问。
读者
w只对第一个读者上锁w对最后一个走的读者解锁写者
w上锁w解锁mutex保护对共享变量readcnt的访问。 readcnt统计当前临界区的读者数量。所有读者-写者答案都有可能导致饥饿

为每个新的客户端创建新的线程,有不少的代价。
一个基于预线程化的服务器利用生产者-消费者模型构造一个更高效率的方式。





主要用于多核CPU的算法。
比如:利用并行来完成n路递归
互斥和生产者-消费者同步的技术,只是并发问题的冰山一角。
同步问题从根本来说是很难的问题。
这章我们以线程为例讨论。
同步问题在任何并发流操作共享资源时都会出现。 信号时,回收进程时的竞争。一个函数被称为线程安全的(thread-safe),当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。否则就是线程不安全的(thread-unsafe)
我们能够定义出四个(不相交)线程不安全函数类:
P,V这样的同步操作来保护共享的变量第 2 类 : 保持跨越多个调用状态的函数
一个伪随机数生成器是这类线程不安全的例子。

因为产生的结果依赖于上一个next的值。
seed无论运行多少次,都是同样的结果。线程不安全的解决方案: 重写
static,而是依靠调用者在参数中传递状态信息。第 3 类 :返回指向静态变量的指针的函数( 有点类似第一类 )
解放方案
指针。加锁-拷贝技术:
用上面的原理写一个线程不安全函数的包装函数来实现线程安全。

第 4 类 : 调用线程不安全函数的函数。
f调用线程不安全函数g。那么f可能不安全。 g是第二类,那么f一定不安全,也没有办法去修正,只能改变g.g是第一,三类,可以用加锁-拷贝技术来解决。
有一类重要的线程安全函数,叫做可重入函数(reentrant function)
其特点在于它们有这样一种属性。
共享数据。被分为两类
隐式可重入
参数可以有指针
是否可重入,同时取决于调用者,和被调用者。
可重入函数比较高效是因为不需要同步操作。
认识到可重入性有时即是调用者也是被调用者的属性。
被调用者的单独属性。大多数Unix函数,包括大部分定义在标准C库的函数(malloc,free,realloc,printf和scanf)都是线程安全的。

asctime,ctime,localtime函数是在不同时间和数据格式相互来回转换时经常使用的函数。
gethostbyname,gethostbyaddr,inet_ntoa函数是经常用的网络编程函数。
strtok函数是一个过时了的同来分析字符串的函数。
Unix系统提供大多数线程不安全函数的可重入版本。
_r后缀结尾。gethostbyname_r。当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点,就会发生竞争。
竞争发生的理由是因为程序员假定某种特殊的轨迹线穿过执行状态空间。例子:

程序十分简单。
主线程创建了四个对等线程,并传递一个指向循环变量i的指针作为线程的ID。并输出。
i一定是四个不同的。所以会想当然觉得会输出四个不同的ID。
对等线程给myid赋值结束后,i才会自增。i++,和对等线程myid=*((int *)vargp)的 竞争。解决方案:用一个临时地址保存i

信号量引入了一种潜在的令人厌恶的运行时错误,叫做死锁 (deadlock)。
进度图对于理解死锁是一个无价的工具。

死锁的区域d是一个只能进,不能出的区域。
禁止区,能进去。禁止区了。如果禁止区不重叠,一定不会发生死锁。
死锁。死锁是一个相当困难的问题,因为它总是不可预测的。
死锁区域。使用二元信号量来实现互斥,可以应用一下有效的规则。
互斥锁加锁顺序规则:如果对于程序中每对互斥锁(s,t),每个占用s和t的线程都按照相同的顺序对它们加锁,那么这个程序就是无死锁的。

GGGGGGGGGGG,暂时告一段落了!!!!!!!!!!!!!!ddd!!
标签:
原文地址:http://blog.csdn.net/zy691357966/article/details/51553092