參考老師提供的教材內容導讀html
本週的內容是要學習以爲最重要的一章,我最終決定選擇第12章——併發編程git
由於對於併發這個概念在計算機學習中在不少方面都要用到。編程
貫徹十二章有幾個概念:安全
上下文調度 /切換服務器
在操做系統中,CPU切換到另外一個進程須要保存當前進程的狀態並恢復另外一個進程的狀態:當前運行任務轉爲就緒(或者掛起、刪除)狀態,另外一個被選定的就緒任務成爲當前任務。上下文切換包括保存當前任務的運行環境,恢復將要運行任務的運行環境。多線程
本次再度學習十二章一是對之前學過的內容查漏補缺二是鞏固重點知識(尤爲是線程的相關知識),所以將不會徹底再描述每個知識點但將結合實例進行學習併發
如下就是這三種構造方法的學習函數
父進程派生子進程爲客戶端提供服務,而父進程本身用來等待下一個鏈接請求。
post
關於進程的echo服務器學習
使用SIGCHLD處理程序來回收僵死子進程的資源。
父進程必須關閉他們各自的connfd拷貝(已鏈接的描述符),避免存儲器泄露。
由於套接字的文件表表項中的引用計數,直到父子進程的connfd都關閉了,到客戶端的鏈接纔會終止。
注意
1.父進程須要關閉它的已鏈接描述符的拷貝(子進程也須要關閉)
2.必需要包括一個SIGCHLD處理程序來回收僵死子進程的資源
3.父子進程之間共享文件表,可是不共享用戶地址空間。
進程的優劣
父、子進程共享狀態信息:共享文件表但不共享用戶空間。
優勢:防止虛擬存儲器被錯誤覆蓋 缺點:開銷高,共享狀態信息才須要IPC機制
基本思想:就是使用select函數,要求內核掛起進程,只有在一個或多個I/O事件發生後,纔將控制返回給應用程序。
Select函數
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval*timeout);
1.返回值
返回已準備好的描述符的非0個數,若出錯則爲-1
2.參數詳解
int maxfdp是一個整數值,是指集合中全部文件描述符的範圍,即全部文件描述符的最大值加1,不能錯!
fd_set * readfds是指向fd_set結構的指針,這個集合中應該包括文件描述符
fd_set * writefds是指向fd_set結構的指針,這個集合中應該包括文件描述符,咱們是要監視這些文件描述符的寫變化的,即咱們關心是否能夠向這些文件中寫入數據了,若是這個集合中有一個文件可寫,select就會返回一個大於0的值,表示有文件可寫,若是沒有可寫的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。能夠傳入NULL值,表示不關心任何文件的寫變化。
fd_set * errorfds同上面兩個參數的意圖,用來監視文件錯誤異常。
struct timeval * timeout是select的超時時間它可使select處於三種狀態:
第一,若將NULL以形參傳入,即不傳入時間結構,就是將select置於阻塞狀態,必定等到監視文件描述符集合中某個文件描述符發生變化爲止; 第二,若將時間值設爲0秒0毫秒,就變成一個純粹的非阻塞函數,無論文件描述符是否有變化,都馬上返回繼續執行,文件無變化返回0,有變化返回一個正值; 第三,timeout的值大於0,這就是等待的超時時間,即select在timeout時間內阻塞,超時時間以內有事件到來就返回了,不然在超時後無論怎樣必定返回,返回值同上述。
#include <sys/types.h> #include <sys/time.h> #include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <unistd.h> int main() { char buffer[128]; int result, nread; fd_set inputs, testfds; struct timeval timeout; FD_ZERO(&inputs);//用select函數以前先把集合清零 FD_SET(0,&inputs);//把要檢測的句柄——標準輸入(0),加入到集合裏。 while(1) { testfds = inputs; timeout.tv_sec = 2; timeout.tv_usec = 500000; result = select(FD_SETSIZE, &testfds, (fd_set *)0, (fd_set *)0, &timeout); switch(result) { case 0: printf("timeout/n"); break; case -1: perror("select"); exit(1); default: if(FD_ISSET(0,&testfds)) { ioctl(0,FIONREAD,&nread);//取得從鍵盤輸入字符的個數,包括回車。 if(nread == 0) { printf("keyboard done/n"); exit(0); } nread = read(0,buffer,nread); buffer[nread] = 0; printf("read %d from keyboard: %s", nread, buffer); } break; } } return 0; }
這一種方法是基於前兩中的複合
每一個線程開始都是由單一線程開始的,稱爲主線程;在某一個時刻主線程建立一個對等線程,開始進場併發的運行,他們之間經過上下文切換
int pthread_create(pthread_t *tid,pthread_attr_t *attr,func *f,void *arg); 成功則返回0,出錯則爲非零
函數參數:
函數返回時,參數tid包含新建立的線程的ID,新線程能夠經過調用pthread_self函數來得到本身的線程ID。
pthread_t pthread_self(void);
返回調用者的線程ID。
#include <pthread.h> pthread_t ntid; void printids(const char *s) { pid_t pid; pthread_t tid; pid = getpid(); tid = pthread_self(); printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid, (unsigned long)tid, (unsigned long)tid); } void* thr_fn(void *arg) { printids("new thread:"); return((void*)0); } int main() { int err; err = pthread_create(&ntid, NULL, thr_fn, NULL); if(err!=0) { printf("can't create thread\n"); exit(1); } printids("main thread:"); sleep(2); exit(0); }
PS:有關線程相關代碼編譯時要加上 -lpthread,是由於鏈接時須要使用庫libpthread.a不然出錯
一個線程是經過如下方式之一來終止的。
當頂層的線程例程返回時,線程會隱式地終止。
經過調用pthread_exit
函數,線程會顯式地終止。他會等待其餘對等線程終止而後再終止主線程
void pthread_exit(void *thread_return);
int pthread_join(pthread_t tid,void **thread_return); 成功則返回0,出錯則爲非零
#include <stdio.h> #include <pthread.h> /*線程一*/ void thread_1(void) { int i=0; for(i=0;i<=6;i++) { printf("This is a pthread_1.\n"); if(i==2) pthread_exit(0); //用pthread_exit()來調用線程的返回值,用來退出線程,可是退出線程所佔用的資源不會隨着線程的終止而獲得釋放 sleep(1); } } /*線程二*/ void thread_2(void) { int i; for(i=0;i<3;i++) printf("This is a pthread_2.\n"); pthread_exit(0); //用pthread_exit()來調用線程的返回值,用來退出線程,可是退出線程所佔用的資源不會隨着線程的終止而獲得釋放 } int main(void) { pthread_t id_1,id_2; int i,ret; /*建立線程一*/ ret=pthread_create(&id_1,NULL,(void *) thread_1,NULL); if(ret!=0) { printf("Create pthread error!\n"); return -1; } /*建立線程二*/ ret=pthread_create(&id_2,NULL,(void *) thread_2,NULL); if(ret!=0) { printf("Create pthread error!\n"); return -1; } /*等待線程結束*/ pthread_join(id_1,NULL); pthread_join(id_2,NULL); return 0; }
int pthread_deacth(pthread_t tid); 成功則返回0,出錯則爲非零
#include <pthread.h> #include <stdio.h> void *doit(void *arg){ printf("arg...\n"); sleep(1); return NULL; } int main(void){ pthread_t tid; pthread_create(&tid,NULL,doit,NULL); pthread_detach(tid); sleep(1); return 0; }
pthread_once_t once_control=PTHREAD_ONCE_INIT; int pthread_once(pthread_once_t *once_contro, void (*init_routine)(void)); 老是返回0
轉換規則:
信號量與操做系統中的沒有區別
1.P(s):若是s是非零的,那麼P將s減一,而且當即返回。若是s爲零,那麼就掛起這個線程,直到s變爲非零。
2.V(s):將s加一,若是有任何線程阻塞在P操做等待s變爲非零,那麼V操做會重啓線程中的一個,而後該線程將s減一,完成P操做。
int sem_init(sem_t *sem,0,unsigned int value);//將信號量初始化爲value
int sem_wait(sem_t *s);//P(s)
int sem_post(sem_t *s);//V(s)
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <semaphore.h> void *thread_function(void *arg); sem_t bin_sem; #define WORK_SIZE 1024 char work_area[WORK_SIZE]; /* 用來存放輸入內容 */ int main() { int res; /* 暫存一些命令的返回結果 */ pthread_t a_thread; /* 織帶新建的線程 */ void *thread_result; /* 存放線程處理結果 */ res = sem_init(&bin_sem, 0, 0); /* 初始化信號量,而且設置初始值爲0*/ if (res != 0) { perror("Semaphore initialization failed"); exit(EXIT_FAILURE); } res = pthread_create(&a_thread, NULL, thread_function, NULL); /* 建立新線程 */ if (res != 0) { perror("Thread creation failed"); exit(EXIT_FAILURE); } printf("Inout some text, Enter 'end' to finish\n"); while(strncmp("end", work_area, 3) != 0) { /* 當工做區內不是以end開頭的字符串時...*/ fgets(work_area, WORK_SIZE, stdin); /* 從標準輸入獲取輸入到worl_area */ sem_post(&bin_sem); /* 信號量+1 */ } printf("\nWaiting for thread to finish...\n"); res = pthread_join(a_thread, &thread_result); /* 等待線程結束 */ if (res != 0) { perror("Thread join failed"); exit(EXIT_FAILURE); } printf("Thread joined\n"); sem_destroy(&bin_sem); /* 銷燬信號量 */ exit(EXIT_SUCCESS); } void *thread_function(void *arg) { sem_wait(&bin_sem); /* 等待信號量有大於0的值而後-1 */ while(strncmp("end", work_area, 3) != 0) { printf("You input %ld characters\n", strlen(work_area)-1); /* 獲取輸入字符串長度 8*/ sem_wait(&bin_sem); /* 等待信號量有大於0的值而後-1 */ } pthread_exit(NULL); }
獲取輸入的字符串長度,wait信號量有大於0的值而後-1
用信號量實現互斥在調度共享資源的時候尤爲重要
二元信號量(互斥鎖):將每一個共享變量與一個信號量s聯繫起來,而後用P(s)(加鎖)和V(s)(解鎖)操做將相應的臨界區包圍起來。
禁止區:s<0,由於信號量的不變性,沒有實際可行的軌跡線可以直接接觸不安全區的部分
互斥實例
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <semaphore.h> void *thread_function(void *arg); pthread_mutex_t work_mutex; #define WORK_SIZE 1024 char work_area[WORK_SIZE]; int time_to_exit = 0; /* 用來控制是否退出*/ int main() { int res; pthread_t a_thread; void *thread_result; res = pthread_mutex_init(&work_mutex,NULL); /* 初始化一個互斥鎖 */ if (res != 0) { perror("Mutex initialization failed"); exit(EXIT_FAILURE); } res = pthread_create(&a_thread, NULL, thread_function, NULL); /* 建立一個新線程 */ if (res != 0) { perror("Thread creation failed"); exit(EXIT_FAILURE); } pthread_mutex_lock(&work_mutex); /* 嘗試對互斥量加鎖 */ printf("Input some text, Enter 'end' to finish\n"); while(!time_to_exit) { /* 檢查是否是該退出*/ fgets(work_area, WORK_SIZE, stdin); /* 從標準輸入獲取輸入到work_area */ pthread_mutex_unlock(&work_mutex); /* 解鎖互斥量 */ while(1) { pthread_mutex_lock(&work_mutex); if (work_area[0] != '\0') { /* 持續檢查work_area 是否爲空, 若是不爲空繼續等待,若是爲空,則從新讀取輸入到work_area*/ pthread_mutex_unlock(&work_mutex); sleep(1); } else { break; } } } pthread_mutex_unlock(&work_mutex); printf("\nWaiting for thread to finish...\n"); res = pthread_join(a_thread, &thread_result); if (res != 0) { perror("Thread join failed"); exit(EXIT_FAILURE); } printf("Thread joined\n"); pthread_mutex_destroy(&work_mutex); exit(EXIT_SUCCESS); } void *thread_function(void *arg) { sleep(1); pthread_mutex_lock(&work_mutex); /* 嘗試加鎖互斥量 */ while(strncmp("end", work_area, 3) != 0) { /* 當work_area裏的值不是以end開頭時*/ printf("You input %ld characters\n", strlen(work_area) -1); /* 輸出輸入的字符長度 */ work_area[0] = '\0'; /* work_area設置爲空 */ pthread_mutex_unlock(&work_mutex); sleep(1); pthread_mutex_lock(&work_mutex); while (work_area[0] == '\0') { /* 持續檢查work_area 直到它裏面有輸入值*/ pthread_mutex_unlock(&work_mutex); sleep(1); pthread_mutex_lock(&work_mutex); } } time_to_exit = 1; /* 當輸入end後,設置退出標誌 */ work_area[0] = '\0'; pthread_mutex_unlock(&work_mutex); pthread_exit(0); }
參考互斥鎖
互斥鎖的基本流程爲:
初始化一個互斥鎖:pthread_mutex_init()函數 加鎖:pthread_mutex_lock()函數或者pthread_mutex_trylock()函數
對共享資源的操做
解鎖:pthread_mutex_unlock()函數 註銷互斥鎖:pthread_mutex_destory()函數
當使用pthread_mutex_lock()函數進行加鎖時,若此時已經被鎖,則嘗試加鎖的線程會被阻塞,直到互斥鎖被其餘線程釋放,當pthread_mutex_lock()函數有返回值時,說明加鎖成功;
解鎖的兩個條件:
解鎖前,互斥鎖必須處於鎖定狀態;
必須由加鎖的線程進行解鎖。
感想:我的感受互斥鎖與操做系統中提到的門很像,加鎖就是關門,當達到加鎖條件時關門,只有等門裏邊的執行完,才能解鎖。雖然互斥鎖叫互斥鎖,可是他是用來解決同步問題的,使得執行起來更有條理,不會亂。
新感悟:再參考了一下網上的資料進行了學習,相比當時在操做系統上的理論學習和書本上關於同步互斥的講解又有了新的理解,不少網上的資料都說要先同步再互斥,但我感受這樣並非必須的,若是生產者中先同步再互斥,而消費者先互斥再同 步,或反之,都不會出現死鎖,由於它們間並無交叉關係。可是先同步,再互斥能夠獲得更 好的併發性。
PS:如下兩句順序不要變
sem_wait(&room_sem); pthread_mutex_lock(&mutex);
#include #include #include #include #include #define N 2 // 消費者或者生產者的數目 #define M 10 // 緩衝數目 int in = 0; // 生產者放置產品的位置 int out = 0; // 消費者取產品的位置 int buff[M] = {0}; // 緩衝初始化爲0, 開始時沒有產品 sem_t empty_sem; // 同步信號量, 當滿了時阻止生產者放產品 sem_t full_sem; // 同步信號量, 當沒產品時阻止消費者消費 pthread_mutex_t mutex; // 互斥信號量, 一次只有一個線程訪問緩衝 int product_id = 0; //生產者id int prochase_id = 0; //消費者id /* 打印緩衝狀況 */ void print() { int i; for(i = 0; i < M; i++) printf("%d ", buff[i]); printf("\n"); } /* 生產者方法 */ void *product() { int id = ++product_id; while(1) { // 用sleep的數量能夠調節生產和消費的速度,便於觀察 sleep(1); //sleep(1); sem_wait(&empty_sem); pthread_mutex_lock(&mutex); in = in % M; printf("product%d in %d. like: \t", id, in); buff[in] = 1; print(); ++in; pthread_mutex_unlock(&mutex); sem_post(&full_sem); } } /* 消費者方法 */ void *prochase() { int id = ++prochase_id; while(1) { // 用sleep的數量能夠調節生產和消費的速度,便於觀察 sleep(1); //sleep(1); sem_wait(&full_sem); pthread_mutex_lock(&mutex); out = out % M; printf("prochase%d in %d. like: \t", id, out); buff[out] = 0; print(); ++out; pthread_mutex_unlock(&mutex); sem_post(&empty_sem); } } int main() { pthread_t id1[N]; pthread_t id2[N]; int i; int ret[N]; // 初始化同步信號量 int ini1 = sem_init(&empty_sem, 0, M); int ini2 = sem_init(&full_sem, 0, 0); if(ini1 && ini2 != 0) { printf("sem init failed \n"); exit(1); } //初始化互斥信號量 int ini3 = pthread_mutex_init(&mutex, NULL); if(ini3 != 0) { printf("mutex init failed \n"); exit(1); } // 建立N個生產者線程 for(i = 0; i < N; i++) { ret[i] = pthread_create(&id1[i], NULL, product, (void *)(&i)); if(ret[i] != 0) { printf("product%d creation failed \n", i); exit(1); } } //建立N個消費者線程 for(i = 0; i < N; i++) { ret[i] = pthread_create(&id2[i], NULL, prochase, NULL); if(ret[i] != 0) { printf("prochase%d creation failed \n", i); exit(1); } } //銷燬線程 for(i = 0; i < N; i++) { pthread_join(id1[i],NULL); pthread_join(id2[i],NULL); } exit(0); }
當新的寫者但願寫時,不容許該寫者後續的讀者訪問數據區,但必須保證以前的讀者讀完。
1) 有寫者正在寫或者等待寫,須等到沒有寫者才能讀
2) 沒有寫者,能夠讀
3) 寫者與寫者互斥。當其它寫者正在寫時,其它寫者不能寫。
4) 寫者與讀者互斥。以前只有讀者在讀,當寫者出現時,必須等到以前的讀者都讀完才能寫。這尊重了以前讀者的意願。
5) 寫者能夠有條件地插讀者的隊。當前有寫者正寫,有讀者在等,這時來了新寫者,新寫者能夠在那些讀者以前執行。
代碼:
#include "stdio.h" #include <stdlib.h> #include <pthread.h> #define N_WRITER 2 //寫者數目 #define N_READER 20 //讀者數目 #define W_SLEEP 1 //控制寫頻率 #define R_SLEEP 0.5 //控制讀頻率 pthread_t wid[N_WRITER],rid[N_READER]; const int MAX_RAND = 1000;//產生的最大隨機數 int data = 0; int readerCnt = 0, writerCnt = 0; pthread_mutex_t accessReaderCnt = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t accessWriterCnt = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t writeLock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t readerLock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t outerLock = PTHREAD_MUTEX_INITIALIZER; void write() { int rd = rand()%MAX_RAND; printf("write %d\n",rd); data = rd; } void read() { printf("read %d\n",data); } void * writer(void * in) { while(1) { pthread_mutex_lock(&accessWriterCnt); {//臨界區,但願修改 writerCnt,獨佔 writerCnt writerCnt++; if(writerCnt == 1){ //阻止後續的讀者加入待讀隊列 pthread_mutex_lock(&readerLock); } } pthread_mutex_unlock(&accessWriterCnt); pthread_mutex_lock(&writeLock); {//臨界區,限制只有一個寫者修改數據 write(); } pthread_mutex_unlock(&writeLock); pthread_mutex_lock(&accessWriterCnt); {//臨界區,但願修改 writerCnt,獨佔 writerCnt writerCnt--; if(writerCnt == 0){ //阻止後續的讀者加入待讀隊列 pthread_mutex_unlock(&readerLock); } } pthread_mutex_unlock(&accessWriterCnt); sleep(W_SLEEP); } pthread_exit((void *) 0); } void * reader (void * in) { while(1) { //假如寫者鎖定了readerLock,那麼成千上萬的讀者被鎖在這裏 pthread_mutex_lock(&outerLock); {//臨界區 pthread_mutex_lock(&readerLock);//只被一個讀者佔有 {//臨界區 pthread_mutex_lock(&accessReaderCnt);//代碼段 1 {//臨界區 readerCnt++; if(readerCnt == 1){ pthread_mutex_lock(&writeLock); } } pthread_mutex_unlock(&accessReaderCnt); } pthread_mutex_unlock(&readerLock);//釋放時,寫者將優先得到readerLock } pthread_mutex_unlock(&outerLock); read(); pthread_mutex_lock(&accessReaderCnt);//代碼段2 {//臨界區 readerCnt--; if(readerCnt == 0){ pthread_mutex_unlock(&writeLock);//在最後一個併發讀者讀完這裏開始禁止寫者執行寫操做 } } pthread_mutex_unlock(&accessReaderCnt); sleep(R_SLEEP); } pthread_exit((void *) 0); } int main() { int i = 0; for(i = 0; i < N_READER; i++) { pthread_create(&rid[i],NULL,reader,NULL); } for(i = 0; i < N_WRITER; i++) { pthread_create(&wid[i],NULL,writer,NULL); } while(1){ sleep(10); } return 0; }
課本練習題12-1解決:父進程派生子進程時,相關文件表中的引用計數從1增長到2。父進程一關閉描述符副本,引用計數就由2減小到1,由於內核不會關閉一個文件,直到文件表中的引用計數值變爲0,因此子進程這邊的鏈接端將保持打開。
課本練習題12.5:在下圖1中基於進程的服務器中,咱們在兩個位置當心地關閉了已鏈接描述符:父進程和子進程。然而,在圖2中,基於線程的服務器中,咱們只在一個位置關閉了已鏈接描述符:對等線程,爲何?
課本練習題12-5解決:由於線程運行在同一個進程中,他們共享同一個描述符表,因此在描述符表中的引用計數與線程的多少是沒有關係的都爲1,所以只須要一個close就夠了。
課本練習題12.10下圖所示的對第一類讀者-寫者問題的解答給予讀者較高的優先級,可是從某種意義上說,這種優先級是很弱的,由於一個離開臨界區的寫者可能重啓一個在等待的寫者,而不是一個在等待的讀者。描述一個場景,其中這種弱優先級會致使一羣寫者使得一個讀者飢餓。
其餘問題加在上文中
在上文中
結對照片
結對學習內容
本週再次學習了多線程、進程和併發的相關內容,雖然當時在操做系統也學習了,本身也學習了,可是此次再學的時候仍是發現問題多多,在不少的理解方面都不細緻,和結對對象討論也會發現一些新的思路,並且感受雖然在JAVA上也對相應內容進行了學習,可是在多線程多進程方面老是理不清的感受,此次一塊兒邊實踐邊回顧,感受此次學下來,效果更好,收穫仍是蠻多的。
代碼行數(新增/累積) | 博客量(新增/累積) | 學習時間(新增/累積) | 重要成長 | |
---|---|---|---|---|
目標 | 5000行 | 30篇 | 200小時 | |
第一週 | 5/5 | 1/1 | 15/15 | |
第二週 | 1/2 | 23/38 | ||
第三週 | 206/327 | 1/3 | 28/66 | |
第四周 | 206/327 | 1/4 | 10/77 | |
第五週 | 285/568 | 1/5 | 20/97 | 主要學習了彙編及反彙編的相關知識 |
第六週 | 160/683 | 3/8 | 20/107 | |
第七週 | / | 2/10 | 20/127 | 第四章學習內容和第二次實驗 |
第八週 | 2/12 | 22/149 | 第十一章、第十二章 | |
第九周 | 408/ 2582 | 3/15 | 21/170 | 第六章內容、第三次實驗、課後pwd的實現 |
第十一週 | 174 / 3035 | 3/18 | 20/200 | 第九章內容、實驗四 |
第十三週 | 741/3776 | 2/22 | 20/220 | 第十二章再學習 |
嘗試一下記錄「計劃學習時間」和「實際學習時間」,到期末看看能不能改進本身的計劃能力。這個工做學習中很重要,也頗有用。
耗時估計的公式
:Y=X+X/N ,Y=X-X/N,訓練次數多了,X、Y就接近了。
計劃學習時間:20小時
實際學習時間:20小時
改進狀況:本週的學習時間用在課本第四章的學習和第二次實驗
(有空多看看現代軟件工程 課件
軟件工程師能力自我評價表)
...