C 多线程
在不了解多线程编程之前,我们的以为同一时刻,只能执行一个函数;在了解多线程之后,情况就不一样了,同一时刻可以有多个函数在执行。本文介绍c语言中多线程的使用,后半部分通过学到的多线程的知识点,将上篇的代码进行改造,使tcp服务端可以处理多个客户端的连接、且数据的接收和发送可以在各自线程中处理。
多线程
在pthread.h头文件中提供了线程创建相关的函数;
线程创建和线程等待
在b站博主正月点灯笼那里,学到了这个小技巧,可以告诉我们函数如何使用:
pthread_create函数使我们可以创建线程,它该如何使用?

故我们写出下面代码,创建一个子线程,执行一些操作。
#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操作”

解决资源争用
方案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!!!

浙公网安备 33010602011771号