pthread 系列函數 和 簡單多線程服務器端程序
一、posix 線程概述
我們知道,進程在各自獨立的地址空間中運行,進程之間共享數據需要用進程間通信機制,有些情況需要在一個進程中同時執行多個控制流程,這時候線程就派上了用場,比如實現一個圖形界面的下載軟件,一方面需要和用戶交互,等待和處理用戶的鼠標鍵盤事件,另一方面又需要同時下載多個文件,等待和處理從多個網絡主機發來的數據,這些任務都需要一個“等待-處理”的循環,可以用多線程實現,一個線程專門負責與用戶交互,另外幾個線程每個線程負責和一個網絡主機通信。
以前我們講過,main函數和信號處理函數是同一個進程地址空間中的多個控制流程,多線程也是如此,但是比信號處理函數更加靈活,信號處理函數的控制流程只是在信號遞達時產生,在處理完信號之後就結束,而多線程的控制流程可以長期並存,操作系統會在各線程之間調度和切換,就像在多個進程之間調度和切換一樣。由於同一進程的多個線程共享同一地址空間,因此TextSegment、Data Segment都是共享的,如果定義一個函數,在各線程中都可以調用,如果定義一個全局變量,在各線程中都可以訪問到,除此之外,各線程還共享以下進程資源和環境:
文件描述符表
每種信號的處理方式(SIG_IGN、SIG_DFL或者自定義的信號處理函數)
當前工作目錄用戶id和組id
但有些資源是每個線程各有一份的:
線程id
上下文,包括各種寄存器的值、程序計數器和棧指針
棧空間
errno變量
信號屏蔽字
調度優先級
我們將要學習的線程庫函數是由POSIX標准定義的,稱為POSIX thread或者pthread。在Linux上線程函數位於libpthread共享庫中,因此在編譯時要加上-lpthread選項。
二、pthread 系列函數
(一)
功能:創建一個新的線程
原型int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
參數
thread:返回線程ID
attr:設置線程的屬性,attr為NULL表示使用默認屬性
start_routine:是個函數地址,線程啟動後要執行的函數
arg:傳給線程啟動函數的參數
返回值:成功返回0;失敗返回錯誤碼
錯誤檢查:
以前學過的系統函數都是成功返回0,失敗返回-1,而錯誤號保存在全局變量errno中,而pthread庫的函數都是通過返回值返回錯誤號,雖然每個線程也都有一個errno,但這是為了兼容其它函數接口而提供的,pthread庫本身並不使用它,通過返回值返回錯誤碼更加清晰。由於pthread_create的錯誤碼不保存在errno中,因此不能直接用perror(3)打印錯誤信息,可以先用strerror(3)把錯誤碼轉換成錯誤信息再打印。
(二)
功能:線程終止
原型void pthread_exit(void *value_ptr);
參數
value_ptr:value_ptr不要指向一個局部變量,因為當其它線程得到這個返回指針時線程函數已經退出了。
返回值:無返回值,跟進程一樣,線程結束的時候無法返回到它的調用者(自身)
如果需要只終止某個線程而不終止整個進程,可以有三種方法:
1、從線程函數return。這種方法對主線程不適用,從main函數return相當於調用exit,而如果任意一個線程調用了exit或_exit,則整個進程的所有線程都終止。
2、一個線程可以調用pthread_cancel 終止同一進程中的另一個線程。
3、線程可以調用pthread_exit終止自己。
(三)
功能:等待線程結束
原型int pthread_join(pthread_t thread, void **value_ptr);
參數
thread:線程ID
value_ptr:它指向一個指針,後者指向線程的返回值
返回值:成功返回0;失敗返回錯誤碼
當pthread_create 中的 start_routine返回時,這個線程就退出了,其它線程可以調用pthread_join得到start_routine的返回值,類似於父進程調用wait(2)得到子進程的退出狀態。
調用該函數的線程將掛起等待,直到id為thread的線程終止。thread線程以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:
1、如果thread線程通過return返回,value_ptr所指向的單元裡存放的是thread線程函數的返回值。
2、如果thread線程被別的線程調用pthread_cancel異常終止掉,value_ptr所指向的單元裡存放的是常數PTHREAD_CANCELED。
3、如果thread線程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。
如果對thread線程的終止狀態不感興趣,可以傳NULL給value_ptr參數。
(四)
功能:返回線程ID
原型pthread_t pthread_self(void);
返回值:成功返回0
在Linux上,pthread_t類型是一個地址值,屬於同一進程的多個線程調用getpid(2)可以得到相同的進程號,而調用pthread_self(3)得到的線程號各不相同。線程id只在當前進程中保證是唯一的,在不同的系統中pthread_t這個類型有不同的實現,它可能是一個整數值,也可能是一個結構體,也可能是一個地址,所以不能簡單地當成整數用printf打印。
(五)
功能:取消一個執行中的線程
原型int pthread_cancel(pthread_t thread);
參數
thread:線程ID
返回值:成功返回0;失敗返回錯誤碼
一個新創建的線程默認取消狀態(cancelability state)是可取消的,取消類型( cancelability type)是同步的,即在某個可取消點( cancellation point,即在執行某些函數的時候)才會取消線程。具體可以man 一下。
相關函數 int pthread_setcancelstate(int state, int *oldstate); int pthread_setcanceltype(int type, int *oldtype);
(六)
功能:將一個線程分離
原型int pthread_detach(pthread_t thread);
參數
thread:線程ID
返回值:成功返回0;失敗返回錯誤碼
一般情況下,線程終止後,其終止狀態一直保留到其它線程調用pthread_join獲取它的狀態為止(僵線程)。但是線程也可以被置為detach狀態,這樣的線程一旦終止就立刻回收它占用的所有資源,而不保留終止狀態。不能對一個已經處於detach狀態的線程調用pthread_join,這樣的調用將返回EINVAL。對一個尚未detach的線程調用pthread_join或pthread_detach都可以把該線程置為detach狀態,也就是說,不能對同一線程調用兩次pthread_join,或者如果已經對一個線程調用了pthread_detach就不能再調用pthread_join了。
下面寫個程序走一下這些函數:
#include<stdio.h> #include<stdlib.h> #include<sys/ipc.h> #include<sys/msg.h> #include<sys/types.h> #include<unistd.h> #include<errno.h> #include<pthread.h> #include<string.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) void *routine(void *arg) { int i; for (i = 0; i < 20; i++) { printf("B"); fflush(stdout); usleep(20); /* if (i == 3) pthread_exit("ABC"); */ } return "DEF"; } int main(void) { pthread_t tid; int ret; if ((ret = pthread_create(&tid, NULL, routine, NULL)) != 0) { fprintf(stderr, "pthread create: %s\n", strerror(ret)); exit(EXIT_FAILURE); } int i; for (i = 0; i < 20; i++) { printf("A"); fflush(stdout); usleep(20); } void *value; if ((ret = pthread_join(tid, &value)) != 0) { fprintf(stderr, "pthread create: %s\n", strerror(ret)); exit(EXIT_FAILURE); } printf("\n"); printf("return msg=%s\n", (char *)value); return 0; }
創建一個線程,主線程打印A,新線程打印B,主線程調用pthread_join 等待新線程退出,打印退出值。
simba@ubuntu:~/Documents/code/linux_programming/UNP/pthread$ ./pthread_create
ABAABABABABABABABABABABABABAABABBABABABB
return msg=DEF
在新線程中也可調用pthread_exit 退出。
三、簡單的多線程服務器端程序
在將socket 編程的時候曾經使用fork 多進程的方式來實現並發,現在嘗試使用多線程方式來實現:
#include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <pthread.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) void echo_srv(int conn) { char recvbuf[1024]; while (1) { memset(recvbuf, 0, sizeof(recvbuf)); int ret = read(conn, recvbuf, sizeof(recvbuf)); if (ret == 0) { printf("client close\n"); break; } else if (ret == -1) ERR_EXIT("read"); fputs(recvbuf, stdout); write(conn, recvbuf, ret); } } void *thread_routine(void *arg) { /* 主線程沒有調用pthread_join等待線程退出 */ pthread_detach(pthread_self()); //剝離線程,避免產生僵線程 /*int conn = (int)arg;*/ int conn = *((int *)arg); free(arg); echo_srv(conn); printf("exiting thread ...\n"); return NULL; } int main(void) { int listenfd; if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); int on = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) ERR_EXIT("setsockopt"); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind"); if (listen(listenfd, SOMAXCONN) < 0) ERR_EXIT("listen"); struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int conn; while (1) { if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) ERR_EXIT("accept"); printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); pthread_t tid; // int ret; /*pthread_create(&tid, NULL, thread_routine, (void*)&conn);*/ // race condition問題,竟態問題 int *p = malloc(sizeof(int)); *p = conn; pthread_create(&tid, NULL, thread_routine, p); /* if ((ret = pthread_create(&tid, NULL, thread_routine, (void*)conn)) != 0) //64位系統時指針不是4個字節,不可移植 { fprintf(stderr, "pthread_create:%s\n", strerror(ret)); exit(EXIT_FAILURE); } */ }
程序邏輯並不復雜,一旦accept 返回一個已連接套接字,就創建一個新線程對其服務,在每個新線程thread_routine 中調用pthread_detach 剝離線程,我們的主線程不能調用pthread_join 等待這些新線程的退出,因為還要返回while 循環開頭去在accept 中阻塞監聽。
如果使用pthread_create(&tid, NULL, thread_routine, (void*)&conn); 存在的問題是如果accept 再次返回一個已連接套接字,而此時thread_routine 函數還沒取走conn 時,可能會讀取到已經被更改的conn 值。
如果使用 pthread_create(&tid, NULL, thread_routine, (void*)conn); 存在的問題是在64位系統中指針不是4個字節而是8個字節,即不可移植 性。
使用上述未被注釋的做法,每次返回一個conn,就malloc 一塊內存存放起來,在thread_routine 函數中去讀取即可。
開多個客戶端,可以看到正常服務。