C 多线程

在不了解多线程编程之前,我们的以为同一时刻,只能执行一个函数;在了解多线程之后,情况就不一样了,同一时刻可以有多个函数在执行。本文介绍c语言中多线程的使用,后半部分通过学到的多线程的知识点,将上篇的代码进行改造,使tcp服务端可以处理多个客户端的连接、且数据的接收和发送可以在各自线程中处理。

多线程

在pthread.h头文件中提供了线程创建相关的函数;

线程创建和线程等待

b站博主正月点灯笼那里,学到了这个小技巧,可以告诉我们函数如何使用:
pthread_create函数使我们可以创建线程,它该如何使用?
image

故我们写出下面代码,创建一个子线程,执行一些操作。

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

void *dosomething(void *arg) {
    puts("i am in work thread");
    return NULL;
}

int main(void) {
    pthread_t th;
    int ret = pthread_create(&th,NULL, dosomething,NULL);
    if (ret == 0) {
        pthread_join(th,NULL);//防止程序一闪而过
    }
    return 0;
}

访问共享资源

有了多线程之后,我们便可在同一时间点运行多个方法;但由此可能会带来一些烦恼--比如下面两个函数分别将全局变量sum进行了10000次加1操作。结果如何?

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

int sum;

void *method1(void *arg) {
    for (int i = 0; i < 1000; i++) {
        sum = sum + 1;
    }
    return NULL;
}

void *method2(void *arg) {
    for (int i = 0; i < 1000; i++) {
        sum = sum + 1;
    }
    return NULL;
}

int main(void) {
    pthread_t th1;
    int ret = pthread_create(&th1,NULL, method1,NULL);


    pthread_t th2;
    int ret2 = pthread_create(&th2,NULL, method2,NULL);

    pthread_join(th1,NULL);
    pthread_join(th2,NULL);

    printf("sum = %d\n", sum);
    return 0;
}

程序运行多次,输出了多次结果!

sum = 1110

为什么不是我们预期的2000?
这是因为两个线程可能在同一时刻取到了sum的值,进行了+1操作,然后又分别将结果交给sum。这样就造成了“少进行了一次加1操作”
image

解决资源争用

方案1:引入锁,让两个方法不可以在同一时刻进行。

这里需要引入 pthread_mutex_t 声明锁;使用 pthread_mutex_init初始化锁;

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

int sum;

pthread_mutex_t lock; //1 声明锁

void *method1(void *arg) {
    pthread_mutex_lock(&lock); //加锁
    for (int i = 0; i < 1000; i++) {
        sum = sum + 1;
    }
    pthread_mutex_unlock(&lock); //释放锁
    return NULL;
}

void *method2(void *arg) {
    pthread_mutex_lock(&lock); //加锁
    for (int i = 0; i < 1000; i++) {
        sum = sum + 1;
    }
    pthread_mutex_unlock(&lock); //释放锁
    return NULL;
}

int main(void) {
    pthread_mutex_init(&lock, NULL); //2 创建锁

    pthread_t th1;
    int ret = pthread_create(&th1,NULL, method1,NULL);


    pthread_t th2;
    int ret2 = pthread_create(&th2,NULL, method2,NULL);

    pthread_join(th1,NULL);
    pthread_join(th2,NULL);

    printf("sum = %d\n", sum);
    return 0;
}

输出

sum = 2000
方案2:解锁资源竞争的终极目标是:无锁

对上面的场景进行分析,可以想到:让两个方法分别将计算结果交给 sum1和sum2;最后再将sum1+sum2就得到最总结果。
不再进行代码演示,只是提供一种思路,即:考虑我们面临的资源争用的场景,能否在不引入锁的情况下解决。

改进tcp客户端和服务端

在上篇C TCP通信中,服务端的接收客户端连接、数据接收都在一个线程进行。学习了多线程之后,便可以将这些步骤放在多个线程中执行,这样服务端可以一边接收新的客户端连接、一边发送消息、一边接收消息。

TCP服务端改造

#include <stdio.h>//控制台输入输出函数所在头文件
#include <string.h>//清空字符串函数所在头文件
#include <sys/socket.h>//套接字结构体所在头文件
#include <arpa/inet.h>//字节序网络序所在头文件
#include <unistd.h>//close套接字所在函数
#include <signal.h>//接收程序退出信号的函数所在头文件
#include <stdlib.h>//exit函数所在头文件

#include <pthread.h>

int socket_fd;

void handle_signal(int sig) {
    puts("用户退出程序");
    if (socket_fd > 0) {
        puts("停止服务");
        close(socket_fd);
    }
    exit(0);
}

//发送消息给客户端
void *execute_send(void *arg) {
    char buf[512];
    while (arg != NULL) {
        memset(buf, 0, sizeof(buf));
        puts("输入要发送的内容(直接按回车后退出程序)");
        fgets(buf, sizeof(buf) - 1, stdin);
        size_t len = strlen(buf);
        if (len > 0 && buf[len - 1] == '\n') {
            buf[len - 1] = '\0';
        }
        if (strlen(buf) == 0) {
            puts("退出发送");
            break;
        }
        printf("发送数据:%s,发送长度:%lu\n", buf, strlen(buf));
        send(*(int *) arg, buf, strlen(buf), 0);
    }
    if (arg != NULL) {
        close(*(int *) arg); //释放客户端连接
    }
    return NULL;
}

//接收来自客户端的消息
void *execute_recv(void *arg) {
    //接收或发送消息
    char buf[512];
    while (arg != NULL) {
        memset(buf, 0, sizeof(buf));
        ssize_t recv_len = recv(*(int *) arg, buf, sizeof(buf) - 1, 0); //最后一个参数0 代表阻塞
        if (recv_len == 0) {
            //客户端主动关闭连接
            puts("客户端关闭连接");
            break;
        }
        if (recv_len < 0) {
            perror("接收数据异常");
            break;
        }
        printf("收到消息: %s,长度:%lu\n", buf, recv_len);
    }
    if (arg != NULL) {
        close(*(int *) arg); //释放客户端连接
    }
    return NULL;
}

void *execute_accept(void *arg) {
    while (arg != NULL) {
        //接受连接
        int client_fd = accept(socket_fd, NULL, NULL);
        printf("收到了一个客户端连接,客户端id:%d\n", client_fd);

        pthread_t th_send;
        pthread_create(&th_send,NULL, execute_send, &client_fd);

        pthread_t th_recv;
        pthread_create(&th_recv, NULL, execute_recv, &client_fd);
    }
    return NULL;
}

int main(void) {
    signal(SIGINT, handle_signal); //解决程序被用户手动关闭 无法正常停止服务的问题
    signal(SIGTERM, handle_signal);
    //创建套接字 指定ipv4 套接字类型 套接字协议默认
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    //在bind之前设置socket 尝试重用端口
    int opt = 1;
    setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR, &opt, sizeof(opt));
    setsockopt(socket_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));


    //绑定本地网络信息到套接字
    struct sockaddr_in local_addr;
    local_addr.sin_family = AF_INET;
    local_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    local_addr.sin_port = htons(8001);
    int ret = bind(socket_fd, (struct sockaddr *) &local_addr, sizeof(local_addr));
    if (ret != 0) {
        perror("bind error");
        close(socket_fd);
        exit(-1);
    }
    //监听连接
    ret = listen(socket_fd, 5);
    if (ret != 0) {
        perror("listen error");
        close(socket_fd);
        exit(-1);
    }

    printf("服务正在监听%s:%d...\n", inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));

    pthread_t th_accept;
    pthread_create(&th_accept, NULL, execute_accept, &socket_fd);

    pthread_join(th_accept, NULL);
    close(socket_fd);
    return 0;
}

改造TCP客户端

#include <stdio.h>//控制台输入输出函数所在头文件
#include <string.h>//清空字符串函数所在头文件
#include <sys/socket.h>//套接字结构体所在头文件
#include <arpa/inet.h>//字节序网络序所在头文件
#include <unistd.h>//close套接字所在函数
#include <signal.h>//接收程序退出信号的函数所在头文件
#include <stdlib.h>//exit函数所在头文件

#include <pthread.h>

int socketfd;

void handle_exit() {
    if (socketfd >= 0) {
        close(socketfd);
        puts("释放套接字");
    }
}


void handle_signal(int sig) {
    puts("收到程序退出信息,正在处理...");
    exit(EXIT_SUCCESS);
}


void *execute_recv(void *arg) {
    char buf[100];
    while (socketfd) {
        memset(buf, 0, sizeof(buf));
        ssize_t len = recv(socketfd, buf, sizeof(buf) - 1, 0);
        if (len < 0) {
            perror("recv error");
            close(socketfd);
            exit(EXIT_SUCCESS);
        } else if (len > 0) {
            printf("收到消息: %s\n", buf);
        }
    }
    return NULL;
}

//发送数据
void *execute_send(void *arg) {
    char buf[100];
    while (1) {
        memset(buf, 0, sizeof(buf));
        puts("输入要消息后按回车发送(直接按回车后退出程序)");
        fgets(buf, sizeof(buf), stdin);
        size_t len = strlen(buf);
        if (len > 0 && buf[len - 1] == '\n') {
            buf[len - 1] = '\0';
        }
        if (strlen(buf) == 0) {
            puts("退出");
            break;
        }
        printf("发送数据:%s,数据长度:%lu\n", buf, strlen(buf));
        send(socketfd, buf, strlen(buf), 0);
    }
    return NULL;
}

int main(void) {
    signal(SIGINT, handle_signal);
    signal(SIGTERM, handle_signal);
    atexit(handle_exit);
    //创建套接字 返回文件描述符 非负值标识创建成功
    socketfd = socket(AF_INET, SOCK_STREAM, 0); //地址族 套接字类型 套接字协议

    //绑定地址 传入套接字描述符 传入网络信息
    //创建目标地址信息
    struct sockaddr_in local_addr;
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = htons(2000);
    local_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bind(socketfd, (struct sockaddr *) &local_addr, sizeof(local_addr));


    //发起连接
    struct sockaddr_in remote_addr;
    remote_addr.sin_family = AF_INET;
    remote_addr.sin_port = htons(8001);
    remote_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    printf("向%s:%d发起连接...\n", inet_ntoa(remote_addr.sin_addr),ntohs(remote_addr.sin_port));
    connect(socketfd, (struct sockaddr *) &remote_addr, sizeof(remote_addr));
    puts("连接成功...");

    pthread_t th_recv;
    int ret = pthread_create(&th_recv,NULL, execute_recv,NULL);
    if (ret != 0) {
        perror("pthread_create error");
    }

    pthread_t th_send;
    pthread_create(&th_send,NULL, execute_send, &th_send);

    pthread_join(th_send, NULL);
    close(socketfd);

    return 0;
}

运行情况:
服务端:

服务正在监听127.0.0.1:8001...
收到了一个客户端连接,客户端id:4
输入要发送的内容(直接按回车后退出程序)
收到消息: 你好👋,长度:10
我是服务端
发送数据:我是服务端,发送长度:15
输入要发送的内容(直接按回车后退出程序)
收到消息: 我是客户端😄,长度:19
客户端关闭连接

客户端:

向127.0.0.1:8001发起连接...
连接成功...
输入要消息后按回车发送(直接按回车后退出程序)
你好👋
发送数据:你好👋,数据长度:10
输入要消息后按回车发送(直接按回车后退出程序)
收到消息: 我是服务端
我是客户端😄
发送数据:我是客户端😄,数据长度:19
输入要消息后按回车发送(直接按回车后退出程序)

退出
释放套接字

over!!!

posted @ 2025-11-30 22:11  BigBosscyb  阅读(1)  评论(0)    收藏  举报