1.引言
線程類似於進程。如同進程,線程由內核按時間分片進行管理。在單處理器系統中,內核使用時間分片來模擬線程的併發執行,這種方式和進程的相同。而在多處理器系統中,如同多個進程,線程實際上一樣可以併發執行。
預備知識:
Linux Socket編程入門——淺顯易懂
Linux Socket編程——多進程併發
2.多線程概述
那麼爲什麼對於大多數合作性任務,多線程比多個獨立的進程更優越呢?這是因爲,線程共享相同的內存空間。不同的線程可以存取內存中的同一個變量。所以,程序中的所有線程都可以讀或寫聲明過的全局變量。如果曾用 fork() 編寫過重要代碼,就會認識到這個工具的重要性。爲什麼呢?雖然 fork() 允許創建多個進程,但它還會帶來以下通信問題: 如何讓多個進程相互通信,這裏每個進程都有各自獨立的內存空間。對這個問題沒有一個簡單的答案。雖然有許多不同種類的本地 IPC (進程間通信),但它們都遇到兩個重要障礙:強加了某種形式的額外內核開銷,從而降低性能。
2.1 線程的創建
pthread_create()函數
#include<pthread.h>
int pthread_create(pthread_t* thread_out, pthread_attr_t const* attr,
void* (*start_routine)(void*), void* arg)
與fork()調用創建一個進程的方法不同,pthread_create()創建的線程並不具備與主線程(即調用pthread_create()的線程)同樣的執行序列,而是使其運行start_routine(arg)函數。
PS:另外,在編譯時注意加上-lpthread參數,以調用靜態鏈接庫。因爲pthread並非Linux系統的默認庫
參數:
- thread_out:創建線程後的標識符,下面會介紹。
- attr:設置線程屬性。傳NULL爲默認屬性(對大多數程序來說,使用默認屬性就夠了)。
- start_routine:線程運行函數的起始地址(簡單說就是函數指針)。同時,start_routine()可以返回一個void *類型的返回值,而這個返回值也可以是其他類型,並由pthread_join()獲取。
- arg:運行函數的參數,如果沒有使用參數,就直接NULL。
返回值:
- 創建成功返回0。
- 若不爲0則說明創建線程失敗,常見的錯誤返回代碼爲EAGAIN和EINVAL。
- EAGAIN表示系統限制創建新的線程,例如線程數目過多了;
- EINVAL表示第二個參數代表的線程屬性值非法。
3.線程的回收
Linux系統中程序的線程資源是有限的,表現爲對於一個程序其能同時運行的線程數是有限的。而默認的條件下,一個線程結束後,其對應的資源不會被釋放,於是,如果在一個程序中,反覆建立線程,而線程又默認的退出,則最終線程資源耗盡,進程將不再能建立新的線程。
和進程一樣,線程也需要回收。
回收方式:
linux線程執行和windows不同,Linux線程有兩種類型:joinable和unjoinable。(可結的和分離的)。創建線程默認是joinable線程。
- 如果線程是joinable狀態,當線程函數自己返回退出時或pthread_exit()時都不會釋放線程所佔用堆棧和線程描述符(總計8K多)。只有當你調用了pthread_join()之後這些資源纔會被釋放。
- 由於pthread_join()函數會阻塞等待指定線程退出,然後回收資源,這樣就有同步的功能,使一個線程等待另一個線程退出,然後才繼續運行,但是對於服務器程序如果主線程在新創建的線程工作時還需要做別的事情,這種方法不是很好。這時就需要由另一個線程來負責回收線程。(這裏要注意,兄弟線程可以回收線程,進程不能回收進程!!!)
- 若是unjoinable狀態的線程,這些資源在線程函數退出時或pthread_exit()時自動會被釋放。unjoinable屬性可以在pthread_create()時指定,或在線程創建後在線程中pthread_detach()自己,如:pthread_detach(pthread_self()),將狀態改爲unjoinable狀態,確保資源的釋放。
4.代碼實例
PS:在編譯時注意加上-lpthread參數,以調用靜態鏈接庫。因爲pthread並非Linux系統的默認庫。
/***
Server端
Author:Liang jie
objective:服務端將客戶端輸入的小寫轉換爲大寫,再傳回客戶端。
*/
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/wait.h>
#include<pthread.h>
#define SRV_PORT 10005
#define MAXLINE 8192
#define INET 100
//定義一個結構體,將地址結構和cfd捆綁在一塊
struct s_info {
struct sockaddr_in cliaddr;
int connfd;
};
//線程函數
void *do_work(void *arg){
int n,i;
struct s_info *ts =(struct s_info*)arg;
char buf[MAXLINE];
char str[INET];
while(1){
n=read(ts->connfd,buf,MAXLINE); //讀客戶端
if (n==0){
printf("the client %d has been closed...\n",ts->connfd);
break;
}
printf("receive from %s at PORT %d\n",
inet_ntop(AF_INET,&(*ts).cliaddr.sin_addr.s_addr,str,sizeof(str)),
ntohs((*ts).cliaddr.sin_port)); //打印客戶端信息 ,ip和端口號
for(i=0;i<n;i++)
buf[i]= toupper(buf[i]);
write(ts->connfd,buf,n); //回寫到客戶端
write(STDOUT_FILENO,buf,n); //寫到桌面
printf("\n");
}
close(ts->connfd);
pthread_exit(0);
}
int main(int argc,char *argv[] ){
int lfd,cfd;
pthread_t tid;
int i=0;
struct s_info ts[256]; //創建結構體數組
struct sockaddr_in servaddr,cliaddr;
socklen_t cliaddr_len;
//創建一個socket
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd<0)
{
printf("socket creation failed\n");
exit(-1);
}
//地址結構清0
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SRV_PORT);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
//綁定
if(bind(lfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
printf("Bind error.\n");
exit(-1);
}
//設置同一時刻鏈接服務器上限數
if(listen(lfd,10)==-1)
{
printf("Listen error!\n");
exit(-1);
}
printf("Start to listen!\n");
cliaddr_len=sizeof(cliaddr);
while(1)
{
//阻塞監聽客戶端請求
cfd=accept(lfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
ts[i].cliaddr=cliaddr; //客戶端的地址結構
ts[i].connfd=cfd;
//創建子線程,因爲是隻讀類型,所以最後一個參數可以傳地址
pthread_create(&tid,NULL,do_work,(void *)&ts[i]);
//子線程分離,防止殭屍線程的產生
pthread_detach(tid);
i++;
}
return 0;
}
Client端和上一篇文章一樣,不用改動。
點擊查看~
5.程序運行截圖
使用XShell,鏈接虛擬機,模擬client端。
多個Client端:
Server端:
6.總結
文章從線程的概念開始講解,到創建子線程,然後回收子線程。多線程相比於多進程來說,相對簡單。回收也比較簡單。
下一篇文章將會講解Select()下的socket編程。
文章代碼如有困難,可以聯繫博主~~