歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux管理 >> Linux網絡

linux網絡編程之posix 線程(一)

pthread 系列函數 和 簡單多線程服務器端程序

一、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了。

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

#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 多進程的方式來實現並發,現在嘗試使用多線程方式來實現:

#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 函數中去讀取即可。

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

Copyright © Linux教程網 All Rights Reserved