轉載:http://blog.csdn.net/qq_29227939/article/details/53782198
上一篇文章使用fork函數實現了多進程并發服務器,但是也提到了一些問題:
- fork是昂貴的。fork時需要復制父進程的所有資源,包括內存映象、描述字等;
- 目前的實現使用了一種寫時拷貝(copy-on-write)技術,可有效避免昂貴的復制問題,但fork仍然是昂貴的;
- fork子進程后,父子進程間、兄弟進程間的通信需要進程間通信IPC機制,給通信帶來了困難;
- 多進程在一定程度上仍然不能有效地利用系統資源;
- 系統中進程個數也有限制。
??下面就介紹實現并發服務器的另外一種方式,使用多線程實現。多線程有助于解決以上問題。
線程基礎
??關于線程的概念就不介紹了,先了解一下linux下線程的一些基本操作。
線程基礎函數
??pthread_create 函數用于創建新線程。當一個程序開始運行時,系統產生一個稱為初始線 程或主線程的單個線程。額外的線程需要由 pthread_create 函數創建。 pthread_create 函數原型如下:
#include <pthread.h>
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *, void *arg);
??如果新線程創建成功,參數 tid 返回新生成的線程 ID。一個進程中的每個線程都由一個 線程 ID 標識,其類型為 pthread_t。attr 指向線程屬性的指針。每個線程有很多屬性包括:優 先級、起始棧大小、是否是守護線程等等。通常將 attr 參數的值設為 NULL,這時使用系統 默認的屬性。?
??但創建完一個新的線程后,需要說明它將執行的函數。函數的地址由參數 func 指定。該函數必須是一個靜態函數,它只有一個通用指針作為參數,并返回一個通用指針。該執行函 數的調用參數是由 arg 指定,arg 是一個通用指針,用于往 func 函數中傳遞參數。如果需要傳遞多個參數時,必須將它們打包成一個結構,然后讓 arg 指向該結構。線程以調用該執行 函數開始。?
??如果函數調用成功返回 0,出錯則返回非 0。
??常見的返回錯誤值:
EAGAIN:超過了系統線程數目的限制。
ENOMEN:沒有足夠的內存產生新的線程。
EINVAL:無效的屬性attr值。
??示例代碼:
#include <pthread.h>
#include <stdio.h>
pthread_t tid;
void *ex()
{printf("this is a thread");
}
void main()
{pthread_create(&tid,NULL,ex,NULL);
}
??給線程傳遞參數:
void *function(void *arg);
struct ARG { int connfd; int other; }; void main() { struct ARG arg; int connfd,sockfd; pthread_t tid; While(1) { if((connfd = accept(sockfd,NULL,NULL))== -1) { } arg.connfd = connfd; if(pthread_create(&tid, NULL, funtion, (void *)&arg)) { } } } void *funtion(void *arg) { struct ARG info; info.connfd = ((struct ARG *)arg) -> connfd; info.other = ((struct ARG *)arg) -> other; close(info.connfd); pthread_exit(NULL); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
??pthread_join 函數與進程的 waitpid 函數功能類似,等待一個線程終止。?
pthread_join 函數原型如下:
#inlcude <pthread.h>
int pthread_join(pthread_t tid, void **status);
??參數 tid 指定所等待的線程 ID。該函數必須指定要等待的線程,不能等待任一個線程結束。要求等待的線程必須是當前進程的成員,并且不是分離的線程或守護線程。?
??幾個線程不 能同時等待一個線程完成,如果其中一個成功調用 pthread_join 函數,則其他線程將返回 ESRCH 錯誤。?
??如果等待的線程已經終止,則該函數立即返回。如果參數 status 指針非空,則 指向終止線程的退出狀態值。?
??該函數如果調用成功則返回 0,出錯時返回正的錯誤碼。
- pthread_detach?
??pthread_detach 函數將指定的線程變成分離的。 pthread_detach 函數原型如下:
#inlcude <pthread.h>
int pthread_detach(pthread_t tid) ;
??參數 tid 指定要設置為分離的線程 ID。
- pthread_self?
??每一個線程都有一個 ID,pthread_self 函數返回自己的線程 ID。 pthread_self 函數原型如下:
#inlcude <pthread.h>
pthread_t pthread_self(void);
??參數 tid 指定要設置為分離的線程 ID。 函數返回調用函數的線程 ID。?
??例如,線程可以通過如下語句,將自己設為可分離的:
pthread_detach(pthread_self());
- pthread_exit?
??函數 pthread_exit 用于終止當前線程,并返回狀態值,如果當前線程是可聯合的,則其 退出狀態將保留。 pthread_exit函數原型如下:
#include <pthread.h>
void pthread_exit(void *status);
??參數 status 指向函數的退出狀態。這里的 status 不能指向一個局部變量,因為當前線程 終止后,其所有局部變量將被撤銷。?
??該函數沒有返回值。?
??還有兩種方法可以使線程終止:
- 啟動線程的函數 pthread_create 的第三個參數返回。該返回值就是線程的終止狀態。
- 如果進程的 main 函數返回或者任何線程調用了 exit 函數,進程將終止,線程將隨之 終止。
??下面可以看一下多線程并發服務器的實例了,需要注意的是,線程建立后,父、子線程不需要關閉任何的描述符,因為線程中使用的描述符是共享進程中的數據。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h> #define PORT 1234
#define BACKLOG 5
#define MAXDATASIZE 1000 void process_cli(int connfd, struct sockaddr_in client);
void *function(void* arg);
struct ARG { int connfd; struct sockaddr_in client;
}; void main()
{ int listenfd,connfd; pthread_t tid; struct ARG *arg; struct sockaddr_in server; struct sockaddr_in client; socklen_t len; if ((listenfd =socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("Creatingsocket failed."); exit(1); } int opt =SO_REUSEADDR; setsockopt(listenfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); bzero(&server,sizeof(server)); server.sin_family=AF_INET; server.sin_port=htons(PORT); server.sin_addr.s_addr= htonl (INADDR_ANY); if (bind(listenfd,(struct sockaddr *)&server, sizeof(server)) == -1) { perror("Bind()error."); exit(1); } if(listen(listenfd,BACKLOG)== -1){ perror("listen()error\n"); exit(1); } len=sizeof(client); while(1) { if ((connfd =accept(listenfd,(struct sockaddr *)&client,&len))==-1) { perror("accept() error\n"); exit(1); } arg = (struct ARG *)malloc(sizeof(struct ARG)); arg->connfd =connfd; memcpy((void*)&arg->client, &client, sizeof(client)); if(pthread_create(&tid, NULL, function, (void*)arg)) { perror("Pthread_create() error"); exit(1); } } close(listenfd);
} void process_cli(int connfd, struct sockaddr_in client)
{ int num; char recvbuf[MAXDATASIZE], sendbuf[MAXDATASIZE], cli_name[MAXDATASIZE]; printf("Yougot a connection from %s. \n ",inet_ntoa(client.sin_addr) ); num = recv(connfd,cli_name, MAXDATASIZE,0); if (num == 0) { close(connfd); printf("Clientdisconnected.\n"); return; } cli_name[num - 1] ='\0'; printf("Client'sname is %s.\n",cli_name); while (num =recv(connfd, recvbuf, MAXDATASIZE,0)) { recvbuf[num] ='\0'; printf("Receivedclient( %s ) message: %s",cli_name, recvbuf); int i; for (i = 0; i <num - 1; i++) { if((recvbuf[i]>='a'&&recvbuf[i]<='z')||(recvbuf[i]>='A'&&recvbuf[i]<='Z')) { recvbuf[i]=recvbuf[i]+ 3; if((recvbuf[i]>'Z'&&recvbuf[i]<='Z'+3)||(recvbuf[i]>'z')) recvbuf[i]=recvbuf[i]- 26; } sendbuf[i] =recvbuf[i]; } sendbuf[num -1] = '\0'; send(connfd,sendbuf,strlen(sendbuf),0); } close(connfd);
} void *function(void* arg)
{ struct ARG *info; info = (struct ARG*)arg; process_cli(info->connfd,info->client); free (arg); pthread_exit(NULL);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
線程安全性
??上面的示例代碼服務器端的業務邏輯都比較簡單,沒有涉及到共享數據產生的同步問題。在某些情況下,我們需要多個線程共享全局數據,在訪問這些數據時就需要用到同步鎖機制。而在共享線程內的全局數據時,可以使用Linux提供的線程特定數據TSD解決。
同步機制
??在linux系統中,提供一種基本的進程同步機制—互斥鎖,可以用來保護線程代碼中共享數據的完整性。?
??操作系統將保證同時只有一個線程能成功完成對一個互斥鎖的加鎖操作。?
??如果一個線程已經對某一互斥鎖進行了加鎖,其他線程只有等待該線程完成對這一互斥鎖解鎖后,才能完成加鎖操作。
互斥鎖函數
pthread_mutex_lock(pthread_mutex_t *mptr);
參數說明:
mptr:指向互斥鎖的指針。
該函數接受一個指向互斥鎖的指針作為參數并將其鎖定。如果互斥鎖已經被鎖定,調用者將進入睡眠狀態。函數返回時,將喚醒調用者。
如果互斥鎖是靜態分配的,就將mptr初始化為常值PTHREAD_MUTEX_INITIALIZER。
??鎖定成功返回0,否則返回錯誤碼。
pthread_mutex_unlock(pthread_mutex_t *mptr);
??用于互斥鎖解鎖操作。成功返回0,否則返回錯誤碼。
示例代碼:
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int myglobal;
pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;
void *thread_function(void *arg) {int i, j;for (i = 0; i < 5; i++) {pthread_mutex_lock(&mymutex);j = myglobal;j = j + 1;printf(".");fflush(stdout);sleep(1);myglobal = j;pthread_mutex_unlock(&mymutex);}return NULL;
}
int main(void) {pthread_t mythread;int i;if (pthread_create(&mythread, NULL, thread_function, NULL)) {printf("error creating thread.");abort();}for (i = 0; i < 5; i++) {pthread_mutex_lock(&mymutex);myglobal = myglobal + 1;pthread_mutex_unlock(&mymutex);printf("o");fflush(stdout);sleep(1);}if (pthread_join(mythread, NULL)) {printf("error joining thread.");abort();}printf("\nmyglobal equals %d\n", myglobal);exit(0);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42

線程私有數據
??在多線程環境里,應避免使用靜態變量。在 Linux 系統中提供 了線程特定數據(TSD)來取代靜態變量。它類似于全局變量,但是,是各個線程私有的, 它以線程為界限。TSD 是定義線程私有數據的惟一方法。同一進程中的所有線程,它們的同 一特定數據項都由一個進程內惟一的關鍵字 KEY 來標志。用這個關鍵字,線程可以存取線程私有數據。 在線程特定數據中通常使用四個函數。
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (* destructor)(void *value));
??pthread_key_create 函數在進程內部分配一個標志 TSD 的關鍵字。?
??參數 key 指向創建的關 鍵字,該關鍵字對于一個進程中的所有線程是惟一的。所以在創建 key 時,每個進程只能調 用一次創建函數 pthread_key_create。在 key 創建之前,所有線程的關鍵字值是 NULL。一旦 關鍵字被建立,每個線程可以為該關鍵字綁定一個值。這個綁定的值對于線程是惟一的,每 個線程獨立維護。?
?? 參數 destructor 是一個可選的析構函數,可以和每個關鍵字聯系起來。如果一個關鍵字 的 destructor 函數不為空,且線程為該關鍵字綁定了一個非空值,那么在線程退出時,析構函 數將會被調用。對于所有關鍵字的析構函數,執行順序是不能指定的。?
??該函數正常執行后返回值為 0,否則返回錯誤碼。
#include <pthread.h>
int pthread_once(pthread_once_t *once, void (*init) (void));
??pthread_once 函數使用 once 參數所指的變量,保證每個進程只調用一次 init 函數。通常 once 參數取常量 PTHREAD_ONCE_INIT,它保證每個進程只調用一次 init 函數。?
??該函數正常執行后返回值為 0,否則返回錯誤碼。
#include <pthread.h>
int pthread_setspecific(pthread_key_t key, const void *value);
??pthread_setspecific 函數為 TSD 關鍵字綁定一個與本線程相關的值。?
??參數 key 是 TSD 關 鍵字。?
??參數 value 是與本線程相關的值。value 通常指向動態分配的內存區域。?
??該函數正常執行后返回值為 0,否則返回錯誤碼。
#include <pthread.h>
void * pthread_getspecific(pthread_key_t key);
??pthread_getspecific 函數獲取與調用線程相關的 TSD 關鍵字所綁定的值。?
??參數 key 是 TSD 關鍵字。?
??該函數正常執行后返回與調用線程相關的 TSD 關鍵字所綁定的值。否則返回 NULL。
??線程安全性代碼示例:
pthread_key_t key;
void echomsg(int t)
{printf("destructor excuted in thread %d,param=%d\n", pthread_self(), t);
}
void * child1(void *arg)
{int tid = pthread_self();printf("thread1 %d enter\n", tid);pthread_setspecific(key, (void *)tid);sleep(2);printf("thread1 %d key’s %d\n", tid, pthread_getspecific(key));sleep(5);
}
void * child2(void *arg)
{int tid = pthread_self();printf("thread2 %d enter\n", tid);pthread_setspecific(key, (void *)tid);sleep(1);printf("thread2 %d key’s %d\n", tid, pthread_getspecific(key));sleep(5);
}
int main(void)
{pthread_t tid1, tid2;printf("hello\n");pthread_key_create(&key, (void *)echomsg);pthread_create(&tid1, NULL, child1, NULL);pthread_create(&tid2, NULL, child2, NULL);sleep(10);pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_key_delete(key);printf("main thread exit\n");return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40

小結
??使用多線程實現并發服務器的優點是線程的開銷小,切換容易。但是由于線程共享相同 的內存區域,所以在對共享數據的進行操作時,要注意同步問題。其中線程特定數據雖然實現起來比較煩瑣,但是它是將一個非線程安全 函數轉換成線程安全函數的常用方法。?
??除此之外,還可以通過改變調用函數參變量的方式實現線程的安全性,這里不作介紹。?
??下一篇文章將介紹另外一種實現并發服務器的方法:I/O 多路復用。