一、线程池概念与应用
- 基本概念
如果进程中的线程过多会带来调度开销,进而影响缓存局部性和整体性能。比如线程频繁创建和销毁会带来极大的开销。通常用户更加关心的是任务可以并发执行,并不想管理线程的创建,销毁和调度。所以可以利用数据结构将需要处理的任务处理成队列,交由线程池(Thread Pool)统一执行,可以提升任务的执行效率。
- 设计思想
线程池的核心思想是线程复用。在没有线程池的情况下,每次执行任务时都需要创建一个新线程,任务完成后销毁线程。这种频繁的线程创建和销毁会带来很大的性能开销,尤其是线程创建时的内存分配、上下文切换以及线程销毁时的资源回收。
预先创建了一组线程,并将这些线程放入线程池中。当有任务需要执行时,线程池会从池中取出一个空闲线程来执行任务,任务完成后线程不会被销毁,而是返回到线程池中继续等待下一个任务。这样就可以避免频繁创建和销毁线程,提高系统的性能。
采用线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法避免了处理任务时创建销毁线程开销的代价,也避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
从上图可以看到,线程被创建出来之后,都处于睡眠态,它们实际上是进入了条件量的等待队列中。而任务都被放入一个链表,被互斥锁保护起来。下面是线程池里面线程们的一生:
- 被创建
- 写遗书(准备好退出处理函数,防止在持有一把锁的状态中死去),提示:不需要反复
- 试图持有互斥锁(等待任务)
- 判断是否有任务,若无则进入条件量等待队列睡眠,若有则进入第5步
- 从任务链表中取得一个任务
- 释放互斥锁
- 销毁遗书(将备用的退出处理函数弹出,避免占用内存),提示:不需要反复
- 执行任务,完毕之后重回第2步
任务实质上是用户需要交给线程池执行的函数,为了方便线程们执行,一般做法是将函数(即函数指针)及其参数存入一个任务节点,并将节点链成一个链表,形成一个队列。
- 接口说明
练习:设计程序,实现批量拷贝文件的功能,比如本地磁盘上有1个用于存储BMP图片的文件夹,文件夹中有若张帧BMP文件,要求主线程先创建一条子线程,子线程读取该目录下的所有BMP图片的名称(利用目录检索实现),把每个BMP图片的名称和路径构造为一个字符串(表示BMP图片的绝对路径),把这个字符串作为任务的参数,构造为一个任务结点,然后把每个任务结点链在一起形成链队(单向链表实现),然后主线程等待该子线程结束(pthread_join)。
然后主线程创建一个线程池(线程池中应该默认存在一些核心线程),然后把任务队列中的所有任务都添加到线程池中,要求每个线程都会输出一些提示信息(比如在开始拷贝之前,输出”cpoy start.....”,拷贝完成后输出”copy done!”)
拓展要求:要求每个线程在拷贝文件的时候显示拷贝的进度条(linux风格 [#########] )(提示:应该先计算出文件的总大小(以字节为单位),可以使用stat函数获取文件属性中的文件大小,然后计算出当前已经读取出来的字节数量,进行百分比的计算)。