摘要:本文主要记录linux下多线程并发服务器开发,在次之前,我想先梳理一下线程相关的知识,闲啰嗦的道友可以直接略过,后面会有源代码链接和测试结果。
一、线程和进程
-
1、线程是进程的一条执行路径,称为轻量级进程。
-
2、所有线程都是在同一进程空间运行,共享该进程中的全部系统资源,例如虚拟地址空间,文件描述符和信号处理等等。
3、虽然说线程在同一进程空间运行,但是每个线程有各自的调用栈、寄存器环境、线程本地存储等。
4、进程中的多个线程并行执行不同的任务,达到并发的作用。
5、不同程序间交换数据,对于多进程,需要在用户空间和内核空间进行频换切换,开销大。对于多线程来说,使用全局变量,只需要在用户空间切换,开销小,效率高。
二、主线程和子线程
-
1、对于c和c++,我的理解是main函数就是主线程,因为程序运行是从main函数开始的,然后再main函数中创建的新进程就是子进程,每个子进程都有自己的线程id,多个子进程之间是平级的,同时运行,没有先后顺序。
-
2、主线程和子线程的关系:主线程退出,所有子线程就会退出,这时整个进程结束或者僵死。
3、为了避免僵死状态,主线程和子线程可会合和相分离两种关系。
(1)、可会合:主线程等待子线程结束之后,在做下一步操作,这样整个程序就会有条不紊地结束,释放掉所有占用的资源。 (2)、相分离:子线程的结束和主线程之间没有关系,子线程结束后自动释放掉资源,主线程无需等待,相比可会合,高效稳定得多。
三、多线程可能用到的知识
-
1、临界资源:多个线程共享的资源,比如一个变量。 2、临界区:临界资源的范围。
-
3、互斥锁:多个线程共享一个临界资源,难免会发生竞争,为了避免发生竞争,就需要用到互斥锁。
4、死锁:用到互斥锁的话,可能会产生死锁,当A线程正在使用资源1的同时想要使用资源2,但是资源2被线程B持有,如果此时线程B也想使用资源1,A和B就会打架,实力不相上下,谁也不想把资源让给对方,此时就产生死锁了。
5、总结一下产生死锁的条件就是:互斥、占有且等待、循环等待、不可抢占。
四、多线程编程相关函数
1、线程属性设置函数,可参考博客:https://www.cnblogs.com/jacklikedogs/p/4030048.html。在项目中我们一般主要设置线程堆栈大小、线程分离状态两个属性,其他可以使用默认值。
typedef struct
{
int detachstate; 线程的分离状态,非分离 状态只有当pthread_join() 函数返回时,创建的线程才算终,才能释放自己占用的系统资源
int schedpolicy; 线程的调度策略
struct sched_param schedparam; 线程的调度参数
int inheritsched; 线程的继承性
int scope; 线程的作用域
size_t guardsize; 线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void * stackaddr; 线程栈的位置
size_t stacksize; 线程栈的大小
}pthread_attr_t;
2、创建进程函数
int pthread_create(
pthread_t *restrict tidp, //新线程ID。
const pthread_attr_t *restrict attr, //线程属性,默认为NULL
void *(*start_rtn)(void *), //子进程执行的程序,函数指针
void *restrict arg //子进程函数的参数,这个参数类型为任意类型,所以子进程需要多个参数时,我们需要打包成一个结构体,传
//入结构体
);
3、销毁结构体函数
int pthread_attr_destroy(pthread_attr_t *attr); //结构体使用结束后应该销毁
成功返回0;
4、会合函数
int pthread_join(pthread_t thread, void **retval);//主线程要等待非分离的子线程结束才能完全释放资源
成功返回0;
5、初始化锁函数
Int pthread_mutex_init(pthread_mutex_t *restrict_mutex,//锁的一种属性结构体
const pthread_mutextattr_t *restrict attr//NULL为默认值普通锁
)
6、锁操作函数
int pthread_mutex_lock(pthread_mutex_t *mutex);//获取阻塞锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);//获取非组赛锁
int pthread_mutex_unlock(pthreadd_mutex_t *mutex);//释放锁
五、代码实现
注:服务器和客户端相互配合使用,这里只贴出服务器server的主程序main程序,main程序中调用的一些用户程序包含在其他的c文件,我将它提交的git服务器进行管理,有需要的朋友点击以下链接查看。
服务器端完整代码链接:点击查看
客户端完整代码链接:点击查看
server_main.c:
#include "server.h"
int main(int argc,char **argv)
{
int server_port;
int sockfd = -1;
int clienfd = -1;
int re_val = -1;
pthread_t pthread_id;
struct sockaddr_in clien_addr;
socklen_t clienaddr_len;
//pthread_args_t pthread_args ;
pthread_args_t *pthread_args ;
//参数解析
re_val = argument_parse(argc, argv, &server_port);
if(re_val < 0)
{
printf("参数解析错误!\n");
exit(0);
}
//套接字初始化
re_val = socket_init(server_port,&sockfd);
if(re_val < 0)
{
printf("套接字初始化错误!\n");
}
//服务器开始工作
while(1)
{
//等待客户端连接上来
printf("serve is waitting to connect new client...\n");
mylog(__FUNCTION__,__LINE__,INFO,"serve is waitting to connect new client...:%s\n",strerror(errno));
if((clienfd = accept(sockfd,(struct sockaddr *)&clien_addr,&clienaddr_len)) <0 )
{
printf("serve accept failed: %s\n",strerror(errno));
mylog(__FUNCTION__,__LINE__,ERROR,"server accept faild:%s\n",strerror(errno));
return -1;
}
printf("Accept new client[%s:%d] scuccessfully.\n",inet_ntoa(clien_addr.sin_addr),ntohs(clien_addr.sin_port));
mylog(__FUNCTION__,__LINE__,INFO,"Accept new client[%s:%d] scuccessfully.\n",\
inet_ntoa(clien_addr.sin_addr),ntohs(clien_addr.sin_port));
//线程开始
pthread_args = (pthread_args_t *)malloc(sizeof(pthread_args_t));
//pthread_args.clienfd = clienfd;
pthread_args->clienfd = clienfd;
thread_start(&pthread_id, pthread_work, pthread_args);
}
return 0;
}
七、测试结果
流程:通过客户端采集数据,这里采集的是室内环境下的温度,然后通过TLV(用户自定义协议)发送给服务器中的数据库中保存。 以下显示图片均是服务器端接收到的数据,数据格式是:设备号28-041731f7c0ff + 温度 + 时间。
第一次测试结果:
显示一些特殊符号<、[ 等乱码,经过检查代码发现下面一个问题:close(sockfd);我以为客户端连接上建立了新的文件描述符,这个描述符就可以关闭,其实不然,关闭就会出现乱码,所以后面的程序中直接将它删除,数据正常。
//线程开始
//close(sockfd);
pthread_args = (pthread_args_t *)malloc(sizeof(pthread_args_t));
//pthread_args.clienfd = clienfd;
pthread_args->clienfd = clienfd;
thread_start(&pthread_id, pthread_work, pthread_args);
第二次测试结果:
client[4]代表第一个客户端已连接
client[5]代表第二个客户端已连接
client[4]单独连接的适合一切正常,但是当client[5]连接上来的时候,client[4]就被顶下去了。和小伙伴们一起分析:
*定义子线程参数结构体*/
typedef struct
{
int clienfd;//客户端文件描述符
int count;//线程数
pthread_mutex_t lock;//线程锁
}pthread_args_t;
pthread_args_t pthread_args;//这是传给子线程的参数结构体
pthread_create //在这里调用
问题就在于结构体定义的方式,因为子线程和主线程在同一个进程空间运行,子进程之间共享资源,我在主函数栈中静态分配一个结构体空间,每来一个客户端都会对这个结构体中clienfd的值进行改变,所以后面连接的都是最后一个连接上来的客户端,经过和小伙伴们分析总结出以下解决方案:
1、 传单个值
2 、malloc动态分配内存,然后传地址
//线程开始
pthread_args = (pthread_args_t *)malloc(sizeof(pthread_args_t));
3、 静态分配内存,可以在栈上分配一个足够大的,可以容纳所有参数,然后传地址;
int main()
{
pthread_args_t pthread_args[10000];
create_x(...., &pthread_args[i]);
}
第三次测试结果:
结果显示,服务器有序的稳定的向客户端4和客户端5交叉接收数据。。。。
注:在这里感谢以下团队小伙伴的帮助,和一群优秀的小伙伴一起学习事倍功半!代码程序肯定有很多地方不够完美,请朋友们积极指出,谢谢~~~~