c++实现线程池(真实入门!)

10分钟学会用C++编写一个简单的线程池(使用windows.h头文件)

前言

这是我们操作系统专业课的一次编程作业,作为菜鸡的我搜索了大量的线程池博客,但不是代 码太秀,比较难懂,就是实现的线程池功能太过强大,不适合入门,或是讲解重点并没解决我 的问题,而不同博客使用的头文件又不同;很难有只需一篇就能让菜鸡搞懂的博客。

所以我打算以我从零开始理解的角度完成这篇 实验报告 博客,能让最菜的菜鸡也能看懂并手 动实现一个最简单的线程池。

但要做到这一点,需要有一点点前提条件

  1. 只需要有简单的c++面向对象编程的能力。
  2. 会使用win自带的 “windows.h” 头文件进行多线程编程。
  3. 对线程有基本的理解。

而我们最终能得到的线程池拥有的功能有

  1. 实现线程池最基本的功能能:线程重复利用,少量线程完成大量工作(线程数<任务 数)。
  2. 可自己设计任务函数,而不是简单的模拟。(同GreatThread() 传入参数的方法类似)
  3. 可调节线程池的线程数,空闲线程等待时间等。
  4. 可随时添加新任务,添加结束后可调用等待函数等待线程完成所有工作。
  5. 不能直接接收返回值,但返回值可利用传入的参数指针传回。

基本概念

  1. 为啥要有线程池
    一般的多线程编程,往往是一个线程对应一个 ‘任务’ 。但由于线程的建立于销毁也会造成一部分开销,所以当任务过多的时候,若还是一个任务对应一个线程,线程的反复建立与销毁,会造成大量的的资源浪费。

  2. 什么是任务
    这一点是大部分博客都没有提及的,以我的理解,这里的任务所指的是你打算在线程函数里面完成的工作,比如打印 “hello word” 或者做一些计算之类的。
    而大部分任务往往都能封装进一个函数,所以这里的任务往往就是指一个有明确目的函数(相同函数传入不同参数也能看成不同的任务)

  3. 什么是线程池
    如果我们重复利用几个线程,去完成比线程数更多的任务,那不就避免了线程建立和销毁造成的开销么。
    一句话说线程池就是一种利用少量线程解决大量任务(线程数 < 任务数)的工具。
    而高级的线程池会从线程的数量,线程的活跃时间,任务的存取策略等角度进行优化,以达到更高效目的。

  4. 如何实现
    我们可以把所有的任务放在一个队列内,并创建一定数量的线程。线程空闲时,可以自动从任务队列中取出任务,任务完成后,又进入空闲,并尝试取出下一个任务。而这,就是最简单的线程池了。

代码实现

“任务”的封装

综上所述,我们需要实现的任务需要有这样的特性:

  1. 创建任务和运行任务可以不同步(不同于简单的调用函数)。
  2. 因为要放入队列,所以需要有具体的数据结构。
  3. 需要固定任务的格式(总不能让用户随便传入啥类型的函数都行吧,得规范一下)

解决方案

首先,统一任务函数的格式:”参数为 void 指针,返回值为void(没有返回值)“。

这样的话,利用void指针可以指向任何数据的特性,任务函数可以传入任意类型的参数了,也可以利用指针传回返回值。

// 声明fp_return_void类型
typedef void(*fp_return_void)(void *paramter);

见:函数指针的用法

其次,将函数指针和参数指针分开来, 并进行封装,想运行函数的时候,直接调用函数指针并把参数传入即可(见run()成员函数)

class Task
{
private:
	// 函数指针和参数指针
	fp_return_void fp;
	void *paramter;

	// 构造函数,需传入函数指针和参数指针
	Task(fp_return_void fp, void *param) :
		fp(fp), paramter(param) {}

	// 运行任务
	void run()
	{
		fp(paramter);
	}

public:// 声明为线程池的友元类
	friend class KyThreadPool;

};

Task使用方法:

// 声明自己的任务函数
void FuncName(void *lpParamter)
{
	int *pmd = (int *)lpParamter;// 假设需要传入int指针,(int *)表示强制类型转换,相当于告诉函数void指针指向的是int类型。
	cout << "task " << pmd[0] << " is working!\n";
}

//声明参数
int x = 1;
int *p = &x;

// 声明一个任务
Task onetask(FuncName, p); 
onetask.run(); // 运行任务

线程池框架构建

根据线程池的概念,线程池的主体必要两个东西:

  1. 线程函数,有任务的时候做任务,没任务的时候等任务,等久了就自动结束。
  2. 任务队列,用于存放任务,供线程函数提取任务用。(见:STL队列用法

还需要一些池子的参数:

  1. 核心线程数/最大线程数(为了简便,假设核心线程数 = 最大线程数)
  2. 空闲线程等待时间,空闲线程也会占用cpu,所以不能让空闲线程活太久
  3. 现存线程数量,活跃线程数量,线程结束标志等,用于管理线程

以及几个供给用户使用的函数:

  1. 等待线程池函数,线程停止接收任务,等待剩余线程完成任务。
  2. 重启线程池函数,使线程池重新工作
  3. 添加任务函数,用于增加一个任务到队列中。

#define CORETHREADNUM 8
#define ALIVETIME 5000

// 线程池,任务默认先进先工作
class KyThreadPool
{
private:
	// 线程队列
	queue<Task> task_queue;

	// 线程函数(必须声明为静态数,才能作为参数传入GreatThread)
	static DWORD WINAPI SingleThread(LPVOID lpParamter);

	// 添加一个线程
	void AddOneThread();
	
	// 线程池信息
	int CoreThreadNum = CORETHREADNUM; // 核心线程数
	int NowThreadNum = 0;              // 现存线程数
	int ThreadEndFlag = 0;             // 线程立即结束标志
	int AliveTime = ALIVETIME;         // 空闲线程等待时间(单位:ms)

	// 互斥锁
	HANDLE TaskQueue_mutex;            // 任务队列读取锁
	HANDLE NowThreadNum_mutex;         // 线程数量修改锁
    HANDLE ActiveThreadNum_mutex;      // 活跃线程数修改锁
    
public:
    // 无参数构造函数,
	KyThreadPool()
	{
	    // 初始化锁
		task_queue_mutex = CreateMutex(NULL, FALSE, NULL);
		now_thread_num_mutex = CreateMutex(NULL, FALSE, NULL);
	}

	// 带参数构造函数,可设置线程信息(线程数,空闲线程生命长度)
	KyThreadPool(int CoreThreadNum, int AliveTime)
	{
		this->CoreThreadNum = CoreThreadNum;
		this->AliveTime = AliveTime;
		// 初始化锁
		task_queue_mutex = CreateMutex(NULL, FALSE, NULL);
		now_thread_num_mutex = CreateMutex(NULL, FALSE, NULL);
	}

    // 添加一个任务到任务队列,参数1:函数指针,参数2:函数参数
	void AddTask(fp_return_void function, void *arg);
	
	// 等待线程结束,使用后队列内添加的新任务不会再执行了
	void WaitAndEnd(void);
	
	// 池子重启,开启线程,执行队列内的任务。
	void PoolRestart(void);
};

成员函数实现

  1. 线程相关函数:SingleThread(), AddOneThread()

核心算法:让其一直循环判定任务队列是否为空,不为空则提取一个任务来完成就行了。

与池子的链接:直接把this指针当作参数传入线程函数即可,这样池子的任务队列以及各种参数就可供其使用了。

线程结束的判断:完成任务后,每次循环会计算其空闲的时间间隔,间隔超过ActiveTime或者接收到退出信息(ThreadEndFlag == 1)就会直接退出。

其它:注意线程状态更改时,对池子的相关参数也进行更改(线程数,活跃线程数等),也要注意共享资源加锁。

//线程函数本身
DWORD WINAPI KyThreadPool::SingleThread(LPVOID lpParamter)
{
	// 记录开始等待的时间
	DWORD wait_start_time = GetTickCount();
	// 这里lpParamter是传入的线程池的this指针,所以需要转换一下
	KyThreadPool *pool = (KyThreadPool*)lpParamter;

	while (1) // 循环等待任务
	{
		if (!pool->task_queue.empty() && pool->ThreadEndFlag == 0)// 如果有正在等待任务
		{
			WaitForSingleObject(pool->TaskQueue_mutex, INFINITE);// 加队列访问锁

			if (!pool->task_queue.empty()) {// 由于不知道.empty()函数是否具有原子性,所以加锁后再判断一遍

				// 取出任务,活跃线程数量+1
				Task T = pool->task_queue.front(); 
				pool->task_queue.pop();
				pool->ActiveThreadNum++;

				// 先解队列锁
				ReleaseMutex(pool->TaskQueue_mutex);

				//再执行任务
				T.run();

				// 记录新的等待开始的时间
				wait_start_time = GetTickCount();

				//活跃线程数量-1
				WaitForSingleObject(pool->ActiveThreadNum_mutex, INFINITE);
				pool->ActiveThreadNum--;
				ReleaseMutex(pool->ActiveThreadNum_mutex);
			}
			// 解队列锁
			else ReleaseMutex(pool->TaskQueue_mutex);
		}

		// 如果等待时间大于ActiveTime,或受到结束信息,则退出死循环,结束线程
		else if (pool->ThreadEndFlag == 1 || GetTickCount() - wait_start_time > pool->AliveTime)
		{
			// 减少一个现存线程数量
			WaitForSingleObject(pool->NowThreadNum_mutex, INFINITE);
			pool->NowThreadNum--;
			ReleaseMutex(pool->NowThreadNum_mutex);

			return 0L;
		}
	}
	return 0L;
}
//增加一个线程,注意细节:传入线程函数的参数是this指针
void KyThreadPool::AddOneThread()
{
	// 新增一个线程
	HANDLE hThread = CreateThread(NULL, 0, SingleThread, this, 0, NULL);

	// 增加一个现存线程数量
	WaitForSingleObject(NowThreadNum_mutex, INFINITE);
	NowThreadNum++;
	ReleaseMutex(NowThreadNum_mutex);
}
  1. 增加一个任务 :AddTask()
// 传入参数:任务函数名(函数指针),任务函数参数
void KyThreadPool::AddTask(fp_return_void function, void *arg)
{

	WaitForSingleObject(TaskQueue_mutex, INFINITE);

	Task task(function, arg);
	this->task_queue.push(task);

	ReleaseMutex(TaskQueue_mutex);

	if (task_queue.size() > 0 && NowThreadNum < CoreThreadNum)
	{
		AddOneThread();
	}
}
  1. 等待以及重启函数:WaitAndEnd(),PoolRestart()
void KyThreadPool::WaitAndEnd(void)
{
    // 等待线程完成队列内的所有任务
	while (ActiveThreadNum > 0 || !task_queue.empty());
	ThreadEndFlag = 1;// 线程结束标记置为1(让线程直接退出等待状态)
}
void KyThreadPool::PoolRestart(void)
{
	// 线程结束标记置为0
	ThreadEndFlag = 0;
	// 建立等于当前任务数量的线程
	for (int i = 0; i < task_queue.size() && i < CoreThreadNum; i++)
	{
		AddOneThread();
	}
}

测试

  1. 任务函数:接收int指针,并打印任务信息(记得给打印设置互斥量)
void TestTask(void *lpParamter)
{
	int *pmd = (int *)lpParamter;// 接收整数参数
	WaitForSingleObject(cout_mutex, INFINITE);
	cout << "Task " << to_string(pmd[0]) << " is working!\n";
	ReleaseMutex(cout_mutex);

	Sleep(1000);// 工作一秒

	WaitForSingleObject(cout_mutex, INFINITE);
	cout << "Task " << to_string(pmd[0]) << " finished!\n";
	ReleaseMutex(cout_mutex);
}
  1. 测试函数:先装载15个任务, 等待后,重启,再装载15个任务(记得最后还得等待)
void KyThreadPoll_test(void)
{
	KyThreadPool PoolTest;
	int a[40];
	for (int i = 0; i < 15; i++)
	{
		a[i] = i;
		PoolTest.AddTask(TestTask, a + i);
	}

	PoolTest.WaitAndEnd();
	cout << "-------------------all Tasks finished!\n";
	PoolTest.PoolRestart();

	for (int i = 15; i < 30; i++)
	{
		a[i] = i;
		PoolTest.AddTask(TestTask, a + i);
	}
	PoolTest.WaitAndEnd();
	cout << "------------------Tasks finished again!\n";
}
  1. 运行结果:可以看到,最多会有8个任务在工作。
    经过多次测试,实验均符合预期。
Task 0 is working!
Task 3 is working!
Task 1 is working!
Task 2 is working!
Task 4 is working!
Task 5 is working!
Task 6 is working!
Task 7 is working!
Task 0 finished!
Task 3 finished!
Task 1 finished!
Task 2 finished!
Task 8 is working!
Task 4 finished!
Task 5 finished!
Task 9 is working!
Task 6 finished!
Task 10 is working!
Task 7 finished!
Task 11 is working!
Task 12 is working!
Task 13 is working!
Task 14 is working!
Task 8 finished!
Task 9 finished!
Task 10 finished!
Task 11 finished!
Task 12 finished!
Task 13 finished!
Task 14 finished!
-------------------all Tasks finished!
Task 15 is working!
Task 20 is working!
Task 19 is working!
Task 17 is working!
Task 18 is working!
Task 16 is working!
Task 21 is working!
Task 22 is working!
Task 15 finished!
Task 23 is working!
Task 20 finished!
Task 19 finished!
Task 24 is working!
Task 17 finished!
Task 25 is working!
Task 18 finished!
Task 26 is working!
Task 16 finished!
Task 21 finished!
Task 27 is working!
Task 22 finished!
Task 28 is working!
Task 29 is working!
Task 23 finished!
Task 24 finished!
Task 25 finished!
Task 26 finished!
Task 27 finished!
Task 28 finished!
Task 29 finished!
---------------------Tasks finished again!
  • 26
    点赞
  • 127
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: 实现线程池的方法有很多,但是最常见的方法是使用队列来维护任务。每个线程都在队列中等待任务,当有新任务到达时,就从队列中取出一个任务并执行。这样,每个线程都可以在并行执行任务,而不需要创建新的线程。 在C语言中实现线程池的代码如下: ``` #include <pthread.h> #include <stdio.h> #include <stdlib.h> #define NUM_THREADS 5 void *print_hello(void *threadid) { long tid; tid = (long)threadid; printf("Hello World! It's me, thread #%ld!\n", tid); pthread_exit(NULL); } int main(int argc, char *argv[]) { pthread_t threads[NUM_THREADS]; int rc; long t; for (t = 0; t < NUM_THREADS; t++) { printf("In main: creating thread %ld\n", t); rc = pthread_create(&threads[t], NULL, print_hello, (void *)t); if (rc) { printf("ERROR; return code from pthread_create() is %d\n", rc); exit(-1); } } pthread_exit(NULL); } ``` ### 回答2: 要实现线程池,首先需要先了解线程池的基本概念和原理。 线程池是一种用来管理和复用线程的技术,它能够维护一个线程队列,按需创建和销毁线程,并将任务分配给这些线程来执行。使用线程池可以提高程序的性能,减少线程创建和销毁的开销。 在C语言中,可以使用多线程的库来实现线程池,比如pthread库。下面是一个简单的用C语言实现线程池的步骤: 1. 定义线程池结构体:创建一个结构体来保存线程池的相关信息,如线程池的大小、任务队列、互斥锁、条件变量等。 2. 初始化线程池:在初始化函数中,需要对线程池中的各个成员进行初始化,如创建指定数量的线程、初始化互斥锁和条件变量等。 3. 定义任务函数:线程池的任务函数用于处理任务队列中的任务,根据具体需求来定义任务的执行逻辑。 4. 添加任务到线程池:当有新的任务时,将任务添加到任务队列中,并通过条件变量来通知线程池中的线程有新任务可执行。 5. 线程池中的线程获取任务并执行:在线程中循环检查任务队列,当有任务时,线程从任务队列中获取任务并执行。 6. 销毁线程池:在停止使用线程池时,要销毁线程池中的资源,包括线程的回收、互斥锁和条件变量的销毁等。 通过以上步骤,就可以在C语言中实现一个简单的线程池。具体实现中还需要考虑线程同步、任务队列的管理等问题,以确保线程池的稳定性和性能。 ### 回答3: 线程池是用来管理和复用线程的一种机制,可以更有效地使用系统资源和提高应用程序的性能。下面是使用C语言实现线程池的一般步骤: 1. 定义一个线程池的结构体,包含线程池的状态、大小、最大线程数、工作任务队列等信息。 ```c typedef struct { pthread_t *threads; // 线程数组 int thread_count; // 线程数 int max_threads; // 最大线程数 int pool_size; // 线程池大小 int shutdown; // 关闭标志 pthread_mutex_t mutex; // 互斥锁 pthread_cond_t notify; // 条件变量 Task *task_queue; // 任务队列 } ThreadPool; ``` 2. 初始化线程池,创建指定数量的线程。 ```c ThreadPool* thread_pool_init(int pool_size) { ThreadPool *pool = malloc(sizeof(ThreadPool)); pool->threads = malloc(pool_size * sizeof(pthread_t)); pool->thread_count = pool_size; pool->max_threads = pool_size; pool->pool_size = 0; pool->shutdown = 0; // 初始化互斥锁和条件变量 pthread_mutex_init(&(pool->mutex), NULL); pthread_cond_init(&(pool->notify), NULL); // 创建线程 for (int i = 0; i < pool_size; i++) { pthread_create(&(pool->threads[i]), NULL, thread_worker, (void*)pool); } return pool; } ``` 3. 定义线程工作函数,不断从任务队列中取出任务执行。 ```c void* thread_worker(void *arg) { ThreadPool *pool = (ThreadPool*)arg; while (1) { pthread_mutex_lock(&(pool->mutex)); // 线程池关闭,退出线程 while (pool->pool_size == 0 && !pool->shutdown) { pthread_cond_wait(&(pool->notify), &(pool->mutex)); } if (pool->shutdown) { pthread_mutex_unlock(&(pool->mutex)); pthread_exit(NULL); } // 从任务队列中取出任务执行 Task *task = pool->task_queue; pool->task_queue = pool->task_queue->next; pool->pool_size--; pthread_mutex_unlock(&(pool->mutex)); task->func(task->arg); free(task); } pthread_exit(NULL); } ``` 4. 定义任务结构体,包含任务函数指针和参数。 ```c typedef struct Task { void (*func)(void*); void *arg; struct Task *next; } Task; ``` 5. 向线程池中添加任务。 ```c void thread_pool_add_task(ThreadPool *pool, void (*func)(void*), void *arg) { Task *task = malloc(sizeof(Task)); task->func = func; task->arg = arg; task->next = NULL; pthread_mutex_lock(&(pool->mutex)); if (pool->task_queue == NULL) { pool->task_queue = task; } else { Task *cur = pool->task_queue; while (cur->next != NULL) { cur = cur->next; } cur->next = task; } pool->pool_size++; pthread_mutex_unlock(&(pool->mutex)); pthread_cond_signal(&(pool->notify)); } ``` 6. 关闭线程池。 ```c void thread_pool_shutdown(ThreadPool *pool) { if (pool == NULL) { return; } pthread_mutex_lock(&(pool->mutex)); pool->shutdown = 1; pthread_mutex_unlock(&(pool->mutex)); pthread_cond_broadcast(&(pool->notify)); // 等待线程退出 for (int i = 0; i < pool->thread_count; i++) { pthread_join(pool->threads[i], NULL); } // 释放资源 free(pool->threads); while (pool->task_queue != NULL) { Task *next = pool->task_queue->next; free(pool->task_queue); pool->task_queue = next; } pthread_mutex_destroy(&(pool->mutex)); pthread_cond_destroy(&(pool->notify)); free(pool); } ``` 以上是一个简单的线程池实现,通过初始化线程池、添加任务、关闭线程池等操作,可以有效地管理和复用线程,提高应用程序的性能。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值