1.引入
1.1 进程的描述
进程的引入:在多道程序中,为了使程序并发执行,并且可以对并发执行的程序加以描述和控制,人们引入了“进程”的概念。为了使参与并发执行的程序都能够独立地执行,操作系统必须为之配置一个数据结构——进程控制块(PCB)。从而有了进程实体的概念——程序段,相关的数据段和PCB(又称为进程映像)。从而我们可以把传统OS中的进程定义为“是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。”
进程的定义:所以可以说,进程是一段可并发执行的具有独立功能的程序,是关于某个数据集上的一次执行过程,也是OS进行资源分配,调度和保护的基本单位。
程序并发执行所付出的时空开销:为使程序能够并发执行,系统必须进行以下操作:1)创建进程;2)撤销进程;3)进程切换。
1.2 线程的引入
为了减少进程在并发执行中的所付出的时空开销,使OS具有更好的并发性,提高CPU利用率。我们引入线程,某种意义上,可以把线程看作是“不具备单独的地址空间的进程”。
以下是线程和进程在6方面的对比表:
对比方面 | 进程 | 线程 |
---|---|---|
1. 调度的基本单位 | 进程是操作系统进行调度的基本单位。每个进程都有独立的代码、数据和堆栈。 | 线程是轻量级的调度单位,属于进程的一部分。线程共享进程的代码段和数据段,但有独立的堆栈和寄存器。 |
2. 并发性 | 进程之间可以并发执行,但由于创建和上下文切换开销大,并发效率相对较低。 | 线程之间可以并发执行,并且线程切换的开销较低,因此并发效率较高。 |
3. 拥有资源 | 进程拥有独立的资源(如地址空间、文件句柄等),是资源分配的基本单位。 | 线程不拥有资源,线程共享其所属进程的资源(如地址空间、文件句柄等)。 |
4. 独立性 | 进程之间相互独立,进程有自己的地址空间,操作互不干扰。 | 线程之间不完全独立,共享进程资源,因此线程之间的操作可能互相干扰,需要同步机制避免资源竞争问题。 |
5. 系统开销 | 由于进程需要独立的资源和地址空间,创建、销毁以及上下文切换的开销较大。 | 线程创建、销毁以及上下文切换的开销较小,因为线程共享进程资源,不需要分配独立的地址空间。 |
6. 支持多处理机系统 | 进程可以分布在多个处理器上运行,但进程之间的通信需要通过操作系统提供的机制(如管道、消息队列等),效率较低。 | 线程可以分布在多个处理器上运行,且线程之间的通信更高效,因为它们共享进程的地址空间和资源。 |
1.3 异步任务
'异步"——指的是时间上不同时刻发生的操作。在编程中,异步意味着发起一个任务后,无需立即等待任务完成,而是继续可以执行其他的工作。当任务完成时,会通过回调(callback),promise, 事件或其他机制通知程序。
异步任务是指哪些无需程序立即等待完成的任务。例如:网络请求等。
2.线程的实现
两种类型的线程实现:
(1)内核支持线程(KST)
是在内核支持下执行的线程。无论是用户进程中的线程,还是系统中的线程,它们的创建、撤销和切换等操作都是依靠内核,在内核空间中实现的。在内核空间中还未每个内核支持线程设置了TCB(线程控制块),内核根据该TCB感知某线程的存在并对实施控制。
(2)用户级线程(ULT)
用户级线程是仅存在于用户空间中的线程,无需内核支持。这种线程的创建与撤销,线程间的同步与通信功能,都无需利用系统调用实现。用户级线程的切换通常发生在一个应用程序的诸多线之间,同样无需内核支持。
以下是用户级线程和内核支持线程的区别,从多个角度进行详细分析:
对比角度 | 用户级线程 | 内核支持线程 |
---|---|---|
1. 实现方式 | 用户级线程完全由用户空间的线程库实现,操作系统内核对此不可见。 | 内核支持线程由操作系统内核直接管理,线程的创建、调度和销毁都由内核负责。 |
2. 调度管理 | 线程调度由线程库在用户空间完成,线程切换不需要内核的参与,因此调度开销较小。 | 线程调度由操作系统内核完成,线程切换需要系统调用,因此调度开销较大。 |
3. 性能 | 切换线程时仅需在用户空间完成上下文切换,无需进入内核态,因此性能较高。 | 切换线程时需要内核态和用户态之间切换,存在一定的性能开销。 |
4. 系统支持 | 不依赖操作系统的线程支持,跨平台性更强,但无法利用多处理器的并行能力。 | 依赖操作系统的线程支持,能够充分利用多处理器实现真正的并行执行。 |
5. 阻塞操作 | 如果一个用户级线程发生阻塞(如I/O操作),整个进程都会阻塞,因为操作系统无法识别线程的存在。 | 如果一个内核支持线程阻塞,操作系统可以调度其他线程继续执行,不会阻塞整个进程。 |
6. 开销 | 由于完全在用户空间实现,线程的创建、销毁和切换开销较小,无需系统调用。 | 线程的创建、销毁和切换都需要系统调用,因此开销较大。 |
7. 并行能力 | 由于内核只管理进程,无法识别用户级线程,因此即使在多处理器环境下,用户级线程也只能在一个处理器上执行(模拟并发)。 | 内核支持线程可以在多个处理器上同时运行,能够实现真正的并行执行。 |
8. 开发灵活性 | 用户级线程可以根据应用需求定制调度策略,灵活性高,但需要开发者自行处理阻塞问题。 | 内核支持线程的调度完全由操作系统控制,开发者无需关心底层细节,易于编程,但灵活性较低。 |
9. 调试难度 | 由于完全运行在用户空间,调试用户级线程较为困难,尤其是在遇到线程调度或阻塞问题时。 | 内核支持线程由操作系统管理,调试工具较为完善,调试和诊断相对简单。 |
10. 典型应用场景 | 适用于对性能要求高、线程数量多但线程阻塞较少的场景,例如科学计算中的并发模拟。 | 适用于需要频繁阻塞操作或多处理器支持的场景,例如多线程服务器和高性能并发应用。 |
3. 线程池?
3.1 什么是线程池?
”A thread pool is a pool threads that can be "reused" to execute tasks, so that each thread may execute more than one task. A thread pool is an alternative to creating a new thread for each task you need to execute.“
“线程”没什么好说的,是 CPU 调度的最小单位,也是操作系统的一种抽象资源。
“池”?水池装着水,线程池则是装着线程,是一种抽象的指代。
抽象的来说,可以当做是一个池子中存放了一堆线程,故称作线程池。简而言之,线程池是指代一组预先创建的、可以复用的线程集合。这些线程由线程池管理,用于执行多个任务而无需频繁地创建和销毁线程。
这是一个典型的线程池结构。线程池包含一个任务队列,当有新任务加入时,调度器会将任务分配给线程池中的空闲线程进行执行。线程在执行完任务后会进入休眠状态,等待调度器的下一次唤醒。当有新的任务加入队列,并且有线程处于休眠状态时,调度器会唤醒休眠的线程,并分配新的任务给它们执行。线程执行完新任务后,会再次进入休眠状态,直到有新的任务到来,调度器才可能会再次唤醒它们。
图中线程1 就是被调度器分配了任务1,执行完毕后休眠,然而新任务的到来让调度器再次将它唤醒,去执行任务6,执行完毕后继续休眠。
3.2 线程池的实现
3.2.1 辅助工具类
在BS::thread_pool中一个关键的辅助工具类——multi_future,它扩展了std::vector<std::future<T>> ,提供了对多个异步任务结果的管理以及操作能力。template <typename T> class [[nodiscard]] multi_future : public std::vector<std::future<T>> { public: // Inherit all constructors from the base class `std::vector`. using std::vector<std::future<T>>::vector; [[nodiscard]] std::conditional_t<std::is_void_v<T>, void, std::vector<T>> get() { if constexpr (std::is_void_v<T>) { for (std::future<T>& future : *this) future.get(); return; } else { std::vector<T> results; results.reserve(this->size()); for (std::future<T>& future : *this) results.push_back(future.get()); return results; } } [[nodiscard]] std::size_t ready_count() const { std::size_t count = 0; for (const std::future<T>& future : *this) { if (future.wait_for(std::chrono::duration<double>::zero()) == std::future_status::ready) ++count; } return count; } [[nodiscard]] bool valid() const noexcept { bool is_valid = true; for (const std::future<T>& future : *this) is_valid = is_valid && future.valid(); return is_valid; } void wait() const { for (const std::future<T>& future : *this) future.wait(); } template <typename R, typename P> bool wait_for(const std::chrono::duration<R, P>& duration) const { const std::chrono::time_point<std::chrono::steady_clock> start_time = std::chrono::steady_clock::now(); for (const std::future<T>& future : *this) { future.wait_for(duration - (std::chrono::steady_clock::now() - start_time)); if (duration < std::chrono::steady_clock::now() - start_time) return false; } return true; } template <typename C, typename D> bool wait_until(const std::chrono::time_point<C, D>& timeout_time) const { for (const std::future<T>& future : *this) { future.wait_until(timeout_time); if (timeout_time < std::chrono::steady_clock::now()) return false; } return true; } }; // class multi_future