三種基本的構造併發程序的方法: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小時
改進狀況:
(有空多看看現代軟件工程 課件
軟件工程師能力自我評價表)