TCP/IP網絡編程之多線程服務端的實現(一)

爲何引入線程編程

爲了實現服務端併發處理客戶端請求,咱們介紹了多進程模型、select和epoll,這三種辦法各有優缺點。建立(複製)進程的工做自己會給操做系統帶來至關沉重的負擔。並且,每一個進程有獨立的內存空間,因此進程間通訊的實現難度也會隨之提升。且進程的切換一樣也是不菲的開銷。什麼是進程切換?咱們都知道計算機即使只有一個CPU也能夠同時運行多個進程,這是由於系統將CPU時間分紅多個微小的塊後分配給多個進程,比方進程B在進程A以後執行,當進程A所分配的CPU時間到點以後,要開始執行進程B,此時須要將進程A的數據移出內存保存到磁盤,並讀入進程B的數據,因此上下文切換須要比較長的時間,即便經過優化加快速度,也會存在侷限安全

爲了保持多進程的優勢,同時在必定程度上克服其缺點,人們引入了線程。這是爲了將進程的各類劣勢降至最低限度而設計的一種「輕量級進程」,線程相比進程有以下優勢:bash

  • 線程的建立和上下文切換比進程的建立和上下文切換更快
  • 線程間交換數據時無需特殊技術

線程和進程的差別併發

每一個進程的內存空間都由保存全局變量的「數據區」、向malloc等函數動態分配提供空間的堆(Heap)、函數運行時使用的棧(Stack)構成。每一個進程都擁有這種獨立的空間,多個進程結構如圖1-1所示函數

圖1-1   進程間獨立的內存優化

但若是以得到多個代碼執行流爲主要目的,則不該像圖1-1那樣徹底分離內存結構,而只需分離棧區域,經過這種方式能夠得到以下優點:spa

  • 上下文切換時不須要切換數據區和堆
  • 能夠利用數據區和堆交換數據

實際上這就是線程,線程爲了保持多條代碼執行流而隔開了棧區域,所以具備如圖1-2所示的內存結構操作系統

圖1-2   線程的內存結構線程

如圖1-2所示,多個線程將共享數據區和堆,爲了保持這種結構,線程將在進程內建立並運行。也就是說,進程和線程能夠定義爲以下形式:設計

  • 進程:在操做系統構成單獨執行流的單位
  • 線程:在進程構成單獨執行流的單位

若是說進程在操做系統內部生成多個執行流,那麼線程就在同一進程內部建立多條執行流。所以,操做系統、進程、線程之間的關係能夠經過圖1-3表示

圖1-3   操做系統、進程、線程之間的關係

線程的建立及運行

線程具備單獨的執行流,所以須要單獨定義線程的main函數,還須要請求操做系統在單獨的執行流中執行該函數,完成該功能的函數以下:

#include<pthread.h>
int pthread_create(pthread_t * restrict thread, const pthread_attr_t * restrict attr, void* (* start_routine)(void *), void * restrict arg);//成功時返回0,失敗時返回其餘值

  

  • thread:保存新建立線程ID的變量地址值,線程與進程相同,也須要用於區分不一樣線程的ID
  • attr:用於傳遞線程屬性的參數,傳遞NULL時,建立默認屬性的線程
  • start_routine:至關於線程的main函數的、在單獨執行流中執行的函數地址值(函數指針)
  • arg:經過第三個參數傳遞調用函數時包含傳遞參數信息的變量地址值

下面,咱們來看一個示例

thread1.c  

#include <stdio.h>
#include <pthread.h>
void *thread_main(void *arg);

int main(int argc, char *argv[])
{
    pthread_t t_id;
    int thread_param = 5;

    if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0)
    {
        puts("pthread_create() error");
        return -1;
    };
    sleep(10); puts("end of main");
    return 0;
}

void *thread_main(void *arg)
{
    int i;
    int cnt = *((int *)arg);
    for (i = 0; i < cnt; i++)
    {
        sleep(1); puts("running thread");
    }
    return NULL;
}

  

  •  第10行:請求建立一個線程,從thread_main函數調用開始,在單獨的執行流中執行。同時在調用thread_main函數時向其傳遞thread_param變量的地址值
  • 第15行:調用sleep函數使main函數停頓10秒,這是爲了延遲進程的終止時間。執行第16行的return語句後終止進程,同時終止內部建立的線程。所以,爲保證線程的正常執行而添加這條語句
  • 第1九、22行:傳入arg參數的是第10行pthread_create函數的第四個參數 

編譯thread1.c並運行

# gcc thread1.c -o thread1 -lpthread
# ./thread1 
running thread
running thread
running thread
running thread
running thread
end of main

  

從上述運行結果能夠看到,線程相關代碼在編譯時需添加-lpthread選項聲明須要鏈接線程庫,只有這樣才能調用頭文件pthread.h中聲明的函數,上述程序的執行流程如圖1-4所示

圖1-4   示例thread1.c的執行流程

圖1-4中的虛線表明執行流程,向下的箭頭指的是執行流,橫向箭頭是函數調用。

接下來,能夠嘗試將上述示例的第15行sleep函數的調用語句改成sleep(2)。運行以後你們會發現不會再像以前那樣打印5次"running thread"字符串。由於main函數返回後整個進程將被銷燬,如圖1-5所示

圖1-5   終止進程和線程

正因如此,咱們以前的示例中經過調用sleep函數向線程提供了充足的時間 

那麼,若是咱們但願等線程執行完畢,再結束程序,是否是必定要調用sleep函數?若是是,那麼又牽扯出一個問題了,線程是在什麼時候執行完畢呢?並不是全部的程序都像thread1.c同樣可預測線程的執行時間。那麼,爲了等待線程執行完畢,難道咱們要用一個很是大的數做爲sleep的參數嗎?那這樣就算線程能夠執行完,程序依然在休眠,形成計算機資源的浪費是必定的。那麼,針對這一困境,是否有解決方案呢?固然是有的,那就是pthread_join函數 

#include <pthread.h>
int pthread_join(pthread_t thread, void ** status);//成功時返回0,失敗時返回其餘值

  

  • thread: thread所對應的線程終止後纔會從pthread_join函數返回,換言之調用該函數後當前線程會一直阻塞到thread對應的線程執行完畢後才返回
  • status:保存線程的main函數返回值的指針變量地址值

thread2.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void *thread_main(void *arg);

int main(int argc, char *argv[])
{
    pthread_t t_id;
    int thread_param = 5;
    void *thr_ret;

    if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0)
    {
        puts("pthread_create() error");
        return -1;
    };

    if (pthread_join(t_id, &thr_ret) != 0)
    {
        puts("pthread_join() error");
        return -1;
    };

    printf("Thread return message: %s \n", (char *)thr_ret);
    free(thr_ret);
    return 0;
}

void *thread_main(void *arg)
{
    int i;
    int cnt = *((int *)arg);
    char *msg = (char *)malloc(sizeof(char) * 50);
    strcpy(msg, "Hello, I'am thread~ \n");

    for (i = 0; i < cnt; i++)
    {
        sleep(1); puts("running thread");
    }
    return (void *)msg;
}

  

  • 第19行:main函數中,針對第13行建立的線程調用pthread_join函數,所以,main函數將等待ID保存在t_id變量中的線程終止
  • 第十一、1九、41行:第41行返回的值將保存到第19行第二個參數thr_ret。須要注意的是,該返回值是thread_main函數內部動態分配的內存空間地址值

編譯thread2.c並運行

# gcc thread2.c -o thread2 -lpthread
# ./thread2 
running thread
running thread
running thread
running thread
running thread
Thread return message: Hello, I'am thread~ 

  

接下來咱們來看thread2.c的執行流程圖,如圖1-6所示

 

圖1-6   調用pthread_join函數

可在臨界區內調用的函數

以前的示例只建立一個線程,接下來的示例將建立多個線程。固然,不管建立多少個線程,其建立方法沒有區別。但關於線程的運行須要考慮「多個線程同時調用函數時(執行時)可能產生的問題」。這類函數內部存在臨界區,也就是說,多個線程同時執行這部分代碼時,可能引發問題。根據臨界區是否引發問題,函數可分爲兩類:

  • 線程安全函數
  • 非線程安全函數

線程安全函數被多個線程同時調用不會發生問題,反之,非線程安全函數被調用時就會出現問題。

下面咱們介紹一個示例,將計算1到10的和,但並非在main函數中計算,而是建立兩個線程,其中一個線程計算1到5的和,另外一個線程計算6到10的和,main函數只負責輸出結果。這種方式的編程模型稱爲「工做線程模型」。計算1到5之和與計算6到10之和的線程將成爲main線程管理的工做。最後,在給出示例代碼以前先給出程序執行流程圖,如圖1-7所示

圖1-7   示例thread3.c的執行流程

thread3.c 

#include <stdio.h>
#include <pthread.h>
void *thread_summation(void *arg);
int sum = 0;

int main(int argc, char *argv[])
{
    pthread_t id_t1, id_t2;
    int range1[] = {1, 5};
    int range2[] = {6, 10};

    pthread_create(&id_t1, NULL, thread_summation, (void *)range1);
    pthread_create(&id_t2, NULL, thread_summation, (void *)range2);

    pthread_join(id_t1, NULL);
    pthread_join(id_t2, NULL);
    printf("result: %d \n", sum);
    return 0;
}

void *thread_summation(void *arg)
{
    int start = ((int *)arg)[0];
    int end = ((int *)arg)[1];

    while (start <= end)
    {
        sum += start;
        start++;
    }
    return NULL;
}

  

這裏要注意一下,兩個線程都訪問全局變量sum

編譯thread3.c 並運行

# gcc thread3.c -o thread3 -lpthread
# ./thread3 
result: 55 

  

運行結果是55,雖然正確,但示例自己存在問題。此處存在臨界區相關問題,所以再介紹另外一示例,該示例與上述示例類似,只是增長了發生臨界區相關錯誤的可能性

thread4.c 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREAD 100

void *thread_inc(void *arg);
void *thread_des(void *arg);
long long num = 0;

int main(int argc, char *argv[])
{
    pthread_t thread_id[NUM_THREAD];
    int i;

    printf("sizeof long long: %d \n", sizeof(long long));
    for (i = 0; i < NUM_THREAD; i++)
    {
        if (i % 2)
            pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
        else
            pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
    }

    for (i = 0; i < NUM_THREAD; i++)
        pthread_join(thread_id[i], NULL);

    printf("result: %lld \n", num);
    return 0;
}

void *thread_inc(void *arg)
{
    int i;
    for (i = 0; i < 50000000; i++)
        num += 1;
    return NULL;
}
void *thread_des(void *arg)
{
    int i;
    for (i = 0; i < 50000000; i++)
        num -= 1;
    return NULL;
}

  

上述示例共建立100個線程,其中一半執行thread_inc函數中的代碼,另外一半則執行thread_des函數中的代碼,全局變量sum通過增減後的值應仍是0,可是,咱們在編譯執行下程序 

# gcc thread4.c -o thread4 -lpthread
# ./thread4 
sizeof long long: 8 
result: 10862532 

  

能夠看到,結果並不是咱們預想的那樣。雖然暫時不清楚緣由,但能夠確定,冒然使用線程對變量進行操做,是有可能發生問題的。那麼,這是什麼問題?如何解決,咱們會在後面的一章介紹

相關文章
相關標籤/搜索