网络编程——多线程服务器端的实现

参考

  1. 《TCP/IP网络编程》 尹圣雨

多线程服务器端的实现

线程和线程

创建进程(复制)的工作本身会给操作系统带来相当沉重的负担。而且,每个进程具有独立的内存空间,所以进程间通信的实现难度也会随之提高。

多进程模型的缺点:

  1. 创建进程的过程会带来一定的开销
  2. 为了完成进程间数据交换,需要特殊的IPC(Inter-Process Communication)技术
  3. 频繁的上下文切换(Context Switching)是创建进程时最大的开销

为了保持多进程的优点,并一定程度上克服其缺点,引入了线程。线程的优点:

  1. 线程的创建和上下文切换比进程的创建和上下文切换更快
  2. 线程间交换数据时无需特殊技术

线程不像进程那样完全分离内存结构,只需分离栈区域,即可保持多条代码执行流。于是,线程获得如下优势:

  1. 上下文切换时不需要切换数据区和堆
  2. 可以利用数据区和堆交换数据

多个线程将共享数据区和堆。为了保持这种结构,线程将在进程内创建并运行。进程与线程的关系和区别可以总结为:

  1. 进程:在操作系统构成单独执行流单位
  2. 线程:在进程构成单独执行流单位

线程的创建

线程具有单独的执行流,因此需要单独定义线程的main函数,还需要请求操作系统在单独的执行流中执行该函数,完成该功能的函数为pthread_create()

#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的变量地址值;attr用于传递线程属性的参数,传递NULL时,创建默认属性的线程;start_routine相当于线程main函数的、在单独执行流中执行的函数地址值(函数指针);arg为通过第三个参数传递调用函数函数时包含传递参数信息的变量地址值

简单示例:

#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;

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

线程相关代码在编译时需要添加-lphread选项声明需要连接线程库,只有这样才能调用头文件pthread.h中声明的函数

线程的运行

main函数返回后整个进程将被销毁,包括线程。上述示例中使用sleep()函数控制线程的执行相当于预测程序的执行流程,实际上这是不可能的。所以,不能用sleep()函数,而使用pthread_join()函数控制线程的执行流

#include <pthread.h>
int pthread_join(pthread_t thread, void** status);

成功时返回0,失败时返回其他值。其中thread表示该参数值ID的线程终止后才会从该函数返回;status保存线程的main函数返回值的指针变量的地址值

简单示例:

#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, thread_main, (void*)&thread_param) != 0)
    {
        puts("pthread_create() error");
        return -1;
    }

    if (pthread_join(t_id, &thr_ret) != 0)                  // 等待ID保存在t_id变量中的线程终止
    {
        puts("pthread_join() error");
        return -1;
    }

    printf("Thread return message: %s \n", (char*)thr_ret); // 返回值thr_ret是thread_main内部动态分配的内存空间地址值
    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;
}

临界区

创建多个线程时,需要考虑多个进程同时调用函数时(执行时)可能产生的问题。这类函数内部存在临界区,多个线程同时执行这部分代码时,可能引起问题。根据临界区是否引起问题,函数可分为:

  1. 线程安全函数
  2. 非线程安全函数

线程安全的函数中同样可能存在临界区。只是在线程安全函数中,同时被多个线程调用时可通过一些措施避免问题

大多数标准函数都是线程安全函数,而且我们不用自己区分线程安全函数和线程非安全函数。因为这些平台在定义非线程安全函数的同时,提供了具有相同功能的线程安全函数。线程安全函数的名称后缀通常为_r。可以通过在声明头文件前定义_REENTRANT宏,自动将非线程安全函数改为线程安全函数调用。另外,无需为了该宏定义特意添加#define语句,可以在编译时通过添加-D_REENTRANT选项定义宏。例如:

gcc -D_REENTRANT mythread.c -o mthread -lpthread

Worker线程模型

例如,计算1到10的和,创建2个线程,其中一个线程计算1到5的和,另一个线程计算6到10的和,main函数只负责输出运算结果。这种方式的编程模型称为Worker线程模型

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

此处存在临界区相关问题,下面通过创建100个线程,增加发生临界区相关错误的可能性

#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函数,全局变量num经过增减后应为0,但运行结果不是0,且每次都不一样。

发生这个现象是因为全局变量num被多个线程同时访问。例如,线程1在尚未将值写入num时,CPU资源被其他线程占用,此时其他线程得到的将是未修改的num值

临界区定义为,函数内同时运行多个线程引起问题的多条语句构成的代码块。临界区通常位于由线程运行的函数内部

在本例中,临界区并非是num本身,而是访问num的2条语句。需要注意的是,2条不同语句由不同线程同时执行时,也有可能构成临界区。前提是这2条语句访问同一内存空间。如tread_inc和thread_des中同时对num访问

线程同步

线程同步用于解决线程访问顺序引发的问题。需要同步的情况分为2种:

  1. 同时访问同一内存空间时发生的情况
  2. 需要指定访问同一内存空间的线程执行顺序的情况
互斥量(Mutual Exclusion)

互斥量主要用于解决线程同步访问的问题

(1)互斥量的创建及销毁

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex)

成功时返回0,失败时返回其他值。其中,mutex为创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值;attr传递即将创建的互斥量属性,没有特别的就传递NULL

(2)互斥量的锁住和释放

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);

成功时返回0,失败时返回其他值

创建好互斥量的前提下,可以通过如下结构包含临界区

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

(3)示例

#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;

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

    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);
    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;
    for (i = 0; i < 50000000; i++)
    {
        pthread_mutex_lock(&mutex);                        // 锁住互斥量
        num -= 1;
        pthread_mutex_unlock(&mutex);                      // 释放互斥量
    }
    return NULL;
}
信号量

信号量与互斥量极为相似

(1)信号量的创建及销毁

#include <semaphore.h>

int sem_init(sem_t* sem, int pthread, unsigned int value);
int sem_destroy(sem_t* sem);

成功时返回0,失败时返回其他值。其中:

  1. sem:创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值;
  2. pshared:传递其他值时,创建可由多个线程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量
  3. value:指定新创建的信号量的初始值

(2)信号量的增加与减少

#include <semaphore.h>

int sem_post(sem_t* sem);
int sem_wait(sem_t* sem);

成功时返回0,失败时返回其他值。其中,sem传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait时信号量减1。在信号量为0的情况下,调用sem_wait函数时,调用函数的线程将进入阻塞状态。可以通过如下形式同步临界区:

sem_wait(&sem);
// 临界区的开始
// ......
// 临界区的结束
sem_post(&sem);

(3)示例
示例的场景为,线程A从用户输入得到值后存入全局变量num,此时线程B将取走该值并累加。该过程共进行5次,完成后输出总和并退出程序

#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* 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;
}

利用信号量变量sem_two调用wait函数和post函数,是为了防止在调用accu函数的线程还未取走数据的情况下,调用read函数的线程覆盖原值;利用信号量变量sem_one调用wait和post函数,是为了防止调用read函数的线程写入新值前,accu函数取走数据

线程的销毁

Linux线程并不是在首次调用的线程main函数返回时自动销毁,如果不主动销毁,由线程创建的内存空间将一直存在。销毁线程的方法:

  1. 调用pthread_join函数
  2. 调用pthread_detach函数

调用pthread_join函数时,不仅会等待线程终止,还会引导线程销毁。但调用该函数的进程将进入阻塞状态。因此,通常调用pthread_detach函数销毁线程

#include <pthread.h>

int pthread_detach(pthread_t thread);

成功时返回0,失败时返回其他值。其中,thread为终止的同时需要销毁的线程ID

调用pthread_detach函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间。调用该函数后,不能再针对相应线程调用pthread_join函数

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

下面实现多个客户端之间可以交换信息的简单的聊天程序

服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.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;                             // 有新连接时,将相关信息写入变量clnt_cnt和clnt_socks
        pthread_mutex_unlock(&mutx);

        pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);    // 创建线程向新接入的客户端提供服务
        pthread_detach(t_id);                                           // 从内存中销毁已终止的线程
        printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));
    }
    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 ((strlen = read(clnt_sock, msg, sizeof(msg))) != 0)
    {
        send_msg(msg, str_len);
    }
    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);
}
客户端

客户端为了分离输入和输出过程而创建了线程

#include <stdio.h>
#include <stdlib.h>
#include <string.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* msg)
{
    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* msg)
{
    int sock = *((int*)arg);
    char name_msg[NAME+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);
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值