linux线程池


前言


一、线程池

1、线程池介绍

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

2、线程池实现

下面我们来实现一个简单的线程池。
我们创建一个Thread类,这个类中记录了tid,回调函数这些属性,还有一个ThreadData类类型对象,ThreadData类中记录了线程的名称和回调函数的参数。当我们实例化一个Thread类类型对象时,就表示创建了一个线程。
在这里插入图片描述
然后我们再来实现线程池类。在ThreadPool类中有一个threads_来存线程,然后还有一个task_queue_队列用来存任务。ThreadPool类的构造函数中创建num_个线程,并且将这些线程放入到threads_容器中,这些创建的线程的回调函数都为routine函数。ThreadPool类的run函数会将threads_容器中的线程都启动。
在这里插入图片描述
下面我们来简单的测试这个线程池。我们看到线程都被成功启动,并且这些线程都调用routine函数来运行。
在这里插入图片描述
在这里插入图片描述
下面我们再来向线程池中添加任务,但是当添加任务时,我们就需要考虑线程安全的问题了。因为当线程1还没有向任务队列中添加任务成功时,其它线程不能向任务队列中添加任务。所以我们需要使用互斥锁来保证线程安全。下面我们实现一个lockGuard。
在这里插入图片描述
当有了锁之后就可以保证线程安全了,但是当线程池的任务队列中如果没有任务时,那么线程就不需要占用CPU呢,所以我们再在线程池中添加一个条件变量用来控制线程同步问题,即只有当一个线程向任务队列中添加了一个任务时,此时再根据条件变量唤醒线程来执行这个任务。
在这里插入图片描述
下面我们在线程的回调函数routine函数中让线程执行任务,但是因为routine函数为静态成员函数,所以无法访问非静态成员变量task_queue_。所以我们在创建线程时可以将this指针作为线程的回调函数的参数传入。这样在routine函数中可以通过ThreadData类型中的args_来得到线程池的this指针,然后通过this指针可以访问线程池的函数。
在这里插入图片描述
下面我们通过routine函数的this指针参数来调用线程池的一些成员函数,然后完成任务的执行过程。
在这里插入图片描述
下面我们实现一个Task任务类,然后让主线程生成任务,让线程池中的新线程来执行这些任务。
在这里插入图片描述
然后我们在主线程中生成任务,让线程池中创建的新线程来执行这些任务。我们看到任务都被线程池中的线程执行了。
在这里插入图片描述
在这里插入图片描述
我们看到上面的程序执行后打印了很多信息,下面我们实现一个日志打印的函数来打印这些信息。
我们让logMessage函数的参数为可变参数。
在这里插入图片描述
我们可以使用下面的方法来操作可变参数。
在这里插入图片描述
然后使用v开头的printf这些函数来打印可变参数。
在这里插入图片描述
下面我们使用这些函数来打印可变参数的信息。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
下面我们将logMessage函数打印的信息分为标准部分和自定义部分,标准部分就是打印信息的级别和实际等。自定义信息就是打印出来用户自定义的信息。
在这里插入图片描述
然后我们将程序中打印信息的地方都换成调用logMessage函数来打印信息。
在这里插入图片描述
在这里插入图片描述
我们还可以进一步实现logMessage函数,即当我们想要调试这个程序时,我们想要生成DEBUG日志信息,而当我们正常运行程序时,我们不想要打印DEBUG信息,此时我们就可以使用条件编译来完成。
在这里插入图片描述
然后当我们编译这个程序时,如果定义了DEBUG_SHOW这个宏,那么就会打印出来DEBUG信息。
在这里插入图片描述
在这里插入图片描述
而当我们将DEBUG_SHOW这个宏屏蔽时,此时运行程序就不会打印DEBUG信息。
在这里插入图片描述
在这里插入图片描述
我们还可以将打印的日志信息写入到文件中。
在这里插入图片描述
在这里插入图片描述
纠正错误:上面的logMessage函数中打印时间的格式应该是%d占位符,因为%ld表示数据按十进制有符号长型整数输入或输出的占位符,而%d表示数据按十进制有符号整型数输入或输出。我们调用的tm_year等函数返回的都是int类型的值,所以应该使用%d占位符。

下面我们来将这个线程池改为单例模式版的线程池。
我们将ThreadPool类的构造函数,拷贝构造函数等都变为私有,那么除了ThreadPool类里面之外的其它地方就不可以创建ThreadPool对象了。我们在ThreadPool类中创建一个静态成员变量thread_ptr,这个静态成员变量指向一个ThreadPool线程池对象。
在这里插入图片描述
在这里插入图片描述
然后我们让ThreadPool类向外提供一个getThreadPool的静态成员函数,通过该函数可以获取到一个线程池对象的指针,如果第一次调用这个函数那么thread_ptr为nullptr,那么这个函数就会创建一个线程池对象并且返回这个线程池对象的指针,如果后面再调用这个函数时,该函数就不会再创建线程池对象了,而是直接将第一次创建的线程池对象返回。这样就做到了整个程序中只有一个线程池对象了。
在这里插入图片描述
我们如果想要创建一个线程池对象,那么就只能通过调用getThreadPool函数来获取了。
在这里插入图片描述
上面的实现还存在一个问题,即当在main主线程中,如果有多个线程都执行了getThreadPool函数,然后同时判断thread_ptr==nullptr时,那么就会出现问题。即getThreadPool这个函数是线程不安全的。所以我们需要给这个函数进行加锁来保证其线程安全。我们在ThreadPool类中再创建一个静态成员变量锁,然后在类外对这个锁进行初始化,因为这个锁为静态成员,所以我们可以直接对其进行初始化。
在这里插入图片描述
然后我们对getThreadPool函数中的代码进行加锁。但是这样实现还存在一个问题,当未来任何一个线程想获取单例时,都必须调用getThreadPool接口,那么就一定会存在大量的申请和释放锁的行为,这个是无用且浪费资源的。所以我们可以再加一个判断,这样就可以减少线程申请锁释放锁的行为。即如果有线程已经第一次成功创建了线程池后,那么thread_ptr就不为空了,那么其它线程当判断thread_ptr不为nullptr了之后,就不会再进行申请锁和释放锁的操作了,而是直接向下执行return语句了,getThreadPool函数会将已经创建过的线程池对象的指针返回了。
在这里插入图片描述
在这里插入图片描述

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值