【C语言】Linux Socket 多线程处理


前言

学完多进程,就到了多线程的范围了,多进程和多线程的区别就在于

  • 多进程是工作在不同的进程空间的,进程间如果需要通信,需要用到这些方法:信号,管道,Socket,信号量,共享内存,消息队列。【一旦创建了过多的子进程,那么就会导致有非常庞大的资源开销,包括时间、内存资源等】
  • 多线程是工作在同一个进程空间的,可以共享主线程的全部系统资源,相比之下,多线程的通信效率会高些。

提示:以下是本篇文章正文内容,下面案例可供参考

一、多线程是什么?

在操作系统原理的术语中,线程是进程的一条执行路径。线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。

下面这个图比较清晰地描述了线程之间的关系【各线程都是对等的,在执行顺序上没有谁先谁后之分】
在这里插入图片描述

线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,进程的创建所花的时间片要比线程大些,另外进程间的通信比较麻烦,需要在用户空间和内核空间进行频繁的切换,开销很大。 但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。

二、使用步骤

1、创建线程

一个进程创建后,会首先生成一个缺省的线程,通常称这个线程为主线程(或称控制线程),C/C++程序中,主线程就是通过main函数进入的线程,由主线程调用pthread_create()创建的线程称为子线程,子线程也可以有自己的入口函数,该函数由用户在创建的时候指定。每个线程都有自己的线程ID,可以通过 pthread_self() 函数获取。 最常见的线程模型中,除主线程较为特殊之外,其他线程一旦被创建,相互之间就是对等关系,不存在隐含的层次关系。每个进程可创建的最大线程数由具体实现决定。
.
无论在windows中还是Posix中,主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。 这时整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁后销毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销毁,取决于线程属性。 在这种情况下,主线程和子线程通常定义以下两种关系:

  1. 可会合(joinable):这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。
  2. 相分离(detached):表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。

线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。

1.1、pthread_create() 函数

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

说明:pthreand_create() 用来创建一个线程,并执行第三个参数 start_routine 所指向的函数

  • 第一个参数thread是一个pthread_t类型的指针,他用来返回该线程的线程ID每个线程都能够通过pthread_self()来获取自己的线程ID(pthread_t类型)
  • 第二个参数是线程的属性,其类型是pthread_attr_t类型,其定义如下程序所示:
  • 第三个参数start_routine是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务(函数);
  • 第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去;
typedef struct
{
	int					detachstate;	//线程的分离状态
	int					schedpolicy;	//线程调度策略
	struct sched_param	schedparam;		//线程的调度参数
	int					inheritsched;	//线程的继承性
	int					scope;			//线程的作用域
	size_t				guardsize;		//线程栈末尾的警戒缓冲区大小
	int					stackaddr_set;
	void *				stackaddr;		//线程栈的位置
	size_t				stacksize;		//线程栈的大小
}pthread_attr_t;

对于这些属性,我们需要设定的是线程的分离状态,如果有必要也需要修改每个线程的栈大小每个线程创建后默认是joinable状态,该状态需要主线程调用 pthread_join 等待它退出,否则子线程在结束时,内存资源不能得到释放造成内存泄漏。所以我们创建线程时一般会将线程设置为分离状态,具体有两种方法:

  1. 线程里面调用 pthread_detach(pthread_self()) 这个方法最简单
  2. 在创建线程的属性设置里设置 PTHREAD_CREATE_DETACHED 属性

记住:主线程退出会导致所有的子线程都退出,请在需要使用多线程的时候,首先要保证主线程不会退出。

1.2、函数指针

1.2.1、什么是函数指针

看到 pthread_create() 这个函数的第三个参数 void *(*start_routine) (void *) 是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务(函数)

函数指针,看名字,大家都知道是一个指向函数的指针,但是怎么去理解、怎么去用呢?这里可能就会有小伙伴奇怪了。

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

首先,函数指针是怎么定义的呢

方法1:
比如说,一个函数的原型是↓

void *func(void *arg);

可以看出,该函数的返回值类型是 void *,参数类型是 void *
那么想要定义一个函数指针去指向这个函数,
只需要将函数名 func 用括号括起来,换成 *ptr

void *(*ptr)(void *);

这时候 ptr 就是一个函数指针了,这里要注意,*ptr 一定要加括号,因为括号改变了运算符的优先级,不加括号的话就变成一个函数声明了,* 就会和前面的 void * 结合,变成一个返回值类型为 void ** 的函数。

定义了一个指针变量 ptr,该指针变量可以指向返回值类型为 void * 型,且有一个 void * 类型参数的函数。ptr 的类型为 void *(*)(void *)

所以,函数指针的定义方式为:

函数返回值类型 (* 指针变量名) (函数参数列表);

方法2:
除了上述的这种方式,也可以使用 typedef 来声明一个函数指针

// 声明一个函数原型为void *Fun(void *arg);的函数指针类型
typedef void *(*FUNCP)(void *);

typedef的功能是定义新的类型。这里定义了一种 FUNCP 的类型,并定义这种类型为指向某种函数的指针,这种函数以一个void * 为参数并返回 void * 类型。后面就可以像使用 int,char 一样使用 FUNCP 了。

方法3:
还有一种用法,是先使用 typedef 来声明一个函数类型

// 定义一个原型为void *Fun(void *arg);的函数类型
typedef void *(FUNCP)(void *);

再使用这种类型去定义一个函数指针,指向 void *Func(void *arg); 这个函数

FUNCP *ptr = Func;

个人觉得这种方法更加符合我们的使用习惯

1.2.2、如何调用函数指针

方法1定义的函数指针使用方法:

void *Func(void *arg);	/* 声明一个函数 */
void *(*ptr)(void *);  	/* 定义一个函数指针 */
ptr = Func;				/* 将Func函数的首地址赋给指针变量ptr */

赋值时函数 Func 不需要带括号,也不需要带参数。
由于函数名 Func 代表函数的首地址,因此经过赋值以后,指针变量 ptr 就指向函数 Func() 代码的首地址了。

方法2定义的函数指针使用方法:

//定义了一种 FUNCP 的类型
//并定义这种类型为指向“返回值类型为void *,参数为void *类型的函数”的指针
typedef void *(*FUNCP)(void *);	
void *Func(void *arg);		/* 声明一个函数 */
FUNCP ptr = Func;			/* 定义一个函数指针ptr,指向该类型 */
void *(*ptr)(void *arg);	/* 等价于 void *Func(void *arg); */

方法3定义的函数指针使用方法:

//定义了一种 FUNCP 的类型
//并定义这种类型为“返回值类型为void *,参数为void *类型”的函数
typedef void *(FUNCP)(void *);	
void *Func(void *arg);		/* 声明一个函数 */
FUNCP *ptr = Func;			/* 定义一个函数指针ptr,指向该类型 */
void *(*ptr)(void *arg);	/* 等价于 void *Func(void *arg); */

2、锁

我们试想一下,如果一个资源作为共享资源,会被不同的线程访问修改,那么我们把这个资源叫做临界资源,那么对于访问、修改该资源的代码就叫做临界区。那么怎么解决多个线程之间共享同一个共享资源,是多线程编程需要考虑的一个问题。

2.1、互斥锁

举个生活中最简单的例子,寝室只有一个浴室,那么几位舍友是怎么解决花洒共享的问题的呢?其实就是锁的机制!浴室在这里就是临界资源,我们在进入到浴室(临界区)后,就首先上锁; 然后用完离开浴室(临界区)之后,把锁释放供别人使用。如果有人想去浴室洗澡时发现门锁上了,他也有两种策略:

  1. 在浴室门口等(阻塞);
  2. 暂时先离开等会再过来看(非阻塞);

篇幅有点长了 ,有关锁的内容以及下面的函数,找个时间再另外详细地写一篇

互斥锁在使用之前,需要先调用 pthread_mutex_init() 函数来初始化互斥锁;

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

调用pthread_mutex_lock() 来申请锁,这里是阻塞锁,如果锁被别的线程持有则该函数不会返回;

int pthread_mutex_lock(pthread_mutex_t *mutex);

调用pthread_mutex_trylock 来申请锁,这里使用的是非阻塞锁;如果锁现在被别的线程占用则返回非0值,如果没有被占用则返回0;

int pthread_mutex_trylock( pthread_mutex_t *mutex );

在访问临界资源(shared_var)完成退出临界区时,我们调用pthread_mutex_unlock来释放锁,这样其他线程才能再次访问;

int pthread_mutex_unlock(pthread_mutex_t *mutex);

互斥锁在使用完之后,我们应该调用pthread_mutex_destroy()将他摧毁释放;

int pthread_mutex_destroy(pthread_mutex_t *mutex);

2.2、死锁

如果多个线程要调用多个对象,则在上锁的时候可能会出现“死锁”。举个例子:
A、B两个线程会同时使用到两个共享变量m和n,同时每个变量都有自己相应的锁M和N。
这时A线程首先拿到M锁访问m,接下来他需要拿N锁来访问变量n; 而如果此时B线程拿着N锁等待着M锁的话,就造成了线程“死锁”。

在这里插入图片描述

死锁产生的4个必要条件

  1. 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。【如果一个资源可以被多个进程访问的话,就不会存在死锁了】
  2. 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
  3. 不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  4. 循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。
.
产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
.
其中最有效的解决方法就是:按顺序加锁(大家都按顺序去申请锁(先申请厨房,再申请洗手间),这种情况下,申请不到第一个锁,就自然不会申请第二个锁)

三、具体代码

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

//声明一个新的类型
typedef void *(THREAD_FUNC) (void *);

int thread_start(pthread_t *tid, THREAD_FUNC *thread_workfunc, void *thread_arg);	//封装好的创建线程的函数
void *thread_func(void *thread_arg);	//子线程工作函数

int main(int argc, char **argv)
{
	int					listen_fd = -1;
	int					client_fd = -1;
	int					on = 1;
	struct sockaddr_in	servaddr;
	struct sockaddr_in	cliaddr;
	socklen_t			cliaddr_len = sizeof(struct sockaddr);
	int					server_port;
	int					backlog = 10;
	pthread_t			tid;

	//用来确认程序执行的格式是否正确,不正确则退出并提醒用户
	if (argc < 2)
	{
		printf("Program usage: %s [Port]\n", argv[0]);
		return -1;
	}
	
	//将端口参数赋给参数变量
	//由于命令行传参进来是字符串类型,所以需要atoi转换为整型
	server_port = atoi(argv[1]);

	/*
	 * socket(),创建一个新的sockfd
	 * 指定协议族为IPv4
	 * socket类型为SOCK_STREAM(TCP)
	 */
	listen_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (listen_fd < 0)
	{
		printf("create socket failure: %s\n", strerror(errno));
		return -2;
	}
	printf("create socket[%d] success\n", listen_fd);
	
	//避免上次结束程序时,端口未被及时释放的问题
	setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

	/*
	 * bind(),将服务器的协议地址绑定到listen_fd
	 */
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(server_port);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	if (bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
	{
		printf("socket[%d] bind port[%d] failure: %s\n", listen_fd, server_port, strerror(errno));
		close(listen_fd);
		return -3;
	}
	printf("socket[%d] bind port[%d] success\n", listen_fd, server_port);

	/*
	 * listen()
	 * 监听listen_fd的端口,并设置最大排队连接个数
	 */ 
	listen(listen_fd, backlog);
	printf("Start listening port[%d]\n", server_port);
		
	/*
	 * accept()
	 * 等待并接受来自客户端的连接请求
	 * 如果没有客户端连接服务器的话该程序将一直阻塞着不会返回,直到有一个客户端连过来为止
	 * 返回一个client_fd与客户通信
	 */
	while (1)
	{
		printf("\nStart waitting and accept new client to connect...\n");
		client_fd = accept(listen_fd, (struct sockaddr *)&cliaddr, &cliaddr_len);
		if (client_fd < 0)
		{
			printf("accept new client failure: %s\n", strerror(errno));
			continue;
		}
		printf("accept new client [%s:%d] with fd[%d] success\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), client_fd);
		
		//使用封装的函数,创建一个子线程并设置它的工作函数为thread_func,将client_fd作为参数传给工作函数
		/* 这里为什么传client_fd的值,而不是地址
		 * 是因为传地址的话,万一有新客户端来连接
		 * 新客户端的client_fd值就会将本次的client_fd值覆盖掉
		 * 导致无法与该客户端进行通信
		 */
		thread_start(&tid, thread_func, (void *)client_fd);

	}
		
	/*
	 * close()
	 * 关闭服务器监听的listen_fd
	 */
	printf("Close listen_fd[%d]\n", listen_fd);
	close(listen_fd);

}


int thread_start(pthread_t *tid, THREAD_FUNC *thread_workfunc, void *thread_arg)
{
	int				rv = -1;
	pthread_attr_t	thread_attr;
	
	//初始化线程属性
	if (pthread_attr_init(&thread_attr))
	{
		printf("pthread_attr_init() failure: %s\n", strerror(errno));
		goto cleanup;
	}
	
	//设置栈大小
	if (pthread_attr_setstacksize(&thread_attr, 120*1024))
	{
		printf("pthread_attr_setstacksize() failure: %s\n", strerror(errno));
		goto cleanup;
	}
	
	//设置为相分离,主线程就不需要在子线程退出的时候与其会合
	if (pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED))
	{
		printf("pthread_attr_setdetachstate() failure: %s\n", strerror(errno));
		goto cleanup;
	}
	
	//创建一个子线程
	if (pthread_create(tid, &thread_attr, thread_workfunc, thread_arg))
	{
		printf("pthread_create failure: %s\n", strerror(errno));
		goto cleanup;
	}

	rv = 0;

cleanup:
	//线程属性在使用完之后,调用pthread_attr_destroy把它摧毁释放
	pthread_attr_destroy(&thread_attr);
	return rv;

}

//子线程工作函数
void *thread_func(void *thread_arg)
{	
	int client_fd;
	int rv = -1;
	char buf[1024];
	
	if (!thread_arg)
	{
		printf("Invalid input arguments in %s()\n", __FUNCTION__);
		pthread_exit(NULL);
	}

	client_fd = (int)thread_arg;
	
	printf("socket[%d] in thread[%lu]\n", client_fd, pthread_self());

	//与客户端保持通信
	while (1)
	{
		//清空buf内容,避免因为随机值出现乱码
		memset(buf, 0, sizeof(buf));
		//从客户端中读取数据
		rv = read(client_fd, buf, sizeof(buf));
		if (rv < 0)
		{
			printf("socket[%d] read date from client failure: %s\n", client_fd, strerror(errno));
			close(client_fd);
			pthread_exit(NULL);
		}
		else if (rv == 0)
		{
			printf("socket[%d] get Disconnected\n", client_fd);
			close(client_fd);
			pthread_exit(NULL);
		}
		else
		{
			printf("socket[%d] read %d Byte data from client: %s\n", client_fd, rv, buf);
		}
		
		//将小写字母转为大写字母
		for (int i=0; i<rv; i++)
		{
			if (buf[i] >= 'a' && buf[i] <= 'z')
				buf[i] = toupper(buf[i]);
		}

		//发送消息给客户端
		rv = write(client_fd, buf, rv);
		if (rv < 0)
		{
			printf("socket[%d] write date to client failure: %s\n", client_fd, strerror(errno));
			close(client_fd);
			pthread_exit(NULL);
		}
		printf("socket[%d] write %d Byte data to client: %s\n\n", client_fd, rv, buf);
	}	
}

四、运行效果

在这里插入图片描述


总结

以上是对Linux 多线程的一些理解,如有写的不好的地方,还请各位大佬不吝赐教。

  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Simply myself

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值