【笔记整理 - 多线程编程】
線程
編譯多線程程序時,要注意參數
g++ -g 源文件.cpp -o 目標文件 -lpthread
[root@localhost coding]# g++ -g test.cpp -o test -lpthread或者
g++ -g -o 目標文件 源文件.cpp -lpthread
[root@localhost coding]# g++ -g -o test test.cpp -lpthread關于線程的注意事項(經常忘)
1、雖然能按順序創建線程,但實際上的線程運行順序是未知的。
2、主線程一退出,所有子線程都會退出。主線程要給子線程的運行留下時間。
void* pth_foo(void* arg) {int i = (long)arg;printf("thread value : %d \n", i); }int main() {pthread_t pid[5];for(long i = 0; i < 5; ++i){if(pthread_create(&pid[i], NULL, pth_foo, (void*)(i + 1)) != 0)return -1;} }因為主線程創建完子線程后就立即退出,并導致程序結束,使得子線程完全沒來得及執行。
解決方法一:
讓主線程休眠一段時間。
sleep(1);解決方法二:
主線程調用pthread_exit函數退出,等到所有線程都執行完畢后,進程才結束。
pthread_exit(NULL); [root@localhost coding]# g++ -g test.cpp -o test -lpthread [root@localhost coding]# ./test thread value : 2 thread value : 3 thread value : 4 thread value : 5 thread value : 1在多線程中如果要輸出信息,最好還是用printf,如果使用cout,會出現多個線程的輸出混雜在一起的情況。
cout是個全局變量,多線程在使用共享資源時沒有上鎖,所以導致輸出混合的情況吧。
創建線程 pthread_create()
#include <pthread.h> int pthread_create(pthread_t*thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void* arg);thread參數是新線程的標識符,為一個整型。
attr參數用于設置新線程的屬性。給傳遞NULL表示設置為默認線程屬性。
start_routine和arg參數分別指定新線程將運行的函數和參數。start_routine返回時,這個線程就退出了
返回值:成功返回0,失敗返回錯誤號。
獲取當前線程id pthread_self()
線程id的類型是thread_t,它只在當前進程中保證是唯一的,在不同的系統中thread_t這個類型有不同的實現,調用pthread_self()可以獲得當前線程的id。
終止線程 pthread_exit() pthread_cancel()
終止某個線程而不終止整個進程,可以有三種方法:
- 從線程函數return。這種方法對主線程不適用,從main函數return相當于調用exit。
- 線程調用pthread_exit()終止自己。
- 線程調用pthread_cancel()終止同一進程中的另一個線程。
線程返回 pthread_exit()
#include <pthread.h> void pthread_exit(void *status);status:可以指向一個變量或數據結構,用于在線程結束時向外傳遞數據,由用戶決定。
其它線程可以調用pthread_join()獲得這個指針。
注意!status指針不能指向線程的局部存儲對象,因為在線程終止時,所有局部存儲對象都會隨之銷毀。
例子:
#include <pthread.h> #include <errno.h>int val = 10;void* pth_foo(void* arg) {int i = 10;pthread_exit((void*)&val);// int *i = new int(15);// pthread_exit((void*)i); }int main() {pthread_t tid;int i = 0;if(pthread_create(&tid, NULL, pth_foo, NULL) != 0){printf("線程創建失敗。(%d:%s) \n", errno, strerror(errno));return -1;}// 線程函數傳出來的是個指針,所以也用指針接收int *pi; // 如果線程函數傳出來的是值,只要修改pi的類型就好了,下方代碼不用修改 // &pi為(void**),pi為(void*),剛好對應“&val”——線程exit傳出的值~pthread_join(tid, (void**)&pi); // ~所以*pi就能得到全局變量val的數據printf("%d \n", *pi); }----------------------------------------- [root@localhost coding]# ./test 10 /* 如果 void* pth_foo(void* arg) {int i = 10;pthread_exit((void*)&i); } 則輸出的結果是0 */補充:在本例中,pthread_exit用return替代也能正常運行。
二者區別(網絡):return會回到調用者,pthread_exit會終止線程。
再補充:return可用于所有函數,而pthread_exit專門用于結束線程。
且return不會自動調用線程清理函數。
線程取消 pthread_cancel()
#include <pthread.h> int pthread_cancel(pthread_t thread);thread:線程的標識符
返回值:成功返回0,失敗返回錯誤碼。
!補充
1、pthread_cancel后,thread內容不變,至少用printf("%x\n", thread);輸出的結果在取消線程前后都是一致的。
2、對一個已經取消的線程再次調用pthread_cancel,返回3。(對應的錯誤碼是3(No such process))
3、線程對pthread_cancel的默認響應狀態是PTHREAD_CANCEL_DEFERRED,線程運行到取消點后才退出。
pthread_setcancelstate
(是否響應)
子線程可以通過調用pthread_setcancelstate,設置對pthread_cancel請求的響應方式。
int pthread_setcancelstate(int state, int *oldstate); // 將線程的響應方式設置為state,通過oldstate返回舊狀態。PTHREAD_CANCEL_ENABLE:響應取消。
PTHREAD_CANCEL_DISABLE:不響應。
pthread_setcanceltype
(如何響應)
int pthread_setcanceltype(int type, int *oldtype);pthread_setcanceltype設置線程的取消方式。
PTHREAD_CANCEL_ASYNCHRONOUS:異步取消,立即將線程取消;
PTHREAD_CANCEL_DEFERRED:推遲取消,線程運行到**取消點(一些特定函數)**后才取消。(取消點:APUE P.363)
pthread_testcancel
調用這個函數時,如果有某個取消請求處于掛起狀態,且取消沒有設置為無效,那么線程就會被取消。
線程等待 pthread_join()
#include <pthread.h> int pthread_join(pthread_t thread, void **status);thread:調用該函數的線程將掛起等待,直到id為thread的線程終止。
status:
1、等待的線程通過return返回
? status所指向的地址存放的是線程函數的返回值。
2、等待的線程被其它線程調用函數pthread_cancel停止
? status所指向的地址存放的是常數PTHREAD_CANCELED。
3、等待的線程自己調用pthread_exit終止
? 可通過status獲取線程終止時保存的數據。如果不需要改數據,傳入NULL。
線程函數退出后。exit時傳入參數保存了數據,就能通過status獲取這個數據。
如果等待的線程調用pthread_exit時傳入參數保存了數據,就能通過status獲取這個數據。
如果有多個線程調用pthread_join獲取同一個線程的執行結果,則只有一個線程能得到結果,其余線程都將執行失敗。
(3、)的例子:
... // 已知線程函數會用pthread_exit保存一個int類型變量int val;// 傳入變量val的地址pthread_join(tid, (void**)&val);// 函數返回后,變量val指向得到pthread_exit保存的數據printf("%d \n", val);返回值:成功返回0,失敗返回錯誤碼(可以當做int用)。可能出現的錯誤碼:
| EDEADLK | 可能引起死鎖,比如2個線程互相針對對方調用pthread_join,或針對自身調用pthread_join |
| EINVAL | 目標線程是不可回收的(分離狀態),或已有其它線程在回收該目標線程 |
| ESRCH | 目標線程不存在 |
線程的結合、分離概念(線程資源回收)
在任何一個時間點上,線程是可結合的(joinable)或者是分離的(detached)。
一個可結合的線程能夠被其他線程收回其資源和殺死。在被其他線程回收之前,它的存儲器資源(例如棧)不釋放。(默認情況下線程的創建都是可結合的)
一個分離的線程是不能被其他線程回收或殺死,它的存儲器資源在它終止時由系統自動釋放。
如果一個可結合線程結束運行但沒有被join,會導致部分資源沒有被回收,所以創建線程者應該調用pthread_join來等待線程運行結束,并得到線程的退出代碼,回收其資源。
在調用pthread_join后,如果該線程沒有運行結束,調用者會被阻塞。如何解決這種情況?
答:將等待的線程設置為分離狀態。也就不需要再調用pthread_join等待該線程。
分離線程 pthread_detach()
#include <pthread.h> int pthread_detach(pthread_t thread);thread:線程id
返回值:函數成功,返回0;失敗,返回錯誤碼。
能在主線程中調用,或子線程中調用都可以。重要的是要傳入正確的線程id。
線程清理 pthread_cleanup_push/pop
線程可以安排它退出時需要調用的函數。退出函數可以建立多個,記錄在棧中。
void pthread_cleanup_push(void (*routine)(void *), void *arg);routine:函數名
arg:參數
void pthread_cleanup_pop(int execute);execute:傳入非0參數,彈出并執行;傳入0,只彈出,不執行。
線程函數調用pthread_exit時,會按出棧的順序執行所有清理函數。使用return退出的線程函數不會執行清理函數。
鎖 !
http://c.biancheng.net/thread/vip_8615.html
大概描述:互斥鎖、信號量、條件變量、讀寫鎖(還有自旋鎖、屏障等)
- 互斥鎖:只允許一個線程進入臨界區;
- 信號量:允許n個線程進入臨界區;
- 條件變量:當某線程滿足特定條件后進入臨界區,通常(幾乎是必須)和互斥鎖配合使用;
- 讀寫鎖:大多數線程能讀臨界區,少數線程能寫臨界區。允許同時有多個讀者進入,只允許1個作者進入;有讀者時不能有作者,有作者時不能有讀者。
補充:條件變量所謂的“滿足特定條件后進入臨界區”是由代碼結構實現的,即它自身并沒有提供實現該功能的函數。
大概的代碼結構:一個線程到進入區調用pthread_cond_wait等待,另一個線程達到滿足的條件后調用pthread_cond_signal解鎖。
所謂的讀寫鎖其實就是提供了2個上鎖的方式而已,具體的讀和寫動作還是得由用戶自覺操作。
線程同步(鎖)
互斥變量為pthread_mutex_t類型。初始化時可以設置為常量PTHREAD_MUTEX_INITIALIZER(只適用于靜態分配的互斥量)或用pthread_mutex_init()初始化。
書上的例子
**1、**一個被互斥鎖(作為成員變量)保護的結構foo,P.322:
#include <stdlib.h> #include <pthread.h>struct foo {int f_count;pthread_mutex_t f_lock;int f_id; }foo* foo_alloc(int id) {foo *fp;if(fp = (foo*)malloc(sizeof(foo)) != NULL){fp->f_count = 1;fp->id = id;if(pthread_mutex_init(&fp->f_lock, NULL) != 0){free(fp);return 0;}}// 繼續初始化操作 }void foo_hold(foo* fp) {pthread_mutex_lock(&fp->f_lock);fp->f_count++;pthread_mutex_unlock(&fp->f_lock); }void foo_rele(foo* fp) {pthread_mutex_lock(&fp->f_lock);if(--fp->f_count == 0){pthread_mutex_unlock(&fp->f_lock);pthread_mutex_destroy(&fp->f_lock);free(fp);}elsepthread_mutex_unlock(&fp->f_lock); }**2、**在程序中使用多個foo對象,多個foo對象用哈希表組織,增加一個互斥鎖2用于保護哈希表。P.323。
**3、**訪問foo結構的成員變量f_cout也使用互斥鎖2進行保護。P.325。兩種用途使用相同的鎖(增加了鎖的粒度),簡化了代碼結構。
線程與信號
外界信號不會中斷子線程的運行(除非是終止進程的信號)。
在多線程程序中,捕獲信號的函數放在哪都一樣,通常放在主函數中。
多線程程序中,在任一線程中調用signal或sigaction都會改變所有信號的信號處理函數。
主線程向子線程發送信號用pthread_kill函數。
多線程服務器的退出
退出信號(2 和 15)處理函數的流程:
1、關閉監聽socket;
2、用pthread_cancel終止所有子線程;
3、釋放資源(IO、文件、內存等);
4、子線程執行清理函數(在里面關閉通信socket);
多線程服務器中,子線程通常都是分離狀態的,且通常都是立即取消狀態。
I/O復用
《APUE》中的標題是“I/O多路轉接”
需求背景
1、一些函數,如read、accept在被調用時,會阻塞調用函數的線程。
read(文件描述符, buf, bufsize); accept(套接字描述符, socketaddr, socketlen);2、可用的描述符數量可能會小于需求數量,例如要打開很多個文件、要響應很多個客戶端的連接等。
使用I/O多路轉接技術,先構造一張感興趣的描述符列表,然后調用一個函數,直到這些描述符中的一個已經準備好響應操作后,函數返回。
select
#include <sys/select.h>int select(int maxfdp1, fd_set* restrict readfds, fd_set* restrict writefds, fd_set* restruct execptfds, struct timeval* restruct tvptr);返回值:準備就緒的描述符數目;超時——0;出錯——-1。從后向前介紹參數
tvptr:等待時間,timeval結構,2個成員:time_t tv_sec、long tv_nsec分別表示s和ns。
struct timeval {time_t tv_sec; //秒suseconds_t tv_usec; //微秒 1ms = 10^(-6)s };? tvptr == NULL:永久等待,直到一個描述符已經準備好,或捕捉到一個信號。捕捉到信號會使函數返回-1,errno設置為EINTR
? tvptr->tv_sec == 0 && tvptr->tv_use c== 0:不等待,立即測試所有指定的描述符并返回。
? tvptr->tv_sec != 0 && tvptr->tv_usec != 0:等待指定的時間。當指定的任一描述符準備好,或超時時返回。
readfds、writefds、execptfds:指向描述符集的指針。描述符集為fd_set類型,基本上可認為是一個很大的字節數組。
fd_set相關的操作函數
#include <sys/select.h>int FD_ISSET(int fd, fd_set *fdset); 返回值:如果fd在字符集中,返回非0值;否則返回0。void FD_CLR(int fd, fd_set *fdset);void FD_SET(int fd, fd_set *fdset);void FD_ZERO(fd_set *fdset);從視頻作者給出的例子看,select關心的描述符集中有描述符準備好了就會返回,然后下方代碼就要用
for(i=0; i<maxfdp; ++i ) {...FD_ISSET(i, 字符集);... }的方式對范圍內的所有描述符進行測試。如果被打開的描述符是想要的,就執行相關操作。
**注意!**所謂的“被打開的描述符”是在調用select函數前,由程序員自己通過調用FD_SET函數來設置的。
maxfdp1:最大描述符編號值+1。即指定描述符集的右區間。
[0, maxfdp1)
3個描述符集
readfds:讀,常用。writefds:寫,只有在輸出緩沖區滿時才會被阻塞,很少會遇到。execptfds:在網絡編程中用不到。pselect
添加了信號屏蔽參數,但很少用到,因為有其他的方式屏蔽信號。
select水平觸發
如果一個標識符的事件沒有被處理完,select會再次報告該事件。(例如沒有將客戶端傳送到的數據一次讀完)
select 缺點
1、默認支持的描述符數量只有1024,可以修改,但數量越多,效率越低。
2、每次確認描述符都要遍歷select。
代碼(已運行,能響應多個客戶端)
#include "CTcpServer.h" #include <stdio.h> #include <string.h> #include <unistd.h> #include <netdb.h> #include <stdlib.h> #include <signal.h> #include <pthread.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/select.h>CTcpServer g_TcpServer;// 處理SIGINT和SIGTERM信號 void EXIT(int sig) {printf("程序退出,信號值=%d \n", sig);close(g_TcpServer.m_listenfd);exit(0); }int main() {if (g_TcpServer.InitServer(5000) == false){printf("服務端初始化失敗,程序退出。\n");return -1;}fd_set readfdset;int maxfd;int listensock = g_TcpServer.m_listenfd;FD_ZERO(&readfdset);FD_SET(listensock, &readfdset);maxfd = listensock;while (1){fd_set tmpfdset = readfdset;int infds = select(maxfd + 1, &tmpfdset, NULL, NULL, NULL);if (infds < 0){printf("select() failed. \n");perror("select()");break;}if (infds = 0){printf("select() timeout. \n");continue;}for (int eventfd = 0; eventfd <= maxfd; ++eventfd){if (FD_ISSET(eventfd, &tmpfdset) <= 0)continue;if (eventfd == listensock){sockaddr_in client;socklen_t len = sizeof(client);int clientsock = accept(listensock, (sockaddr*)&client, &len);if (clientsock < 0){printf("accept() failed\n");continue;}printf("client(socket = %d) connect success.\n", clientsock);FD_SET(clientsock, &readfdset);if (maxfd < clientsock) maxfd = clientsock;continue;}else{char strbuffer[1024];memset(strbuffer, 0, sizeof(strbuffer));ssize_t isize = read(eventfd, strbuffer, sizeof(strbuffer));if (isize <= 0){printf("client(evenfd = %d) disconnected. \n", eventfd);close(eventfd);FD_CLR(eventfd, &readfdset);if (eventfd == maxfd){for (int i = maxfd; i > 0; --i)if (FD_ISSET(i, &readfdset)){maxfd = i; break;}printf("maxfd = %d, update. \n", maxfd);}continue;}printf("recv(eventfd = %d, size = %d):%s \n)", eventfd, isize, strbuffer);write(eventfd, strbuffer, strlen(strbuffer));}}}return 0; }問題(似乎已解決)
作者代碼中調用select時,要先創建一個fd_set類型的臨時變量tmpfdset,讓該臨時變量參與select函數,在遍歷查找有事件響應的標識符時,使用的也是臨時變量tmpfdset。
fd_set tmpfdset = readfdset;int infds = select(maxfd+1, &tmpfdset, NULL, NULL, NULL);...for(int eventfd = 0; eventfd <= maxfd; ++eventfd){if(FD_ISSET(eventfd, &tmpfdset) <= 0)continue;if(eventfd == listensock)...}經過自己的粗略檢查(在select前后檢查tmpfdset標識符4的值)得出的結論:
似乎select在返回時,會將有事件的描述符以外的所有描述符都清零,所以使用for循環從0開始遍歷一定且只會遇到發生了事件的那個描述符。
嘗試過直接將readfdset傳入select,結果服務端只能響應第一個連入的客戶端。
驗證代碼:
#define CHECK(x) printf("readfdset[%d] is %d \n", x, FD_ISSET(x, &tmpfdset))...fd_set tmpfdset = readfdset;CHECK(4); // 經過幾次實踐,已知描述符4一定對應第一個連接的客戶端socketint infds = select(maxfd+1, &tmpfdset, NULL, NULL, NULL);...for(int eventfd = 0; eventfd <= maxfd; ++eventfd){if(FD_ISSET(eventfd, &tmpfdset) <= 0)continue;CHECK(4);if(eventfd == listensock)...}結果:第1個客戶端的對應描述符是4
... ----------------------- readfdset[4] is 1 // 調用select之前 readfdset[4] is 0 // 調用select之后 recv(eventfd = 5, size = 3): 345 ----------------------- readfdset[4] is 1 readfdset[4] is 1 recv(eventfd = 4, size = 3): qwe如果不是第1個客戶端的事件導致select返回,則select返回后,對應的FD_ISSET(客戶端1, tmpfdset)會返回0。
poll
#include <poll.h>int poll(struct pollfd fdarray[]. nfds_t nfds, int timeout);返回值:成功——準備就緒的描述符數目;超時——返回0;出錯——返回-1nfds:就是select的maxfdp1。
struct pollfd {inf fd; // 文件標識符。short events; // 期待標識符上發生的事件short revents; // 標識符發生事件后的返回值 }fd:若設置為-1,則表示忽略events,且revents返回0。
events:若設置為0,則忽略所有fd發生的事件,且revents返回0。輸入的參數似乎可以用“|”連接。
revents:是個輸出參數,值由內核填充,表示發生的事件。
例子中的代碼結構與select基本一致。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <poll.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/fcntl.h>// ulimit -n #define MAXNFDS 1024// 初始化服務端的監聽端口。 int initserver(int port);int main(int argc,char *argv[]) {...// 初始化服務端用于監聽的socket。int listensock = initserver(atoi(argv[1]));printf("listensock=%d\n",listensock);...int maxfd; // fds數組中需要監視的socket的大小。struct pollfd fds[MAXNFDS]; // fds存放需要監視的socket。// ------------- 等價于FD_ZERO() -------------for (int ii=0;ii<MAXNFDS;ii++) fds[ii].fd=-1; // 初始化數組,把全部的fd設置為-1。// 把listensock添加到數組中。fds[listensock].fd=listensock;fds[listensock].events=POLLIN; // 有數據可讀事件,包括新客戶端的連接、客戶端socket有數據可讀和客戶端socket斷開三種情況。maxfd=listensock;while (1){ // ------------- 阻塞 -------------int infds = poll(fds, maxfd+1, 5000);// 返回失敗。if (infds < 0){printf("poll() failed.\n"); perror("poll():"); break;}// 超時。if (infds == 0){printf("poll() timeout.\n"); continue;}// 檢查有事情發生的socket,包括監聽和客戶端連接的socket。// 這里是客戶端的socket事件,每次都要遍歷整個集合,因為可能有多個socket有事件。for (int eventfd=0; eventfd <= maxfd; eventfd++){if (fds[eventfd].fd<0) continue;// ------------- 與select略有不同,這里先檢查“.revents”事件類型 -------------if ((fds[eventfd].revents&POLLIN)==0) continue; // ------------- 未知 -------------fds[eventfd].revents=0; // 先把revents清空。// ------------- “.f”非零,且“.revents&POLLIN”非零,再檢查匹配標識符 -------------if (eventfd==listensock){ // ------------- 下文內容與select基本相同 -------------// 如果發生事件的是listensock,表示有新的客戶端連上來。struct sockaddr_in client;socklen_t len = sizeof(client);int clientsock = accept(listensock,(struct sockaddr*)&client,&len);if (clientsock < 0){printf("accept() failed.\n"); continue;}printf ("client(socket=%d) connected ok.\n",clientsock);if (clientsock>MAXNFDS){ printf("clientsock(%d)>MAXNFDS(%d)\n",clientsock,MAXNFDS); close(clientsock); continue;}// ------------- poll登記新標識符 -------------fds[clientsock].fd=clientsock;fds[clientsock].events=POLLIN; fds[clientsock].revents=0; if (maxfd < clientsock) maxfd = clientsock;printf("maxfd=%d\n",maxfd);continue;}else {// 客戶端有數據過來或客戶端的socket連接被斷開。char buffer[1024];memset(buffer,0,sizeof(buffer));// 讀取客戶端的數據。ssize_t isize=read(eventfd,buffer,sizeof(buffer));// 發生了錯誤或socket被對方關閉。if (isize <=0){printf("client(eventfd=%d) disconnected.\n",eventfd);close(eventfd); // 關閉客戶端的socket。fds[eventfd].fd=-1;// 重新計算maxfd的值,注意,只有當eventfd==maxfd時才需要計算。if (eventfd == maxfd){for (int ii=maxfd;ii>0;ii--){if ( fds[ii].fd != -1){maxfd = ii; break;}}printf("maxfd=%d\n",maxfd);}continue;}printf("recv(eventfd=%d,size=%d):%s\n",eventfd,isize,buffer);// 把收到的報文發回給客戶端。write(eventfd,buffer,strlen(buffer));}}}return 0; }epoll
相關數據結構
typedef union epoll_data {void *ptr;int fd; // 目前唯一關注的成員uint32_t u32;uint64_t u64; } epoll_data_t;struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */ };events的取值
EPOLLIN: 關聯的文件已經對read操作可用。(常用)EPOLLPUT: 關聯的文件已經對write操作可用。(常用)EPOLLRDHUP: 對應的socket連接的已經關閉。(常用)EPOLLPRI: 緊急消息對read操作可用。(~)EPOLLERR: 對應的文件發生錯誤,默認等待,不需要再手動設置。(-)EPOLLHUP: 對應的文件掛起,默認等待,不需要再手動設置。(-)EPOLLET: 設置為模式邊緣觸發模式。(默認是水平觸發模式)EPOLLONESHOT: “Sets the one-shot behavior for the associated file descriptor.”代碼結構
// 1、需要1個int類型接收epoll_create函數的返回值(標識符,表示eopll實例),epoll_create的參數大于0即可,沒什么意義 int epollfd = epoll_create(1);// 2、創建一個epoll_event結構并初始化,設定標識符和監聽的事件 struct epoll_event ev; ev.data.fd = listensock; ev.events = EPOLLIN;// 3、使用epoll_ctl函數進行設置 epoll_ctl(epollfd, EPOLL_CTL_ADD, listensock, &ev);// 4、while。添加刪除socket標識符的方式與2、3流程一致 while (1) {// MAXEVENTS的值由實際需求決定struct epoll_event events[MAXEVENTS]; // -------------------------------int infds = epoll_wait(epollfd, events, MAXEVENTS, -1);if (infds < 0)...if (infds == 0)... // 查找的范圍由epoll_wait的返回值決定for (int i=0; i<infds; i++){if ((events[i].data.fd == listensock) &&(events[i].events & EPOLLIN)){...響應客戶端連接...int clientsock = accept(li...;...// ------------------------------- // 復用全局變量ev,設置好參數后用epoll_ctl添加// 把新的客戶端添加到epoll中。memset(&ev,0,sizeof(struct epoll_event));ev.data.fd = clientsock;ev.events = EPOLLIN;epoll_ctl(epollfd, EPOLL_CTL_ADD, clientsock, &ev);}else if (events[i].events & EPOLLIN){...與客戶端通信...if (isize <=0) // 發生錯誤或連接被斷開{// 把已斷開的客戶端從epoll中刪除。 // ------------------------------- // 執行流程與上文的添加基本一致,就只是修改了epoll_ctl的1個參數而已memset(&ev, 0, sizeof(struct epoll_event));ev.data.fd = events[i].data.fd;ev.events = EPOLLIN;epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, &ev);close(events[i].data.fd);continue;}}} }// 5、關閉epoll close(epollfd);水平觸發和邊緣觸發
epoll默認使用水平觸發。
水平觸發:報告了fd后事件沒被處理或數據沒有被全部讀取,epoll會立即再報告該fd。
邊緣觸發:報告了fd后事件沒被處理或數據沒有被全部讀取,epoll會下次再報告該fd。
----------------------------
pthread_equal
int pthread_equal(pthread_t tid1, pthread_t tid2);返回值:相等——非0值,不相等——0。pthread_t類型是采用數據類型來實現的,所以不能作為整數處理(==),得用pthread_equal來進行比較。
一個打印pthread_t類型變量的方法
// “%lu” 和 “%lx” pthread_t tid = pthread_self(); printf("tid: %lu(0x%lx) \n", (unsigned long)tid, (unsigned long)tid);// 輸出 tid: 140048119990016(0x7f5f7e718700)pthread_self
pthread_t pthread_self(void);返回值:調用函數的線程ID通常與pthread_equal一起使用。
pthread_create
int pthread_create(pthread_t* restrict tidp, constpthread_attr_t* restrict attr, void* (*start_rtn)(void*), void* restrict arg);返回值:成功——0;失敗——錯誤編號。restrict:C語言中的一種類型限定符(Type Qualifiers),用于告訴編譯器,對象已經被指針所引用,不能通過除該指針外所有其他直接或間接的方式修改該對象的內容。
創建后的線程ID存入tidp所指的地址;
attr:屬性;
start_rtn:函數指針
arg:函數參數
線程能按順序創建,但不一定會按順序執行。
函數調用失敗時會返回錯誤碼,不會設置errno。
pthread_exit
void pthread_exit(void* rval_ptr);在不終止整個進程的情況下,停止線程。
rval_ptr指針指向線程要返回的數據。其它線程可以通過pthread_join訪問到這個指針。(如果要返回的數據大小<=sizeof(void*),則可以通過強制轉換直接返回數據。)
**注意:**如返回的是個指針,必須確保在線程結束后,指針所指內存數據仍有效。如果返回的是值,則不需要擔心。
void* func_one(void* ptr) {int val = 10;pthread_exit((void*)(long)val);// pthread_exit((void*)(long)20);// 兩種方法都能有效將返回值傳遞出去 }SIGSEGV
如果上方代碼是:
void* func_one(void* ptr) {int* val = 0;*val = 30;pthread_exit((void*)(long)val); }在gdb調試中就會有如下警告:
Program terminated with signal SIGSEGV, Segmentation fault.SIGSEGV是當一個進程執行了一個無效的內存引用,或發生段錯誤時發送給它的信號。
與return的區別
return只是退出了函數,線程仍有可能存在;pthread_exit讓線程停止。
二者都會調用清理函數,其它的區別還看不懂。
pthread_join
int pthread_join(pthread_t thread, void** rval_ptr);返回值:成功——0;失敗——錯誤編號。如果線程被取消(pthread_cancel),rval_ptr所指向的內存單元被設置為PTHREAD_CANCELED(好像值是-1)。
如果不關心線程返回值,rval_ptr可以直接填NULL。
獲取返回值:
幫助理解:pthread_join返回后,rval_ptr所指向的內存單元保存的就是pthread_exit的返回值(可能是數據本身,也可能是指針)。
// 1、返回數據本身 void* func_one(void* ptr) {g_val = 10;pthread_exit((void*)(long)g_val); } int main() {pthread_t thread_one;pthread_create(&thread_one, NULL, func_one, NULL);int i;// &i所指向的內存單元就是i的值,也就是g_val的值pthread_join(thread_one, (void**)&i);printf("%d\n", i); }// 2、返回的是指針 void* func_one(void* ptr) {g_val = 10;pthread_exit((void*)&g_val); } int main() {pthread_t thread_one;pthread_create(&thread_one, NULL, func_one, NULL);int *i; // &i所指向的內存單元是指針i,仍是一個指針,也就是&g_val,解一次引用后得到數據pthread_join(thread_one, (void**)&i);printf("%d\n", *i); }pthread_cancel
int pthread_cancel(pthread_t tid);返回值:成功——0;失敗——錯誤編號。終止同一進程中的其它線程。
調用這一函數只是提出請求,并不會阻塞等待。
線程安排它退出時需要調用的函數。這樣的函數被稱為線程清理處理程序,可以設置多個,存入棧中。
例子:讓一個子線程取消另一個子線程,參數的傳遞好像有點麻煩
int main() {...// 將tid1的地址轉為void*后傳給線程函數th_cancelpthread_create(&tid2, NULL, th_cancel, (void*)&tid1);... }void* th_cancel(void* arg) { // 先將arg轉為pthread_t*類型的指針,然后對其解引用*(...)pthread_cancel(*((pthread_t*)arg)); }pthread_cleanup_push/pop
設置線程清理處理程序。
線程結束時要執行一些善后工作,這些代碼不方便寫在主函數中,所以有了這對函數。
void pthread_cleanup_push(void (*rtn)(void*), void *arg);void pthread_cleanup_pop(int execute);-
二者是以宏的形式定義的,pthread_cleanup_push帶有一個"{",而pthread_cleanup_pop帶有一個"}",所以必須成對使用!
-
如果使用pthread_cleanup_pop(0),則只會將棧中的一個清理程序彈出,不執行。傳入任意非0的值都會彈出并執行函數。
清理函數的執行時機
**!!**清理函數執行時,執行多少個清理函數,按什么順序執行,都取決于此時棧中的情況!!
**1、**函數運行到pthread_cleanup_pop(非0);
**2、**線程退出。自行退出或被cancel都能觸發清理函數,且會執行棧中的所有清理函數。
注意!:如果主線程-進程終止而導致的子線程終止,可能會使清理函數來不及執行。
所以主線程總是要給子線程的運行留下足夠的時間。
再補充:清理函數要被執行,一個大前提是“棧中存有清理函數”,即線程退出時,pthread_cleanup_pop被執行的次數必須少于pthread_cleanup_push。
誤區糾正
誤區1:pthread_cleanup_pop僅僅是用來設置棧中函數在彈出時是否執行。
糾正:pthread_cleanup_pop既是函數調用,也會在線程被結束時彈出并調用清理函數。
證:
void* func_one(void* ptr) {pthread_cleanup_push(exit_foo, (void*)(long)1);pthread_cleanup_push(exit_foo, (void*)(long)2);// pthread_exit((void*)(long)5);// 主線程用此函數創建了子線程后,立即調用pthread_cancelsleep(5);printf("before pop \n");pthread_cleanup_pop(1);pthread_cleanup_pop(1);printf("after pop, sleep \n");sleep(5);printf("thread exit \n");pthread_exit((void*)(long)10); }輸出結果:和預料的一樣,子線程在退出時按棧順序調用了清理函數 [root@localhost coding]# make run g++ -g -o test test.cpp -lpthread ./test thread cleanup 2 thread cleanup 1 -1------------------------------------------------------void* func_one(void* ptr) {pthread_cleanup_push(exit_foo, (void*)(long)1);pthread_cleanup_push(exit_foo, (void*)(long)2);// pthread_exit((void*)(long)5);// 主線程創建子線程后,等待 // sleep(5);printf("before pop \n");pthread_cleanup_pop(1);pthread_cleanup_pop(1);printf("after pop, sleep \n");sleep(5);printf("thread exit \n");pthread_exit((void*)(long)10); }輸出結果:就像調用了函數一樣,在線程結束前就執行了清理函數,等到線程退出時不再執行清理函數 [root@localhost coding]# make run ./test before pop thread cleanup 2 thread cleanup 1 after pop, sleep // (約5s后) thread exit 10!收獲
被pthread_cleanup_push和pthread_cleanup_pop“包裹”起來的代碼段,就是這對push/pop要負責善后的代碼段。
實際應用中應該是像這樣吧?:
pthread_cleanup_push(文件清理函數); ...文件操作... pthread_cleanup_pop(1);pthread_cleanup_push(IO清理函數); ...IO操作... pthread_cleanup_pop(1);pop的位置
例子:在本例中,return和pthread_exit效果一樣。
void* func_one(void* ptr) {pthread_cleanup_push(exit_foo, (void*)(long)1);pthread_cleanup_push(exit_foo, (void*)(long)2);// 一、 // pthread_exit((void*)(long)5);pthread_cleanup_pop(0);pthread_cleanup_pop(1);// 二、pthread_exit((void*)(long)10); }// ------------------------------- // 一、 [root@localhost coding]# make run ./test thread cleanup 2 thread cleanup 1 5 // 二、 [root@localhost coding]# make run g++ -g -o test test.cpp -lpthread ./test thread cleanup 1 10將pthread_exit放在pthread_cleanup_pop之前,所有的清理函數都會執行,無論是否傳入參數0。(是否可以理解為“還沒有讀到pthread_cleanup_pop的具體設置,線程就退出了,所以默認都彈出并執行”)
**pthread_cancel同理!**如果一個子線程在執行到pthread_cleanup_pop之前就被其它線程cancel,也會執行所有的清理函數,而不管具體的pop設置。
pthread_detach
可以讓線程函數自己調用pthread_detach(pthread_self()),或是由別的線程調用pthread_detach(tid)。
int pthread_detach(pthread_t tid);返回值:成功——0;失敗——錯誤編號。讓ID為tid的線程處于分離狀態,不能再用pthread_join對其進行等待。
對分離狀態的線程調用pthread_join不會阻塞調用函數的線程,且pthread_join接收的數據沒有意義。
目前只知道會對pthread_join有影響。cancel和push/pop無影響。
----------------------------
除了對應的init函數,似乎所有鎖都能用一個PTHREAD_XXX_INITIALIZER變量進行賦值來完成初始化。
pthread_mutex_init/destroy
PTHREAD_MUTEX_INITIALIZER可對靜態分配的互斥鎖(或作為全局變量的互斥鎖)進行初始化。
// 2個參數 int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);int pthread_mutex_destroy(pthread_mutex_t* restrict mutex);2個函數的返回值:成功——0;失敗——錯誤編號。將attr設為NULL,可使用默認的初始化互斥量。
pthread_mutex_lock/trylock/unlock
int pthread_mutex_lock(pthread_mutex_t* mutex);int pthread_mutex_trylock(pthread_mutex_t* mutex);int pthread_mutex_unlock(pthread_mutex_t* mutex);3個函數的返回值:成功——0;失敗——錯誤編號。使用pthread_mutex_lock對互斥量上鎖,如果互斥量已經上鎖,調用pthread_mutex_lock的函數會被阻塞,直到互斥量被解鎖。
pthread_mutex_unlock解鎖互斥量。
如果不希望線程被阻塞,可以使用pthread_mutex_trylock嘗試對互斥量加鎖。如果互斥量可用,加鎖;否則返回EBUSY。
pthread_mutex_timedlock
tsptr使用的是絕對時間:1970年1月1日以來經過的秒數。
int pthread_mutex_timedlock(pthread_mutex_t* restrict mutex, const struct timespec* restrict tsptr);返回值:成功——0;失敗——錯誤編號。功能與pthread_mutex_lock基本等價,但到達超時時間時,會返回錯誤碼ETIMEDOUT。即這個函數愿意阻塞等待X秒。
tsptr:timespec結構,2個成員:time_t tv_sec、long tv_nsec分別表示s和ns。
1*109ns= 1s 。
獲取時間參數的例子
struct timespec tout; clock_gettime(CLOCK_REALTIME, &tout); tout.tv_sec += 10; pthread_mutex_timedlock(&lock, &tout);pthread_rwlock_init/destroy
PTHREAD_RWLOCK_INITIALIZER可對靜態分配的讀寫鎖(或作為全局變量的讀寫鎖)進行初始化。
// 2個參數 int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr);int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);2個函數的返回值:成功——0;失敗——錯誤編號。只有當讀狀態的鎖的使用頻率遠高于寫狀態的鎖的使用頻率,使用讀寫鎖才可以改善性能。
pthread_rwlock_rdlock/wrlock/unlock
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);3個函數的返回值:成功——0;失敗——錯誤編號。pthread_rwlock_rdlock:在讀模式下鎖定讀寫鎖。
pthread_rwlock_wrlock:在寫模式下鎖定讀寫鎖。
pthread_rwlock_unlock:2種模式的讀寫鎖都能解鎖。
pthread_rwlock_tryrdlock/trywrlock
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);返回值:成功——0;失敗——錯誤編號。可以獲得鎖,加鎖,返回0;否則返回EBUSY。
pthread_rwlock_timedrdlock/timedwrlock
int pthread_rwlock_timedrdlock(pthread_rwlock_t* restrict rwlock, const struct timespec* restrict tsptr);int pthread_rwlock_timedwrlock(pthread_rwlock_t* restrict rwlock, const struct timespec* restrict tsptr);返回值:成功——0;失敗——錯誤編號。超時到期都返回ETIMEDOUT。都使用絕對時間。
總結
以上是生活随笔為你收集整理的【笔记整理 - 多线程编程】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 零基础Ar学习之Unity3D运行Eas
- 下一篇: 杰力科创--单片机--DLT8P65SA