1. 程式人生 > >linux多執行緒伺服器

linux多執行緒伺服器

上一篇文章使用fork函式實現了多程序併發伺服器,但是也提到了一些問題:

  1. fork是昂貴的。fork時需要複製父程序的所有資源,包括記憶體映象、描述字等;
  2. 目前的實現使用了一種寫時拷貝(copy-on-write)技術,可有效避免昂貴的複製問題,但fork仍然是昂貴的;
  3. fork子程序後,父子程序間、兄弟程序間的通訊需要程序間通訊IPC機制,給通訊帶來了困難;
  4. 多程序在一定程度上仍然不能有效地利用系統資源;
  5. 系統中程序個數也有限制。

  下面就介紹實現併發伺服器的另外一種方式,使用多執行緒實現。多執行緒有助於解決以上問題。

執行緒基礎

  關於執行緒的概念就不介紹了,先了解一下linux下執行緒的一些基本操作。

執行緒基礎函式

  • pthread_create 建立執行緒

  pthread_create 函式用於建立新執行緒。當一個程式開始執行時,系統產生一個稱為初始線 程或主執行緒的單個執行緒。額外的執行緒需要由 pthread_create 函式建立。 pthread_create 函式原型如下:

#include <pthread.h> 
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func)(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; //other data }; void main() { struct ARG arg; int connfd,sockfd; pthread_t tid; //... While(1) { if((connfd = accept(sockfd,NULL,NULL))== -1) { //handle exception } arg.connfd = connfd; if(pthread_create(&tid, NULL, funtion, (void *)&arg)) { // handle exception } } } 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); }
  • pthread_join 
      看這個函式首先提出一個概念,執行緒的型別。執行緒分為兩類:可聯合的和分離的。

    1. 預設情況下執行緒都是可聯合的。可聯合的執行緒終止 時,其執行緒 ID 和終止狀態將保留,直到執行緒呼叫 pthread_join 函式。
    2. 而分離的執行緒退出後, 系統將釋放其所有資源,其他執行緒不能等待其終止。如果一個執行緒需要知道另一個執行緒什麼 時候終止,最好保留第二個執行緒的可聯合性。

  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 不能指向一個區域性變數,因為當前執行緒 終止後,其所有區域性變數將被撤銷。 
  該函式沒有返回值。 
  還有兩種方法可以使執行緒終止:

  1. 啟動執行緒的函式 pthread_create 的第三個引數返回。該返回值就是執行緒的終止狀態。
  2. 如果程序的 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);  
}  

執行緒安全性

  上面的示例程式碼伺服器端的業務邏輯都比較簡單,沒有涉及到共享資料產生的同步問題。在某些情況下,我們需要多個執行緒共享全域性資料,在訪問這些資料時就需要用到同步鎖機制。而在共享執行緒內的全域性資料時,可以使用Linux提供的執行緒特定資料TSD解決。

同步機制

  在linux系統中,提供一種基本的程序同步機制—互斥鎖,可以用來保護執行緒程式碼中共享資料的完整性。 
  作業系統將保證同時只有一個執行緒能成功完成對一個互斥鎖的加鎖操作。 
  如果一個執行緒已經對某一互斥鎖進行了加鎖,其他執行緒只有等待該執行緒完成對這一互斥鎖解鎖後,才能完成加鎖操作。

互斥鎖函式

pthread_mutex_lock(pthread_mutex_t  *mptr)

引數說明:

mptr:指向互斥鎖的指標。
該函式接受一個指向互斥鎖的指標作為引數並將其鎖定。如果互斥鎖已經被鎖定,呼叫者將進入睡眠狀態。函式返回時,將喚醒呼叫者。
如果互斥鎖是靜態分配的,就將mptr初始化為常值PTHREAD_MUTEX_INITIALIZER。

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);
}

執行效果

執行緒私有資料

  在多執行緒環境裡,應避免使用靜態變數。在 Linux 系統中提供 了執行緒特定資料(TSD)來取代靜態變數。它類似於全域性變數,但是,是各個執行緒私有的, 它以執行緒為界限。TSD 是定義執行緒私有資料的惟一方法。同一程序中的所有執行緒,它們的同 一特定資料項都由一個程序內惟一的關鍵字 KEY 來標誌。用這個關鍵字,執行緒可以存取執行緒私有資料。 線上程特定資料中通常使用四個函式。

  • pthread_key_create
#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,否則返回錯誤碼。

  • pthread_once
#include <pthread.h> 
int pthread_once(pthread_once_t *once, void (*init) (void)); 

  pthread_once 函式使用 once 引數所指的變數,保證每個程序只調用一次 init 函式。通常 once 引數取常量 PTHREAD_ONCE_INIT,它保證每個程序只調用一次 init 函式。 
  該函式正常執行後返回值為 0,否則返回錯誤碼。

  • pthread_setspecific
#include <pthread.h> 
int pthread_setspecific(pthread_key_t key, const void *value);

  pthread_setspecific 函式為 TSD 關鍵字繫結一個與本執行緒相關的值。 
  引數 key 是 TSD 關 鍵字。 
  引數 value 是與本執行緒相關的值。value 通常指向動態分配的記憶體區域。 
  該函式正常執行後返回值為 0,否則返回錯誤碼。

  • pthread_getspecific
#include <pthread.h> 
void * pthread_getspecific(pthread_key_t key); 

  pthread_getspecific 函式獲取與呼叫執行緒相關的 TSD 關鍵字所繫結的值。 
  引數 key 是 TSD 關鍵字。 
  該函式正常執行後返回與呼叫執行緒相關的 TSD 關鍵字所繫結的值。否則返回 NULL。

  執行緒安全性程式碼示例:

#include <stdio.h>
#include <pthread.h>
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;
}

執行結果

小結

  使用多執行緒實現併發伺服器的優點是執行緒的開銷小,切換容易。但是由於執行緒共享相同 的記憶體區域,所以在對共享資料的進行操作時,要注意同步問題。其中執行緒特定資料雖然實現起來比較煩瑣,但是它是將一個非執行緒安全 函式轉換成執行緒安全函式的常用方法。 
  除此之外,還可以通過改變呼叫函式參變數的方式實現執行緒的安全性,這裡不作介紹。 
  下一篇文章將介紹另外一種實現併發伺服器的方法:I/O 多路複用。

相關推薦

Linux執行伺服器-門禁打卡系統

原始碼地址 系統採用一個伺服器+兩種客戶端(網頁+APP),執行在樹莓派2上 OpenDoorMultiThreadServer OpenDoorMultiThreadServer 實驗室門禁打卡系統 1、mydb是操作資料庫Mysql類,表示每個

linux執行伺服器

上一篇文章使用fork函式實現了多程序併發伺服器,但是也提到了一些問題:fork是昂貴的。fork時需要複製父程序的所有資源,包括記憶體映象、描述字等;目前的實現使用了一種寫時拷貝(copy-on-write)技術,可有效避免昂貴的複製問題,但fork仍然是昂貴的;fork子

賴勇浩:推薦《Linux 執行伺服器端程式設計》

推薦《Linux 多執行緒伺服器端程式設計》 賴勇浩(http://laiyonghao.com)最近,有一位朋友因為工作需要,需要從網遊的客戶端程式設計轉向伺服器端程式設計,找我推薦一本書。我推薦了《Linux 多執行緒伺服器端程式設計——使用 muduo C++ 網路庫

Linux執行伺服器端程式設計

目錄 Linux多執行緒伺服器端程式設計 執行緒安全的物件生命期管理 物件的銷燬執行緒比較難 執行緒同步精要 借shared_ptr實現寫時拷貝(copy-on-write)

TCP/IP網路程式設計 基於Linux程式設計_4 --執行伺服器端的實現

執行緒基本概念 前面我們講過多程序伺服器,但我們知道它開銷很大,因此我們才引入執行緒,我們可以把它看成是一種輕量級程序。它相比程序有如下幾個優點: 執行緒的建立和上下文切換開銷更小且速度更快。 執行緒間交換資料時無需特殊技術。 程序:在作業系統構成

linux tcp執行伺服器與客戶端程式設計例項

伺服器端: #include<iostream> #include<arpa/inet.h> #include<sys/socket.h> #include<cstdlib> #include<cstdio> #i

linux c++執行池的實現(執行伺服器

        本文給出了一個通用的執行緒池框架,該框架將與執行緒執行相關的任務進行了高層次的抽象,使之與具體的執行任務無關。另外該執行緒池具有動態伸縮性,它能根據執行 任務的輕重自動調整執行緒池中執行緒的數量。文章的最後,我們給出一個簡單示例程式,通過該示例程式,我們會發

Linux執行程式設計---執行間同步(互斥鎖、條件變數、訊號量和讀寫鎖)

本篇博文轉自http://zhangxiaoya.github.io/2015/05/15/multi-thread-of-c-program-language-on-linux/ Linux下提供了多種方式來處理執行緒同步,最常用的是互斥鎖、條件變數、訊號量和讀寫鎖。  下面是思維導

LINUX 執行 JNI 回撥 java static

1.Linux 開啟執行緒 //渲染執行緒Rendering void* thread_rendering_process(void *lParam) {     unsigned int local_wr;     int index; &

linux 執行之訊號量 sem_init

1. 什麼是訊號量 linux sem 訊號量是一種特殊的變數,訪問具有原子性, 用於解決程序或執行緒間共享資源引發的同步問題。 使用者態程序對 sem 訊號量可以有以下兩種操作: 等待訊號量 當訊號量值為 0 時,程式等待;當訊號量值大於 0 時,訊號量減 1,程式

Linux執行學習總結

原文:https://www.cnblogs.com/luoxn28/p/6087649.html Linux多執行緒學習總結   執行緒是程式中完成一個獨立任務的完整執行序列,即一個可排程的實體;程序相當於執行中程式的一種抽象。根據執行環境的排程者的身份,執行緒可分為核心執行緒和使用者執行

Linux執行/客戶程式設計參考/程式碼

Linux多執行緒/多客戶程式設計參考/程式碼 (1)linux多執行緒程式設計例項及講解 https://blog.csdn.net/m0_37051593/article/details/80719469 (2)Linux多執行緒程式設計——多執行緒與執行緒同步 https://

Qt TCP通訊,執行伺服器

相信許多初學Qt的同學都會和我一樣遇到這樣的問題: 一、Qt TCP通訊在使用nextPendingConnect後,伺服器端就只會與最後接入的客戶端通訊,這個時候就會考慮繼承QThread實現多執行緒,從而實現多個客戶端與伺服器端通訊,每當一個新的客戶端連線時,通過標識碼socke

用 threading 寫執行伺服器

import socket import threading   server = socket.socket() server.bind(("127.0.0.1",8899)) server.listen(1000)   def func(conn):   while T

Linux執行同步機制

一、互斥鎖 儘管在Posix Thread中同樣可以使用IPC的訊號量機制來實現互斥鎖mutex功能,但顯然semphore的功能過於強大了,在Posix Thread中定義了另外一套專門用於執行緒同步的mutex函式。 1. 建立和銷燬    有兩種方法建

Linux 執行程序的區別(小結)

最近學習Linux,看到“hairetz的專欄”的帖子不錯,特轉來大家一起學習。 很想寫點關於多程序和多執行緒的東西,我確實很愛他們。但是每每想動手寫點關於他們的東西,卻總是求全心理作祟,始終動不了手。 今天終於下了決心,寫點東西,以後可以再修修補補也無妨。一.為何需要多程序(或者多執行緒),為何需

linux執行執行資料TSD Thread Specific Data

在linux中,同一程序下所有執行緒是共享全域性變數的,但有時候會有這樣的需求,某一個執行緒想單獨用於某個全域性變數。這就有了TSD,Thread Specific Data。 使用TSD時需要建立一個全域性的鍵值,每個執行緒都通過鍵值來設定和獲取自己所獨有的全域性變數。 使用TSD分為

執行伺服器

#coding=utf-8 from socket import * from threading import Thread from time import sleep # 處理客戶端的請求並執行事情 def dealWithClient(newSocket,destAddr):

OS:(Linux)執行實現生產者-消費者問題--pthread庫

OS實驗——多執行緒實現生產者-消費者問題時,正確輸入程式碼生成檔案pthread.c,在終端執行: gcc編譯:輸入gcc -o pthread pthread.c 無法成功編譯,錯誤提示如下:   CSDN查詢解決方案後,發現pthread庫並不是Linux系統預設的庫,連結時需要使

Qt 執行伺服器與客戶端

文章目錄 思路 伺服器 myserver.h myserver.cpp mythread.h mythread.cpp mysocket.h mysocket.cpp