在最開始實現併發服務端的時候, 最開始想到的辦法便是使用多進程, 使每個 TCP 連接對應一個進程; 之後我們又將多進程併發改寫成了 IO 複用的方式實現了相同的功能; 但現在唯一沒有將服務端改寫成線程, 本節就來改寫服務端.
線程服務端
在改寫成多線程之前, 要對 [線程創建], [線程同步]以及線程終止有所瞭解才行.
主函數僅僅只是將連接後回射操作交給線程即可 :
int main(int argc, char *argv[]){
int sockfd, clientfd;
socklen_t socklen;
struct sockaddr_in servAddr, cliAddr;
pthread_t tid;
bzero(&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(atoi(argv[1]));
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
socklen = sizeof(cliAddr);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
goto exit;
if(bind(sockfd, (struct sockaddr* )&servAddr, sizeof(servAddr)) < 0)
goto exit;
listen(sockfd, 5);
while(1){
clientfd = accept(sockfd, (struct sockaddr *)&cliAddr, &socklen);
if(clientfd < 0)
goto exit;
// 將回射功能交給線程
if(pthread_create(&tid, NULL, echo, (void *)&clientfd))
goto exit;
}
exit:
return 0;
}
線程函數就只是一個簡單的回射功能 :
void *echo(void *sockfd){
// 使線程分離, 退出後直接釋放資源
pthread_detach(pthread_self());
char buf[BUFSIZE];
int n;
int fd = *(int *)sockfd;
while(1){
n = read(fd, buf, sizeof(buf));
if(0 == n) break;
write(fd, buf, n);
}
close(fd);
pthread_exit((void *)0);
}
線程函數中注意需要執行 pthread_detach()
函數來完成線程分離, 這樣主進程就可以不執行 pthread_join
阻塞來回收線程的資源, 線程結束後自己回收資源.
服務端完整代碼 : service.c
服務端 :
./service 8080
客戶端完整代碼 : client.c
客服端 :
./client 127.0.0.1 8080
線程回射程序bug
在前面不管是多進程還是IO複用我們都或多或少遇到過問題, 那麼改寫成線程也同樣會有意想不到的問題.
上面的回射程序有一處很嚴重的bug.
pthread_create(&tid, NULL, echo, (void *)&clientfd)
我們函數參數傳入的是指針, 線程是併發執行, 對於參數 clientfd
就容易導致同步問題. 如果爲每次的clientfd
申請內存, 也會導致函數並非線程安全, 並且頻繁的申請也容易導致申請失敗同時內存釋放也是一個問題.
解決方法 :
- 加鎖, 保證同步. 但是使用鎖運行效率肯定就很低, 反而性能會降低.
- 封裝函數傳入形參或者結構體 . 一般都採用該呢方式, 也有更好的封裝和擴展性. 之後可以在線程池[1]中看到.
小結
- 線程編程注意同步問題.
- 可以將 web 客戶端 的改寫成線程, 這樣就可以不將 connect 設置成非阻塞.