三种基本的构造并发程序的方法:html
1)进程。每一个逻辑控制流都是一个进程,由内核来调度和维护git
2)I/O多路复用程序员
3)线程编程
基于I/O多路复用的并发编程:安全
面对困境——服务器必须响应两个互相独立的I/O事件:服务器
1)网络客户端发起的链接请求网络
2)用户在键盘上键入的命令 ,解决的办法是I/O多路复用技术。select函数:
数据结构
select函数处理类型为fd_set的集合,即描述符集合,并在逻辑上描述为一个大小为n的位向量,每一位b[k]对应描述符k,但当且仅当b[k]=1,描述符k才代表是描述符集合的一个元素。多线程
使用select函数的过程以下:并发
第一步,初始化fd_set集,19~22行;
第二步,调用select,25行;
第三步,根据fd_set集合如今的值,判断是哪一种I/O事件,26~31行。
基于I/O多路复用的并发事件驱动服务器:I/O多路复用能够用作并发事件驱动程序的基础,在事件驱动程序中,流是由于某种事件而前进的,通常概念是把逻辑流模型化为状态机。一个状态机就是一组状态、输入事件和转移。
并发事件驱动程序中echo服务器中逻辑流的状态机,以下图所示:
线程执行模型
建立线程,调用pthread_create函数来建立其余线程,pthread_create函数建立一个新的线程,带着一个输入变量arg,在新线程的上下文运行线程例程f。
attr默认为NULL
# include <pthread.h> typedef void *(func)(void *); int pthread_create(pthread_t *tid, pthread_attr_t *attr, func *f, void *arg); 返回:成功返回0,出错返回非0
#include <pthread.h> pthread_t pthread_self(void); 返回调用者的线程ID(TID)
1)当顶层的线程例程返回时,线程会隐式终止;
2)线程调用pthread_exit函数,线程会显示终止;若是主线程调用pthread_exit,它会等到全部其余对等线程终止,而后再终止主线程和整个线程,返回值为thread_return;
pthread_exit函数
#include <pthread.h> void pthread_exit(void *thread_return); 若成功返回0,出错为非0
3)某个对等线程调用exut函数,则函数终止进程和全部与该进程相关的线程;
4)另外一个对等线程调用以当前ID为参数的函数ptherad_cancel来终止当前线程
#include <pthread.h> void pthread_cancle(pthread_t tid); 若成功返回0,出错为非0
这个函数会阻塞,直到线程tid终止,将线程例程返回的(void*)指针赋值为thread_return指向的位置,而后回收已终止线程占用的全部存储器资源
借助进度图
1.进度图是将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,原点对应于没有任何线程完成一条指令的初始状态。
当n=2时,状态比较简单,是比较熟悉的二维坐标图,横纵坐标各表明一个线程,而转换被表示为有向边
2.进度图将指令执行模型化为从一种状态到另外一种状态的转换(transition)。转换被表示为一条从一点到相邻点的有向边。合法的转换是向右(线程 1 中的一条指令完成〉或者向上(线程 2 中的一条指令完成)的。两条指令不能在同一时刻完成一一对角线转换是不容许的。程序决不会反向运行,因此向下或者向左移动的转换也是不合法的。
一个程序的执行历史被模型化为状态空间中的一条轨迹线。
临界区:对于线程i,操做共享变量cnt内容的指令L,U,S构成了一个关于共享变量cnt的临界区。
不安全区:两个临界区的交集造成的状态
安全轨迹线:绕开不安全区的轨迹线,绕开不安全区的轨迹线叫作安全轨迹线 (safe trajectory)。相反,接触到任何不安全区的轨迹线就叫作不安全轨迹线 (unsafe trajectory)。
一种解决同步不一样执行线程问题的方法,这种方法是基于一种叫作信号量 (semaphore) 的特殊类型变量的。信号量 s 是具备非负整数值的全 局变量,只能由两种特殊的操做来处理,这两种操做称为 P 和 V:
经过调用 sem_wait 和 sem_post 函数来执行P和V操做。
1)P(s):若是s是非零的,那么P将s减1,而且当即返回。若是s为零,那么就挂起这个线程,直到s变为非零,而一个V操做会重启这个线程。在重启以后,P 操做将s减1,并将控制返回给调用者。
2)V(s):V操做将s加1。若是有任何线程阻塞在P 操做等待s变成非零,那么V操做会重启这些线程中的一个,而后该线程将s减1,完成它的P操做。
信号量的函数
P和V的包装函数
使用信号量来实现互斥
基本思想:将每一个共享变量(或者一组相关的共享变量)与一个信号量s(初始为1)联系起来,而后用P和V操做将相应的临界区包围起来。
利用信号量来调度共享资源,信号量有两个做用:
1)实现互斥
2)调度共享资源
使用线程提升并行性
顺序、并发和并行程序集合之间的关系
并行程序的加速比 一般定义为,其中p 是处理器核的数量,凡是在 k个核上的运行时间。这个公式有时称为强扩展 (strong scaling)。当 T1 是程序顺序执行版本的执行时间时, Sp 称为绝对加速比.(absolute speedup)。当 T1 是程序并行版本在一个核上的执行时间时, Sp 称为相对加速比 (relative speedup)。绝对加速 比比相对加速比能更真实地衡量并行的好处。
效率被定义如图所示,它一般表示为范围在 (0, 100] 之间的百分比。效率是对因为并行化形成的开销的衡量。具备高 效率的程序比效率低的程序在有用的工做上花费更多的时间,在同步和通讯上花费更少的时间。
可重入函数,其特色在于它们具备这样一种属性:当它们被多个线程调用时,不会引用任何共享数据。
可重入函数一般要比不可重人的线程安全的函数高效一些,由于它们不须要同步操做。更进一步来讲,将第2类线程不安全函数转化为线程安全函数的惟一方法就是重写它,使之变为可重入的。
可重入函数分为两类:
1)显式可重入的:全部函数参数都是传值传递,没有指针,而且全部的数据引用都是本地的自动栈变量,没有引用静态或全剧变量。
2)隐式可重入的:调用线程当心的传递指向非共享数据的指针。
消除竞争的方法:动态的为每一个整数ID分配一个独立的块,而且传递给线程例程一个指向这个块的指针。
12.1 在图12-5中,并发服务器的第33行上,父进程关闭了已链接描述符后,子进程仍可以使用该描述符和客户端通讯,为何。
12.2 若是咱们要删除图12-5中关闭已链接描述符的第30行,从没有内存泄漏的角度来讲,代码将仍然是正确的,为何?
12.3 在Linux系统里,在标准输入上键入Ctrl+D表示EOF。图12-6中的程序阻塞在对select的调用上,若是你键入Ctrl+D会发生什么
12.4 图12-8所示的服务器中,咱们在每次调用select以前都当即当心地从新初始化pool.ready_set变量,为何?
12.5 在图12-5中基于进程的服务器中,咱们在两个位置当心地关闭了已链接描述符:父进程和子进程。然而,在图12-14中,基于线程的服务器中,咱们只在一个位置关闭了已链接描述符:对等线程,为何?
答:由于线程运行在同一个进程中,它们都共享相同的描述符表。不管有多少线程使用这个已链接描述符,这个已链接描述符的文件表的引用计数都等于1.所以,当咱们用完它时,一个close操做就足以释放于这个已链接描述符相关的内存资源了。
12.6
A.利用12.4节中的分析,为图12-15中的示例程序在下表的每一个条目中填写“是”或者“否”。在第一列中,符号v,t表示变量v的一个实例,它驻留在线程t的本地栈中,其中t要么是m(主线程),要么是p0(对等线程)或者p1(对等线程1)
答:
B.根据A部分的分析,变量ptr、cnt、i、msgs和myid那些是共享的
答:变量ptr、cnt、msgs被多于一个线程引用,因此它们是共享的。
12.7 根据badcnt.c的指令顺序完成下表,这种顺序会产生一个正确的值吗?
答:
12.8 使用图12-21中的进度图,将下列轨迹线划分为安全的或者不安全的
A.H1,L1,U1,S1,H2,L2,U2,S2,T2,T1
B.H2,L2,H1,L1,U1,S1,T1,U2,S2,T2
C.H1,H2,L2,U2,S2,L1,U1,S1,T1,T2
答:
A.H1,L1,U1,S1,H2,L2,U2,S2,T2,T1是安全的
B.H2,L2,H1,L1,U1,S1,T1,U2,S2,T2是不安全的
C.H1,H2,L2,U2,S2,L1,U1,S1,T1,T2是安全的12.9 设p表示生产者数量,c表示消费者数量,而n表示以项目单元为单位的缓冲区大小。对于下面的每一个场景,指出sbuf_insert和sbuf_remove中的互斥锁信号量是不是必需的。
A.p=1,c=1,n>1
B.p=1,c=1,n=1
C.p>1,c>1,n=1
答:
A.互斥锁是须要的,由于生产者和消费者会并发地访问缓冲区
B.不须要互斥锁,由于一个非空的缓冲区就等于满的缓冲区。当缓冲区包含一个项目时,生产者就别阻塞了,当缓冲区为空时,消费者就被阻塞了。因此在任意时刻,只有一个线程能够访问缓冲区,所以不用互斥锁也能保证互斥
C.不须要互斥锁,缘由与B相同。12.10 图12-26所示的对第一类读者-写者问题的解答给予读者较高的优先级,可是从某种意义上说,这种优先级是很弱的,由于一个离开临界区的写者可能重启一个在等待的写者,而不是一个在等待的读者。描述一个场景,其中这种弱优先级会致使一群写者使得一个读者饥饿。
12.11 对于下表中的并行程序,填写空白处。假设使用强扩展。
答:
12.12 图12-38中的ctime_ts函数是线程安全的,但不是可重入的,请解释说明。
12.13 在图12-43中,咱们可能想要在主线程中的第14行后当即释放已分配的内存块,而不是在对等线程中释放它。可是这会是个坏主意,为何?
12.14
A.在图12-43中,咱们经过为每一个整数ID分配一个独立的块来消除竞争。给出一个不调用malloc或者free函数的不一样的方法。
答:另外一种方法是直接传递整数i,而不是传递一个指向i的指针:
for(i=0;i<N;i++) Pthread_create(&tid[i],NULL,thread,(void*)i);
在线程例程中,咱们将参数强制转换成一个int型变量,并将它赋值给myid;
int myid=(int)vargp;
B.这种方法的利弊是什么?
答:优势是它经过消除对malloc和free的调用下降了开销。一个明显的缺点是,它假设指针至少和int同样大。即使这种假设对于全部得现代系统来讲都为真,可是它对于那些过去遗留下来的或从此的系统来讲可能就不为真了。
12.15 思考下面的程序,它试图使用一对信号量来实现互斥。
A.画出这个程序的进度图。
答:
B.它老是会死锁吗
答:会,由于任何可行的轨迹最终都陷入思索状态。
C.若是是,那么对初始信号量的值作哪些简单的改变就能消除这种潜在的死锁呢?
答:为了消除潜在的死锁,将二元信号量t初始化为1而不是0。
D.画出获得的无死锁程序的进度图。
答:
#include<stdio.h> #include<stdlib.h> #include<fcntl.h> #include<unistd.h> #include<pthread.h> void *thread(void *vargp); int main(int argc, char **argv) { pthread_t tid; int i,n; n = atoi(argv[1]); for(i=0;i<n;i++) { pthread_create(&tid,NULL,thread,NULL); pthread_join(tid,NULL); } exit(0); } void *thread(void *vargp) { printf("hello world!\n"); return NULL; }
#include<stdio.h> #include<stdlib.h> #include<fcntl.h> #include<unistd.h> #include<pthread.h> void *thread(void *vargp); int main(int argc, char **argv) { pthread_t tid; int i,n; pthread_create(&tid,NULL,thread,NULL); exit(0); } void *thread(void *vargp) { sleep(1); printf("hello world!\n"); return NULL; }
答:A.是由于exit(0),使得线程在主线程中结束了,因此没有打印出字符。
B.使用pthread_join就能够解决这个问题
12.18 用进度图说明下面的轨迹分类是否安全:
A.H2,L2,U2,H1,L1,S2,U1,S1,T1,T2
B.H2,H1,L1,U1,S1,L2,T1,U2,S2,T2
C.H1,L1,H2,L2,U2,S2,U1,S1,T1,T2
答:结果:A不安全,B安全,C安全
int readcnt; sem_t mutex=1,w=1,z=1; void reader(void) { while(1) { P(&mutex); readcnt++; if(readcnt==1) P(&w); V(&mutex); P(&mutex); readcnt--; if(readcnt==0) V(&w); V(&mutex); } } void writer(void) { while(1) { P(&z); P(&w); V(&w); V(&z); } }
12.22 检查对select函数的理解,修改下图所示服务器使得它在主服务器每次迭代中至多只会送一个文本:
答:在while循环中增长一个FD_AERO(&ready_set);
就能够了。
12.24 RIO I/O包中的函数都是线程安全的,那么他们都是是可重入的吗?
答:是不可重入的,RIO包中有专门的数据结构为每个文件描述符都分配了相应的独立的读缓冲区,它提供了与系统I/O相似的函数接口,在读取操做时,RIO包加入了读缓冲区,必定程度上增长了程序的读取效率。另外,带缓冲的输入函数是线程安全的,这与Stevens的 UNP 3rd Edition(中文版) P74 中介绍的那个输入函数不一样。UNP的那个版本的带缓冲的输入函数的缓冲区是以静态全局变量存在,因此对于多线程来讲是不可重入的。
12.29 下面的程序会死锁吗?为何?
初始时:a=1;b=1;c=1
线程1 : 线程2:
p(a); p(c);
p(b); p(b);
v(b); v(b);
p(c); v(c);
v(c);
v(a);
答: 会死锁,由于线程1和2在执行完第一步以后都被挂起,都得不到须要的资源。
(一个模板:我看了这一段文字 (引用文字),有这个问题 (提出问题)。 我查了资料,有这些说法(引用说法),根据个人实践,我获得这些经验(描述本身的经验)。 可是我仍是不太懂,个人困惑是(说明困惑)。【或者】我反对做者的观点(提出做者的观点,本身的观点,以及理由)。 )
知足下列条件的函数多数是不可重入的:
(1)函数体内使用了静态的数据结构;
(2)函数体内调用了malloc()或者free()函数;
(3)函数体内调用了标准I/O函数。
详细能够参考连接提供的内容。
(statistics.sh脚本的运行结果截图)
- 20155301](https://home.cnblogs.com/u/fengxingck/) - 结对照片 - 结对学习内容 - 共同窗习了第12章
xxx
xxx
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 200/200 | 2/2 | 20/20 | |
第二周 | 300/500 | 2/4 | 18/38 | |
第三周 | 500/1000 | 3/7 | 22/60 | |
第四周 | 300/1300 | 2/9 | 30/90 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进本身的计划能力。这个工做学习中很重要,也颇有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。
计划学习时间:XX小时
实际学习时间:XX小时
改进状况:
(有空多看看现代软件工程 课件
软件工程师能力自我评价表)