TCP/IP网络编程笔记-ch18.多线程服务器端的实现

函数

线程创建

定义:

#include <pthread.h>

    @param thread:          保存新创建线程ID的变量地址值。线程与进程相同,也需用于区分不同线程的ID
    @param attr:            用于传递线程属性的参数,传递NULL时,创建默认属性的线程
    @param start_routine:   相当于线程main函数的、在单独执行流中执行的函数地址值(函数指针)
    @param arg:             通过第三个参数传递调用函数时包含传递参数信息的变量地址值
int pthread_create(
    pthread_t * restrict thread,const pthread_attr_t * restrict attr, void* (* start_routine)(void *),void 
    * restrict arg
    );
    //return:成功返回0,失败返回-1

使用:

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

int main(int argc, char *argv[])
{
    pthread_t t_id;
    int thread_param = 5;
    // 请求创建一个线程,从 thread_main 调用开始,在单独的执行流中运行。同时传递参数
    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) //传入的参数是 pthread_create 的第四个:(void *)&pthread_param
{
    int i;
    int cnt = *((int *)arg);
    for (int i = 0; i < cnt; i++)
    {
        sleep(1);
        puts("running thread");
    }
    return NULL;
}

调用sleep(10):为了延迟进程终止时间,因为main函数一旦终止,产生的线程会被直接销毁,为了让线程能执行完毕,所以sleep(10)以确保其执行完毕,如果想验证的话,可以将sleep(10)换为sleep(1),会发现线程中输出的字段与预计不符。 ## 补充

但我们不会使用sleep来保证线程执行完毕,而应该利用pthread_join来保证线程执行完毕。

让线程的进程等待

定义

#include<pthread.h>

@param thread:该参数值ID的线程终止后才会从该函数返回
@param status:保存线程的main函数返回值的指针变量地址值
int pthread_join(pthread_t thread, void ** status);
//成功时返回0,失败时返回其他值

使用

#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;
    // 请求创建一个线程,从 thread_main 调用开始,在单独的执行流中运行。同时传递参数
    if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0)
    {
        puts("pthread_create() error");
        return -1;
    }
    //main函数将等待 ID 保存在 t_id 变量中的线程终止
    //thr_ret:保存线程函数thread_main内部动态分配的内存空间地址
    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) //传入的参数是 pthread_create 的第四个
{
    int i;
    int cnt = *((int *)arg);
    char *msg = (char *)malloc(sizeof(char) * 50);
    strcpy(msg, "Hello,I'am thread~ \n");
    for (int i = 0; i < cnt; i++)
    {
        sleep(1);
        puts("running thread");
    }
    return (void *)msg; //返回值是 thread_main 函数中内部动态分配的内存空间地址值
}

根据多个线程同时执行临界区代码是否会产生问题,函数可分为:线程安全函数非线程安全函数
线程安全函数被多个线程同时调用也不会引发问题,而非线程安全函数则不然。

但大多数标准函数都是线程安全的函数,而且即使是非线程安全函数也基本提供了相同功能的线程安全函数。

如:
gethostbyname为非线程安全函数,其对应的线程安全函数:gethostbyname_r

如果感到麻烦,可以通过下方法自动将gethostbyname函数调用改为gethostbyname_r函数调用:
声明头文件前定义_REENTRANT宏
也可在编译时添加-D_REENTRANT选项定义宏
gcc -D_REENTRANT mythread.c -o mthread -lpthread

补充

回收进程 pthread_join

//一个进程中的所有线程都可以调用pthread_join函数来回收其他线程(前提是目标进程可回收),即等待其他线程结束,
//这类似于回收进程的wait和waitpid系统调用

@param thread:目标线程的标识符
@param retval:目标线程返回的退出信息
int pthread_join(pthread_t thread, void** retval);
//该函数一直阻塞,直到被回收的线程结束为止。
//成功时返回0,失败返回错误码。
错误码描述
EDEADLK可能引起死锁。比如两个线程互相针对对方调用pthread_join,或线程对自身调用pthread_join
EINVAL目标线程不可回收,或已有其他线程在回收该目标线程
ESRCH目标线程不存在

取消进程 pthread_cancel

//异常终止一个线程,即取消线程

@param thread:目标线程的标识符
int pthread_cancel(pthread_t thread);
//函数成功时返回0,失败则返回错误码。

接收到取消请求的目标线程可以决定是否允许被取消以及如何取消:
@param state:       设置是否允许被取消
@param oldstate:    记录线程原来的取消状态和取消类型
int pthread_setcancelstate(int state, int *oldstate);
//成功时返回0,失败时返回错误码    
    state:
        PTHREAD_CANCEL_ENABLE:允许线程被取消。(线程创建时默认)
        PTHREAD_CANCEL_DISABLE:禁止线程被取消。这时若一线程收到取消请求,它会将请求挂起,直到该线程允许被取消。
    
@param type:        取消类型(如何取消)
@param oldtype:     记录线程原来的取消状态和取消类型
int pthread_setcanceltype(int type, int *oldtype);
//成功时返回0,失败时返回错误码
    type:
        PTHREAD_CANCEL_ASYNCHRONOUS:线程随时可以被取消,被取消的目标立刻采取行动。
        PTHREAD_CANCEL_DEFERRED:    允许目标线程推迟行动,直到它调用了取消点函数中的一个:
            pthread_join、pthread_testcancel、pthread_cond_wait、pthread_cond_timedwait
            、sem_wait、sigwait

结束进程 pthread_exit

//线程函数结束时调用如下函数,确保安全、干净地退出

@param retval:函数通过该参数向线程的回收者传递其退出信息。
void pthread_exit(void* retval);
//执行完后不会返回到调用者,且永远不会失败

线程区存在问题和临界区

当多个线程同时访问全局变量时,如线程A要对全局变量x执行500次++操作,进程B则对其执行500次–操作,那结果它是不是应该不变呢?
并不是,这也并非是全局变量带来的问题,而是任何内存空间在被同时访问时,都可能发生问题。

为了解决这种问题,我们需要认清:
临界区是什么:
函数内同时运行多个线程时引起问题的多条语句构成的代码块

然后运用线程同步来解决它.

有两种技术能够实现线程同步:
互斥量信号量

互斥量

互斥量为变量上锁,保证没有多个操作同时访问该变量。

创建、销毁互斥量

#include<pthread.h>

@param mutex:创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址
@param attr:传递即将创建的互斥量属性,没有特别需要指定的属性时传递NULL
int pthread_init(pthread_mutex_t * mutex, const pthread_mutexattr_t * attr);
int pthread_destroy(pthread_mutex_t * mutex);
//均为:成功返回0,失败返回其他值

利用互斥量锁住或释放临界区使用的函数

#include<pthread.h>

int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);
//均为:成功返回0,失败返回其他值

调用pthread_mutex_lock时,若其他线程已经进入临界区,则pthread_mute_lock不会返回,知道其他线程调用pthread_mutex_unlock退出临界区。

所以可以利用下列结构保护临界区:

pthread_mutex_lock(&mutex);
//临界区开始
//.........
//临界区结束
pthread_mutex_unlock(&mutex);

利用互斥量的例子

#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;
pthread_mutex_t mutex; //保存互斥量读取值的变量

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

    pthread_mutex_init(&mutex, NULL); //创建互斥量

    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);
    pthread_mutex_destroy(&mutex); //销毁互斥量
    return 0;
}

void *thread_inc(void *arg)
{
    int i;
    pthread_mutex_lock(&mutex); //上锁
    for (i = 0; i < 50000000; i++)
        num += 1;
    pthread_mutex_unlock(&mutex); //解锁
    return NULL;
}
void *thread_des(void *arg)
{
    int i;
    pthread_mutex_lock(&mutex);
    for (i = 0; i < 50000000; i++)
        num -= 1;
    pthread_mutex_unlock(&mutex);
    return NULL;
}

信号量

信号量创建与销毁

#include<semaphore.h>

@param sem:创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址
@param pshared:传递其他值时,创建可由多个进程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量。线程同步在同一进程内,所以用于线程同步时应该传递0.
@param value:指定新创建的信号量初始值
int sem_init(sem_t * sem, int pshared, unsigned int value);
int sem_destroy(sem_t * sem);
//均为:成功返回0,失败返回其他值

用于线程同步时,pshared传递0即可。

修改信号量值

#include <semaphore.h>

@param sem:传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait时信号量减1
int sem_post(sem_t * sem);
int sem_wait(sem_t * sem);
//均为:成功返回0,失败返回其他值

信号量为0时调用sem_wait,调用函数的线程将进入阻塞(因为函数未返回)。这时有其他线程调用sem_post,信号量值变为1,原本阻塞的线程可调用sem_wait将信号量重新减为0并跳出阻塞状态

所以可以利用下列格式构建临界区:

sem_wait(&sem); //信号量变为0 其他调用sem_wait的函数会阻塞,直到本段临界区结束使得信号量变为1
//临界区的开始
//...........
//临界区的结束
sem_post(&sem);//信号量变为1

这种信号量在0与1直接跳转的特性的机制,也称"二进制信号量"。

利用信号量的例子

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

void *read(void *arg);
void *accu(void *arg);
static sem_t sem_one;
static sem_t sem_two;
static int num;

int main(int argc, char const *argv[])
{
    pthread_t id_t1, id_t2;
    sem_init(&sem_one, 0, 0);
    sem_init(&sem_two, 0, 1);

    pthread_create(&id_t1, NULL, read, NULL);
    pthread_create(&id_t2, NULL, accu, NULL);

    pthread_join(id_t1, NULL);
    pthread_join(id_t2, NULL);

    sem_destroy(&sem_one);
    sem_destroy(&sem_two);
    return 0;
}

void *read(void *arg)
{
    int i;
    for (i = 0; i < 5; i++)
    {
        fputs("Input num: ", stdout);

        sem_wait(&sem_two);
        scanf("%d", &num);
        sem_post(&sem_one);
    }
    return NULL;
}
void *accu(void *arg)
{
    int sum = 0, i;
    for (i = 0; i < 5; i++)
    {
        sem_wait(&sem_one);
        sum += num;
        sem_post(&sem_two);
    }
    printf("Result: %d \n", sum);
    return NULL;
}

如果不理解为何需要2个信号量,可将代码注释部分去掉,再运行一次,观察运行结果.

线程的销毁

销毁线程的3种方法

Linux线程并非在首次调用的线程的main函数返回时自动消失。应该使用如下方法销毁进程:

pthread_join
pthread_detach

pthread_join调用时,会等待线程终止再销毁它。
而pthread_detach则不会引起线程终止或进入阻塞状态,通过该函数引导注销线程创建的内存空间,使用该函数后不能针对相应线程调用pthread_join函数。调用该函数后,对应线程在结束后会自动销毁自身创建的内存空间。
还有一种方法在创建线程时可以指定销毁时机,但与pthread_detach相比无太大差异,故省略。

pthread_detach

#include<pthread.h>

@param thread:终止的同时需要销毁的线程ID
int pthread_detach(pthread_t thread);
//成功返回0,失败返回其他值

多线程并发服务器端的实现

chat_server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

#define BUF_SIZE 100
#define MAX_CLNT 256

void *handle_clnt(void *arg);
void send_msg(char *msg, int len);
void error_handling(char *msg);

int clnt_cnt = 0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    int clnt_adr_sz;
    pthread_t t_id;
    if (argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    pthread_mutex_init(&mutx, NULL); //创建互斥锁
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    while (1)
    {
        clnt_adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);

        pthread_mutex_lock(&mutx);          //上锁
        clnt_socks[clnt_cnt++] = clnt_sock; //写入新连接
        pthread_mutex_unlock(&mutx);        //解锁

        pthread_create(&t_id, NULL, handle_clnt, (void *)&clnt_sock);       //创建线程为新客户端服务,并且把clnt_sock作为参数传递
        pthread_detach(t_id);                                               //引导线程销毁,不阻塞
        printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr)); //客户端连接的ip地址
    }
    close(serv_sock);
    return 0;
}

void *handle_clnt(void *arg)
{
    int clnt_sock = *((int *)arg);
    int str_len = 0, i;
    char msg[BUF_SIZE];

    while ((str_len = read(clnt_sock, msg, sizeof(msg))) != 0)
        send_msg(msg, str_len);
    //接收到消息为0,代表当前客户端已经断开连接
    pthread_mutex_lock(&mutx);
    for (i = 0; i < clnt_cnt; i++) //删除没有连接的客户端
    {
        if (clnt_sock == clnt_socks[i])
        {
            while (i++ < clnt_cnt - 1)
                clnt_socks[i] = clnt_socks[i + 1];
            break;
        }
    }
    clnt_cnt--;
    pthread_mutex_unlock(&mutx);
    close(clnt_sock);
    return NULL;
}
void send_msg(char *msg, int len) //向连接的所有客户端发送消息
{
    int i;
    pthread_mutex_lock(&mutx);
    for (i = 0; i < clnt_cnt; i++)
        write(clnt_socks[i], msg, len);
    pthread_mutex_unlock(&mutx);
}
void error_handling(char *msg)
{
    fputs(msg, stderr);
    fputc('\n', stderr);
    exit(1);
}

chat_clnt.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>

#define BUF_SIZE 100
#define NAME_SIZE 20

void *send_msg(void *arg);
void *recv_msg(void *arg);
void error_handling(char *msg);

char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    pthread_t snd_thread, rcv_thread;
    void *thread_return;
    if (argc != 4)
    {
        printf("Usage : %s <IP> <port> <name>\n", argv[0]);
        exit(1);
    }

    sprintf(name, "[%s]", argv[3]);
    sock = socket(PF_INET, SOCK_STREAM, 0);

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error");

    pthread_create(&snd_thread, NULL, send_msg, (void *)&sock); //创建发送消息线程
    pthread_create(&rcv_thread, NULL, recv_msg, (void *)&sock); //创建接受消息线程
    pthread_join(snd_thread, &thread_return);
    pthread_join(rcv_thread, &thread_return);
    close(sock);
    return 0;
}

void *send_msg(void *arg) // 发送消息
{
    int sock = *((int *)arg);
    char name_msg[NAME_SIZE + BUF_SIZE];
    while (1)
    {
        fgets(msg, BUF_SIZE, stdin);
        if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n"))
        {
            close(sock);
            exit(0);
        }
        sprintf(name_msg, "%s %s", name, msg);
        write(sock, name_msg, strlen(name_msg));
    }
    return NULL;
}

void *recv_msg(void *arg) // 读取消息
{
    int sock = *((int *)arg);
    char name_msg[NAME_SIZE + BUF_SIZE];
    int str_len;
    while (1)
    {
        str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1);
        if (str_len == -1)
            return (void *)-1;
        name_msg[str_len] = 0;
        fputs(name_msg, stdout);
    }
    return NULL;
}

void error_handling(char *msg)
{
    fputs(msg, stderr);
    fputc('\n', stderr);
    exit(1);
}

知识点

为什么使用线程

我们使用线程,往往是为了替换进程。所以为什么使用线程,要从进程的缺点出发:
多进程模型的缺点为:

  • 创建进程的过程会带来一定的开销。
  • 为完成进程间数据交换,需要特殊的IPC技术。
    除了上述两点外,最重要的是,创建进程会带来**每秒少则数十次、多则数千次的’上下文切换’(Context Switching)**导致的开销。

上下文切换是什么?
运行程序前需要将相应进程信息读入内存,若运行进程A后需紧接着运行进程B,则应该将进程A相关信息移出内存,并读入进程B的内存信息。这就是上下文切换。

为什么要用上下文切换?
要从只有一个CPU的系统是怎么同时运行多个进程的说起:
系统把CPU时间分成多个微小的块再分配给进程。为分时使用这些CPU时间,就需要’上下文切换’。

而为了保持多进程的优点:同时执行多个任务
克服其缺点:避免上下文切换
才引入了线程,其优点就是:
线程创建于上下文切换比进程的创建和上下文切换更快。
线程间交换数据无需特殊技术。

进程在操作系统中构成单独执行流的单位.(需要使用保存全局变量的数据区、动态分配所用空间堆、函数运行所要的栈)
而线程在进程构成单独执行流的单位.(扎根于进程,将函数运行所需栈区域隔开,只使用数据区和空间堆)。

进程在操作系统内部生成多条执行流,线程则在进程内部创建多条执行流

实例

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值