linux網路編程之posix 線程(一):pthread 系列函數 和 簡單多線程伺服器端程式

來源:互聯網
上載者:User

一、posix 線程概述

我們知道,進程在各自獨立的地址空間中運行,進程之間共用資料需要用處理序間通訊機制,有些情況需要在一個進程中同時執行多個控制流程程,這時候線程就派上了用場,比如實現一個圖形介面的下載軟體,一方面需要和使用者互動,等待和處理使用者的滑鼠鍵盤事件,另一方面又需要同時下載多個檔案,等待和處理從多個網路主機發來的資料,這些任務都需要一個“等待-處理”的迴圈,可以用多線程實現,一個線程專門負責與使用者互動,另外幾個線程每個線程負責和一個網路主機通訊。


以前我們講過,main函數和訊號處理函數是同一個進程地址空間中的多個控制流程程,多線程也是如此,但是比訊號處理函數更加靈活,訊號處理函數的控制流程程只是在訊號遞達時產生,在處理完訊號之後就結束,而多線程的控制流程程可以長期並存,作業系統會在各線程之間調度和切換,就像在多個進程之間調度和切換一樣。由於同一進程的多個線程共用同一地址空間,因此TextSegment、Data Segment都是共用的,如果定義一個函數,在各線程中都可以調用,如果定義一個全域變數,在各線程中都可以訪問到,除此之外,各線程還共用以下進程資源和環境:


檔案描述符表

每種訊號的處理方式(SIG_IGN、SIG_DFL或者自訂的訊號處理函數)

當前工作目錄使用者id和組id


但有些資源是每個線程各有一份的:

線程id

上下文,包括各種寄存器的值、程式計數器和棧指標

棧空間

errno變數

訊號屏蔽字

調度優先順序


我們將要學習的線程庫函數是由POSIX標準定義的,稱為POSIX thread或者pthread。在Linux上線程函數位於libpthread共用庫中,因此在編譯時間要加上-lpthread選項。


二、pthread 系列函數

(一)

功能:建立一個新的線程
原型 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
參數
thread:返回線程ID
attr:設定線程的屬性,attr為NULL表示使用預設屬性
start_routine:是個函數地址,線程啟動後要執行的函數
arg:傳給線程啟動函數的參數
傳回值:成功返回0;失敗返回錯誤碼


錯誤檢查:

以前學過的系統函數都是成功返回0,失敗返回-1,而錯誤號碼儲存在全域變數errno中,而pthread庫的函數都是通過傳回值返回錯誤號碼,雖然每個線程也都有一個errno,但這是為了相容其它函數介面而提供的,pthread庫本身並不使用它,通過傳回值返回錯誤碼更加清晰。由於pthread_create的錯誤碼不儲存在errno中,因此不能直接用perror(3)列印錯誤資訊,可以先用strerror(3)把錯誤碼轉換成錯誤資訊再列印。


(二)

功能:線程終止
原型 void pthread_exit(void *value_ptr);
參數
value_ptr:value_ptr不要指向一個局部變數,因為當其它線程得到這個返回指標時線程函數已經退出了。
傳回值:無傳回值,跟進程一樣,線程結束的時候無法返回到它的調用者(自身)


如果需要只終止某個線程而不終止整個進程,可以有三種方法:

1、從線程函數return。這種方法對主線程不適用,從main函數return相當於調用exit,而如果任意一個線程調用了exit或_exit,則整個進程的所有線程都終止。

2、一個線程可以調用pthread_cancel 終止同一進程中的另一個線程。

3、線程可以調用pthread_exit終止自己。


(三)

功能:等待線程結束
原型 int pthread_join(pthread_t thread, void **value_ptr);
參數
thread:線程ID
value_ptr:它指向一個指標,後者指向線程的傳回值
傳回值:成功返回0;失敗返回錯誤碼


當pthread_create 中的 start_routine返回時,這個線程就退出了,其它線程可以調用pthread_join得到start_routine的傳回值,類似於父進程調用wait(2)得到子進程的退出狀態。

調用該函數的線程將掛起等待,直到id為thread的線程終止。thread線程以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:

1、如果thread線程通過return返回,value_ptr所指向的單元裡存放的是thread線程函數的傳回值。

2、如果thread線程被別的線程調用pthread_cancel異常終止掉,value_ptr所指向的單元裡存放的是常數PTHREAD_CANCELED。

3、如果thread線程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。

如果對thread線程的終止狀態不感興趣,可以傳NULL給value_ptr參數。


(四)

功能:返回線程ID
原型 pthread_t pthread_self(void);
傳回值:成功返回0

在Linux上,pthread_t類型是一個地址值,屬於同一進程的多個線程調用getpid(2)可以得到相同的進程號,而調用pthread_self(3)得到的線程號各不相同。線程id只在當前進程中保證是唯一的,在不同的系統中pthread_t這個類型有不同的實現,它可能是一個整數值,也可能是一個結構體,也可能是一個地址,所以不能簡單地當成整數用printf列印。


(五)

功能:取消一個執行中的線程
原型 int pthread_cancel(pthread_t thread);
參數
thread:線程ID
傳回值:成功返回0;失敗返回錯誤碼

一個新建立的線程預設取消狀態(cancelability state)是可取消的,取消類型( cancelability type)是同步的,即在某個可取消點( cancellation point,即在執行某些函數的時候)才會取消線程。具體可以man 一下。

相關函數 int pthread_setcancelstate(int state, int *oldstate);  int pthread_setcanceltype(int type, int *oldtype);


(六)

功能:將一個線程分離
原型 int pthread_detach(pthread_t thread);
參數
thread:線程ID
傳回值:成功返回0;失敗返回錯誤碼

一般情況下,線程終止後,其終止狀態一直保留到其它線程調用pthread_join擷取它的狀態為止(僵線程)。但是線程也可以被置為detach狀態,這樣的線程一旦終止就立刻回收它佔用的所有資源,而不保留終止狀態。不能對一個已經處於detach狀態的線程調用pthread_join,這樣的調用將返回EINVAL。對一個尚未detach的線程調用pthread_join或pthread_detach都可以把該線程置為detach狀態,也就是說,不能對同一線程調用兩次pthread_join,或者如果已經對一個線程調用了pthread_detach就不能再調用pthread_join了。


下面寫個程式走一下這些函數:

 C++ Code 
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
#include<stdio.h>
#include<stdlib.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<sys/types.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<string.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

void *routine(void *arg)
{
    int i;
    for (i = 0; i < 20; i++)
    {
        printf("B");
        fflush(stdout);
        usleep(20);
        /*
            if (i == 3)
                pthread_exit("ABC");
            */
    }
    return "DEF";
}

int main(void)
{
    pthread_t tid;
    int ret;
    if ((ret = pthread_create(&tid, NULL, routine, NULL)) != 0)
    {
        fprintf(stderr, "pthread create: %s\n", strerror(ret));
        exit(EXIT_FAILURE);
    }

    int i;
    for (i = 0; i < 20; i++)
    {
        printf("A");
        fflush(stdout);
        usleep(20);
    }

    void *value;
    if ((ret = pthread_join(tid, &value)) != 0)
    {
        fprintf(stderr, "pthread create: %s\n", strerror(ret));
        exit(EXIT_FAILURE);
    }

    printf("\n");

    printf("return msg=%s\n", (char *)value);
    return 0;
}

建立一個線程,主線程列印A,新線程列印B,主線程調用pthread_join 等待新線程退出,列印退出值。

simba@ubuntu:~/Documents/code/linux_programming/UNP/pthread$ ./pthread_create 
ABAABABABABABABABABABABABABAABABBABABABB
return msg=DEF

在新線程中也可調用pthread_exit 退出。


三、簡單的多線程伺服器端程式

在將socket 編程的時候曾經使用fork 多進程的方式來實現並發,現在嘗試使用多線程方式來實現:

 C++ Code 
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
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void echo_srv(int conn)
{
    char recvbuf[1024];
    while (1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = read(conn, recvbuf, sizeof(recvbuf));
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        else if (ret == -1)
            ERR_EXIT("read");
        fputs(recvbuf, stdout);
        write(conn, recvbuf, ret);
    }
}

void *thread_routine(void *arg)
{
    /* 主線程沒有調用pthread_join等待線程退出 */
    pthread_detach(pthread_self()); //剝離線程,避免產生僵線程
    /*int conn = (int)arg;*/
    int conn = *((int *)arg);
    free(arg);
    echo_srv(conn);
    printf("exiting thread ...\n");
    return NULL;
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;

    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pthread_t tid;
        //      int ret;
        /*pthread_create(&tid, NULL, thread_routine, (void*)&conn);*/ // race condition問題,竟態問題
        int *p = malloc(sizeof(int));
        *p = conn;
        pthread_create(&tid, NULL, thread_routine, p);
        /*
                if ((ret = pthread_create(&tid, NULL, thread_routine, (void*)conn)) != 0) //64位系統時指標不是4個位元組,不可移植
                {
                    fprintf(stderr, "pthread_create:%s\n", strerror(ret));
                    exit(EXIT_FAILURE);
                }
        */
    }

程式邏輯並不複雜,一旦accept 返回一個已串連通訊端,就建立一個新線程對其服務,在每個新線程thread_routine 中調用pthread_detach 剝離線程,我們的主線程不能調用pthread_join 等待這些新線程的退出,因為還要返回while 迴圈開頭去在accept 中阻塞監聽。

如果使用pthread_create(&tid, NULL, thread_routine, (void*)&conn); 存在的問題是如果accept 再次返回一個已串連通訊端,而此時thread_routine 函數還沒取走conn 時,可能會讀取到已經被更改的conn 值。

如果使用  pthread_create(&tid, NULL, thread_routine, (void*)conn); 存在的問題是在64位系統中指標不是4個位元組而是8個位元組,即不可移植 性。

使用上述未被注釋的做法,每次返回一個conn,就malloc 一塊記憶體存放起來,在thread_routine 函數中去讀取即可。

開多個用戶端,可以看到正常服務。


參考:

《linux c 編程一站式學習》

《UNP》

《APUE》

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.