【系统编程】线程池以及API接口简介


        线程池(Thread Pool)是一种并发编程的设计模式,它用于管理和复用线程,以便更有效地处理并发任务。线程池的主要目标是降低线程的创建和销毁成本,提高系统的性能和资源利用率

一、API接口

        API(Application Programming Interface)接口是一组定义了软件组件之间如何互相通信和交互的规范和协议。API允许不同的软件模块、应用程序或系统之间共享功能和数据,从而实现各种复杂的任务和功能。

        我们在学习线程池之前要明白,线程池本身不是一个API接口,而是一种用于管理和执行任务的并发编程模型。然而,线程池通常会被包装在API接口中,以便其他开发人员可以更容易地使用它来执行并发任务。

        API接口通常定义了一组可用于与软件组件、服务或库进行交互的方法和函数。在这种情况下,如果创建了一个包含线程池的库,并提供了一组方法或函数来操作和管理线程池,那么这些方法或函数可以被视为API接口的一部分。

        例如,可以设计一个具有以下功能的线程池API接口:

init_pool():初始化线程池
add_task():向线程池添加任务
add_thread():添加新的工作线程
remove_thread():从线程池中删除工作线程
destroy_pool():销毁线程池

      
        这些方法将构成线程池API接口的一部分,其他开发人员可以使用这些方法来实现并发任务执行,而无需了解线程池的内部工作原理。

        所以,线程池可以成为一个可用于构建API接口的组件,以简化并发编程任务的处理,这就是我们了解AIP接口的目的,当然知道这些还不足以写出一个线程池应用程序,我们还需先了解一下有关线程池的结构体。

二、线程池相关的结构体

        线程池相关的结构体在线程池的设计中扮演着重要角色,用于管理线程池的状态、任务队列等信息。

1.线程池结构体(Thread Pool)

        线程池结构体通常包含了线程池的各种属性和信息,用于管理线程池的整体状态。以下是一般线程池结构体的示例:

typedef struct ThreadPool {
    pthread_mutex_t lock;           // 互斥锁,保护线程池内部数据
    pthread_cond_t cond;            // 条件变量,用于线程之间的同步
    bool shutdown;                  // 线程池销毁标志
    struct Task *task_list;         // 任务链队列
    pthread_t *tids;                // 存储线程ID的数组
    unsigned int waiting_tasks;     // 等待执行的任务数量
    unsigned int active_threads;    // 活跃线程数量
} ThreadPool;
  • lockcond是用于线程同步的互斥锁和条件变量;
  • shutdown标志用于指示线程池是否正在销毁;
  • task_list是一个任务链队列,存储待执行的任务;
  • tids是存储线程ID的数组;
  • waiting_tasks记录等待执行的任务数量;
  • active_threads表示当前活跃线程数量。

2.任务结构体(Task)

        任务结构体表示线程池中的任务,包含了任务函数指针和参数。以下是一个示例:

//任务结构体
struct task
{
	void *(*task)(void *arg);	//返回值为void *的函数指针,参数列表void *arg,表示任务的地址
	void *arg;					//表示任务需要的参数
	struct task *next;			//表示下一个任务的地址
};
  • task 是指向任务函数的指针,任务函数接受一个void *参数;
  • arg 是传递给任务函数的参数;
  • next 是一个指向下一个任务的指针,用于构建任务队列。


        这些结构体协同工作,帮助线程池管理任务的执行和线程的管理。线程池结构体用于维护线程池的状态,任务结构体用于表示具体的任务。线程池中的线程会不断从任务队列中取出任务并执行,同时线程池负责管理线程的生命周期。

三、线程池

        线程池(Thread Pool)是一种并发编程的设计模式,它用于管理和复用线程,以便更有效地处理并发任务。线程池的主要目标是降低线程的创建和销毁成本,提高系统的性能和资源利用率

1.基本原理


        (1)线程复用: 线程池在启动时创建一组线程,这些线程一直保持活动状态,可用于处理任务。线程复用消除了频繁创建和销毁线程的开销,这也是为什么我们要学习线程池的原因。

        (2)任务队列: 线程池通常包括一个任务队列,用于存储等待执行的任务(条件变量)。当任务到达时,线程池将任务放入队列,并从池中的空闲线程中选择一个来执行任务。

        (3)线程调度: 线程池负责调度任务并分配给空闲线程。一旦任务完成,线程将返回池中等待下一个任务。

        (4)线程池大小控制: 线程池的大小通常是有限的,可以根据系统资源和性能需求进行配置。这有助于避免创建过多线程,从而导致资源耗尽和性能下降,也就是做到合理利用资源。

2.组成部分


        (1)线程池管理器(Thread Pool Manager): 负责创建、管理和监控线程池的核心组件。它维护线程池的状态,包括活动线程数、等待任务数等。

        (2)任务队列(Task Queue): 用于存储等待执行的任务。任务可以是函数、方法或对象,线程池从队列中取出任务并将其分配给可用线程。

        (3)工作线程(Worker Threads): 线程池中的线程,用于执行任务。这些线程在初始化时启动,并在完成任务后返回池中以供重用。

        (4)任务接口(Task Interface): 描述任务的接口或抽象类,通常包括一个run方法或函数,线程池根据此接口来执行任务。

四、C语言实现线程池

1. 初始化线程池

          用于初始化线程池,设置线程池的初始状态

原型:

        bool init_pool(thread_pool * pool, unsigned int threads_number);

参数:
        pool        线程池结构体指针,用于表示要初始化的线程池
        threads_number        指定线程池中的初始线程数量

// 初始化线程池结构体里面的成员,根据传入的线程个数,创建线程
bool init_pool(thread_pool *pool, unsigned int threads_number)
{
	//初始化互斥锁
	pthread_mutex_init(&pool->lock, NULL);
	//初始化条件变量
	pthread_cond_init(&pool->cond, NULL);

	// 关闭销毁线程池标识
	pool->shutdown = false;
	// 任务队列头结点
	pool->task_list = malloc(sizeof(struct task)); 
	// 线程ID的指针申请空间
	pool->tids = malloc(sizeof(pthread_t) * MAX_ACTIVE_THREADS);
	// 判断任务队列头结点指针跟线程ID的指针是否申请成功
	if(pool->task_list == NULL || pool->tids == NULL)
	{
		perror("allocate memory error");
		return false;
	}
	//将任务链式队列的下一个节点的地址初始化
	pool->task_list->next = NULL;
	//初始化任务个数0个
	pool->waiting_tasks = 0;
	//初始化活跃线程个数为传入的threads_number个
	pool->active_threads = threads_number;

	int i;
	// 循环创建指定数目线程
	for(i = 0; i<pool->active_threads; i++) 
	{
		//调用pthread_create函数创建线程,线程ID存放在pool->tids的数组里面
		if(pthread_create(&((pool->tids)[i]), NULL, routine, (void *)pool) != 0)
		{
			perror("create threads error");
			return false;
		}
	}

	return true;
}

2. 添加任务

        将一个任务添加到线程池的任务队列中,等待线程池的线程执行。

原型:

        bool add_task(thread_pool *pool, void *(*do_task)(void *arg), void * arg);

参数:
        pool        线程池结构体指针,表示要添加任务的线程池
        do_task        任务函数的指针,表示要执行的任务
        arg        任务函数需要的参数,可以是任何类型的数据

// 往链式任务队列添加任务,单个唤醒线程去执行任务
bool add_task(thread_pool *pool, void *(*task)(void *arg), void *arg)
{
	// 新任务节点
	struct task *new_task = malloc(sizeof(struct task));
	//新任务节点创建失败
	if(new_task == NULL)
	{
		perror("allocate memory error");
		return false;
	}
	//新任务节点的函数指针做初始化
	new_task->task = task;
	//新任务节点的函数指针需要的参数做初始化
	new_task->arg = arg;
	//新任务节点的下一个节点的地址初始化为NULL
	new_task->next = NULL;

	// 访问任务队列前获取互斥锁,此处无需注册取消处理例程
	pthread_mutex_lock(&pool->lock);

	//如果 任务链队列里面任务个数 大于等于 最大任务个数
	if(pool->waiting_tasks >= MAX_WAITING_TASKS)
	{
		//解锁
		pthread_mutex_unlock(&pool->lock);

		fprintf(stderr, "too many tasks.\n");
		free(new_task);

		return false;
	}
	//拿链式队列的头节点指针
	struct task *tmp = pool->task_list;
	//循环遍历找到最后一个节点的地址
	while(tmp->next != NULL)
	{
		tmp = tmp->next;
	}
	// 添加新的任务节点
	tmp->next = new_task;
	// 等待任务个数+1
	pool->waiting_tasks++;

	// 释放互斥锁
	pthread_mutex_unlock(&pool->lock);
	// 并唤醒其中一个阻塞在条件变量上的线程
	pthread_cond_signal(&pool->cond);

	return true;
}

3. 添加活跃线程

        向线程池中添加额外的活跃线程,增加线程池的处理能力。

原型:

        int add_thread(thread_pool *pool, unsigned int additional_threads);

参数:
        pool        线程池结构体指针,表示要添加线程的线程池        
        additional_threads        指定要添加的额外线程数量        

// 根据传入的数量创建线程
int add_thread(thread_pool *pool, unsigned additional_threads)
{
	//添加活跃线程个数为0,不需要往后执行了
	if(additional_threads == 0)
	{
		return 0;
	}
	// 定义一个变量total_threads = 当前活跃线程个数 + 添加活跃线程个数
	unsigned total_threads = pool->active_threads + additional_threads;
                           
	int i, actual_increment = 0;
	// 循环地创建若干指定数目的线程
	for(i = pool->active_threads; i < total_threads && i < MAX_ACTIVE_THREADS; i++)
	{
		if(pthread_create(&((pool->tids)[i]), NULL, routine, (void *)pool) != 0)
		{
			perror("add threads error");
			//添加活跃线程个数如果一开始就为0,表示一个都没有创建成功
			if(actual_increment == 0)
			{
				return -1;
			}
			break;
		}
		actual_increment++;
	}
	//更新活跃线程个数,用创建成功的个数加上之前的个数
	pool->active_threads += actual_increment;

	return actual_increment;
}

4. 删除活跃线程

        从线程池中删除指定数量的活跃线程,减少线程池的处理能力。

原型:

        int remove_thread(thread_pool *pool, unsigned int removing_threads);

参数:
        pool        线程池结构体指针,表示要删除线程的线程池        
        removing_threads        指定要删除的线程数量        

// 根据传入的个数去删除,返回删除之后的剩下的活跃线程个数
int remove_thread(thread_pool *pool, unsigned int removing_threads)
{
	//如果删除的线程个数为0,直接返回当前的活跃线程个数
	if(removing_threads == 0)
	{
		return pool->active_threads;
	}
	//定义一个变量删除之后剩下的线程个数 = 当前活跃线程个数-删除活跃线程个数
	int remain_threads = pool->active_threads - removing_threads;
	//如果删除之后的线程个数小于0,保留一个,如果不小于0,就有多少保留多少

	remain_threads = remain_threads > 0 ? remain_threads : 1;

	int i; 
	// 循环地取消掉指定数目的线程
	for(i = pool->active_threads-1; i>remain_threads-1; i--)
	{
		errno = pthread_cancel(pool->tids[i]);
		if(errno != 0)
		{
			break;
		}
	}
	//如果一个都没有删,返回-1
	if(i == pool->active_threads-1)
	{
		return -1;
	}
	else
	{
		//更新最大活跃线程个数
		pool->active_threads = i+1;
		return i+1;
	}
}

5. 销毁线程池

        销毁线程池,释放线程池占用的资源,并停止线程池的运行。

原型:

        bool destroy_pool(thread_pool *pool);

参数:

  pool        线程池结构体指针,表示要销毁的线程池

// 释放资源
bool destroy_pool(thread_pool *pool)
{
	//线程池销毁标志为真
	pool->shutdown = true;
	//广播唤醒条件变量等待队列里面的线程
	pthread_cond_broadcast(&pool->cond);

	int i;
	for(i=0; i<pool->active_threads; i++)
	{
		//pthread_join( )指定的线程如果尚在运行,那么他将会阻塞等待
		errno = pthread_join(pool->tids[i], NULL);
		if(errno != 0)
		{
			printf("join tids[%d] error: %s\n", i, strerror(errno));
		}
		else
		{
			printf("[%u] is joined\n", (unsigned)pool->tids[i]);
		}	
	}

	free(pool->task_list);
	free(pool->tids);
	free(pool);

	return true;
}

6.线程任务函数

        routine( )函数是线程池中线程的主要工作函数,它执行以下任务:

        ①不断地从任务队列中取出任务;
        ②执行取出的任务;
        ③如果任务队列为空且线程池没有被销毁,则进入等待状态,等待新任务的到来。

//创建线程之后,开始去执行任务,有任务就执行,没有任务,线程进入条件变量等待队列等待唤醒执行任务
void *routine(void *arg)
{
	//接收传入进来的参数pool
	thread_pool *pool = (thread_pool *)arg;
	//定义任务结构体指针变量
	struct task *p;

	while(1)
	{
		// 访问任务队列前加锁,为防止取消后死锁,注册处理例程 handler
		pthread_cleanup_push(handler, (void *)&pool->lock);
		//加锁访问任务队列
		pthread_mutex_lock(&pool->lock);

		// 若当前没有任务,且线程池关闭标志未关闭,则进入条件变量等待队列睡眠,等待新任务的到来或线程池关闭
		while(pool->waiting_tasks == 0 && !pool->shutdown)
		{
			//解锁 进入条件变量等待队列睡眠   收到通知的时候,加锁
			pthread_cond_wait(&pool->cond, &pool->lock);
		}

		// 若当前没有任务,且线程池关闭标识为真,则立即释放互斥锁并退出
		if(pool->waiting_tasks == 0 && pool->shutdown == true)
		{
			//解锁
			pthread_mutex_unlock(&pool->lock);
			//线程退出
			pthread_exit(NULL);
		}

		// 若当前有任务,则消费任务队列中的任务
		// 拿链式队列的下一个节点的地址
		p = pool->task_list->next;
		//将链式队列当前指向,指向下一个
		pool->task_list->next = p->next;
		//任务个数-1
		pool->waiting_tasks--;

		// 释放互斥锁,并弹栈 handler(但不执行他)
		pthread_mutex_unlock(&pool->lock);
		pthread_cleanup_pop(0);

		// 执行任务,并且在此期间禁止响应取消请求,执行期间用pthread_cancel发送取消请求
		pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
		//调用任务结构体里面的函数,传参数
		(p->task)(p->arg);	// task(arg)
		//执行任务完成,并且在此期间使能响应取消请求
		pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
		//释放p指向的地址空间
		free(p);
	}
	//线程退出
	pthread_exit(NULL);
}

7.任务函数

void *mytask(void *arg);

        该函数接受一个 void 指针类型的参数 arg,并返回一个 void 指针。函数主要用于你想要完成的任务操作,这里举例了一个文件复制函数:

//任务函数(想要完成的操作)
void *mytask(void *arg)
{
	//文件复制
	int fd1 = open("./1.txt", O_RDWR);//int fd1 = open(arg->first, O_RDWR);
	if (fd1 == -1)
	{
		perror("open fd1 error");
		return NULL;
	}
	int fd2 = open("./2.txt", O_RDWR|O_CREAT, 0777);//int fd2 = open(arg->second, O_RDWR|O_CREAT, 0777);
	if (fd2 == -1)
	{
		perror("open fd2 error");
		return NULL;
	}
	char buf[1024];
	int size;
	while(1)
	{
		bzero(buf, 1024);
		size = read(fd1, buf, 1024);
		if (size==0)
		{
			break;
		}
		write(fd2, buf, size);
	}
	close(fd1);
	close(fd2);
	return NULL;
}

        这里我们创建了五条线程的一个线程池,来复制一个拥有50个文件大小为45MB的大文件,可以看到用时4.433s,然而如果采用单线程处理的话,至少时间在8s左右。当然具体情况因个人电脑配置不同而异。

五、总结

        让我们用一个类比来解释一下线程池,每辆出租车就是一个线程它的工作是执行各种任务,就像司机可以运送乘客到不同的目的地一样。而线程池就像是一家出租公司这家公司拥有多组出租车(线程),并根据需要分配任务给这些出租车。公司负责管理、维护和监控这些出租车,以确保它们随时可以为乘客提供服务。任务(工作)就像是乘客需要到达的目的地。这可以是任何需要执行的工作,例如计算、数据处理、文件上传等等。线程池接受这些任务,并将它们分配给可用的出租车(线程),从而高效的完成他们。

1.优点

  • 资源控制: 线程池可以限制同时运行的线程数量,有效控制系统资源的使用。
  • 性能提升: 通过减少线程的创建和销毁,线程池可以提高应用程序的性能,减少了线程切换的开销。
  • 任务管理: 线程池可以管理任务的排队和执行,确保任务按照顺序或优先级执行。
  • 可伸缩性: 线程池可以根据系统负载动态调整线程数量,以适应不同的工作负载。

2.适用场景

        线程池适用于需要并发执行多个任务的情况,特别是在以下情况下使用效果更佳:

  • Web服务器处理多个并发请求。
  • 数据库连接池管理多个数据库连接。
  • 后台任务处理,如日志处理、邮件发送、文件处理等。
  • 任何需要并发执行的计算密集型或I/O密集型任务。

3.注意事项

  • 线程池的大小应该根据系统资源和负载需求进行调整,过大的线程池可能会消耗大量内存,而过小的线程池可能会导致任务排队和性能下降。

  • 线程池中的任务应该是独立的,不应该有共享状态或依赖关系,以避免竞态条件和死锁。

  • 异常处理对于线程池非常重要,确保捕获和处理任务中的异常,以避免线程终止和资源泄漏。

        更多C/C++语言Linux系统数据结构ARM板实战相关文章,关注专栏:

   手撕C语言

            玩转linux

                    脚踢数据结构

                            系统、网络编程

                                     探索C++

                                             6818(ARM)开发板实战

📢写在最后

  • 今天的分享就到这啦~
  • 觉得博主写的还不错的烦劳 一键三连喔~
  • 🎉🎉🎉感谢关注🎉🎉🎉
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

祐言QAQ

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

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

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

打赏作者

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

抵扣说明:

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

余额充值