C++ 用户态与内核态线程调度:一场线程的“宫斗戏”
各位观众,大家好!今天咱们来聊聊C++里线程调度这档子事儿。这就像后宫佳丽三千,皇上(操作系统)决定今天宠幸谁,明天又翻谁的牌子。只不过,这里的“佳丽”是线程,而“皇上”是操作系统内核。
咱们先捋捋清楚,啥是用户态线程,啥是内核态线程,它们之间又有什么爱恨情仇。
第一幕:角色登场——用户态线程 vs. 内核态线程
-
内核态线程(Kernel-Level Thread,KLT): 这位可是皇家的正统血脉,由操作系统内核直接管理。创建、销毁、调度都由内核一手包办。Linux的
pthread
库创建的线程,基本上都是内核态线程。每个KLT都有自己的内核线程控制块(TCB),内核直接维护这些TCB。 -
用户态线程(User-Level Thread,ULT): 这位就有点像“私生子”,它不是由内核直接管理,而是由用户程序自己维护的。用户程序自己实现线程库,负责线程的创建、销毁、调度。这就像一个公司内部自己搞了一套线程管理系统,老板(用户程序)说了算。
用一张表来总结一下:
特性 | 内核态线程 (KLT) | 用户态线程 (ULT) |
---|---|---|
管理者 | 操作系统内核 | 用户程序 |
创建销毁 | 内核调用 | 用户程序调用 |
调度者 | 操作系统内核 | 用户程序实现的线程库 |
切换代价 | 较高,需要陷入内核态 | 较低,无需陷入内核态 |
阻塞影响 | 一个线程阻塞,不会影响其他线程运行,只要别的线程没被阻塞。 | 一个线程阻塞,整个进程都会阻塞(除非使用异步IO等技巧) |
资源占用 | 每个线程都有独立的内核资源,占用较多 | 资源占用较少,依赖于进程的资源 |
第二幕:内核态线程的“皇权特许”
咱们先看看内核态线程的调度。这部分比较简单,因为一切都由内核说了算。
调度策略:
操作系统内核通常会采用一些经典的调度算法,比如:
- 先来先服务(FCFS): 谁先来,谁先执行。公平是公平,但效率不高,容易出现“长作业霸占CPU”的情况。
- 短作业优先(SJF): 谁执行时间短,谁先执行。能有效提高吞吐量,但需要预先知道每个线程的执行时间,不太现实。
- 优先级调度: 给每个线程分配一个优先级,优先级高的先执行。优先级可以静态设置,也可以动态调整。
- 时间片轮转(RR): 给每个线程分配一个时间片,用完就切换到下一个线程。保证公平性,但时间片过短会增加上下文切换的开销。
- 多级反馈队列(MLFQ): 这是个比较复杂的调度算法,它结合了优先级调度和时间片轮转。有多个就绪队列,每个队列的优先级不同,时间片大小也不同。线程根据运行情况在不同队列之间移动。
代码示例(模拟内核态线程调度):
虽然我们不能直接模拟内核的调度,但可以用C++模拟一个简单的优先级调度器:
#include <iostream>
#include <queue>
#include <vector>
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>
using namespace std;
// 线程结构体
struct Thread {
int id;
int priority;
int runtime; // 线程需要运行的时间
bool finished = false;
Thread(int id, int priority, int runtime) : id(id), priority(priority), runtime(runtime) {}
};
// 自定义比较函数,用于优先级队列
struct CompareThreads {
bool operator()(Thread const& t1, Thread const& t2) {
// 优先级高的线程排在前面
return t1.priority < t2.priority;
}
};
class SimpleScheduler {
public:
SimpleScheduler() {}
// 添加线程
void addThread(Thread thread) {
lock_guard<mutex> lock(queue_mutex);
thread_queue.push(thread);
cv.notify_one(); // 通知调度器有新线程加入
}
// 调度线程
void run() {
while (true) {
unique_lock<mutex> lock(queue_mutex);
cv.wait(lock, [this] { return !thread_queue.empty() || stop_scheduler; }); // 等待队列非空或停止信号
if (stop_scheduler && thread_queue.empty()) {
break; // 停止调度器
}
Thread current_thread = thread_queue.top();
thread_queue.pop();
lock.unlock(); // 释放锁,允许其他线程添加任务
cout << "Running thread " << current_thread.id << " with priority " << current_thread.priority << endl;
// 模拟线程运行
this_thread::sleep_for(chrono::milliseconds(current_thread.runtime * 100));
lock_guard<mutex> lock2(queue_mutex);
cout << "Thread " << current_thread.id << " finished." << endl;
current_thread.finished = true;
if (thread_queue.empty()) {
stop_scheduler = true;
}
}
cout << "Scheduler stopped." << endl;
}
void stop() {
{
lock_guard<mutex> lock(queue_mutex);
stop_scheduler = true;
}
cv.notify_one();
}
private:
priority_queue<Thread, vector<Thread>, CompareThreads> thread_queue; // 优先级队列
mutex queue_mutex; // 互斥锁,保护队列
condition_variable cv; // 条件变量,用于线程同步
bool stop_scheduler = false;
};
int main() {
SimpleScheduler scheduler;
// 创建线程
Thread t1(1, 1, 5); // ID 1, 优先级 1, 运行时间 5
Thread t2(2, 3, 2); // ID 2, 优先级 3, 运行时间 2
Thread t3(3, 2, 3); // ID 3, 优先级 2, 运行时间 3
// 启动调度器
thread scheduler_thread(&SimpleScheduler::run, &scheduler);
// 添加线程到调度器
scheduler.addThread(t1);
scheduler.addThread(t2);
scheduler.addThread(t3);
// 等待调度器完成
scheduler_thread.join();
return 0;
}
代码解释:
Thread
结构体: 表示一个线程,包含ID、优先级、运行时间等信息。CompareThreads
结构体: 用于优先级队列的比较函数,优先级高的排在前面。SimpleScheduler
类: 模拟一个简单的优先级调度器。addThread()
: 添加线程到优先级队列。run()
: 调度线程,从队列中取出优先级最高的线程执行,直到队列为空。使用互斥锁和条件变量保证线程安全。
main()
函数: 创建线程并添加到调度器,启动调度器线程。
这个代码仅仅是个演示,真实的内核调度远比这复杂得多,涉及到中断处理、上下文切换等底层操作。
上下文切换:
当内核决定切换到另一个线程时,需要保存当前线程的状态(CPU寄存器、程序计数器等),然后加载下一个线程的状态。这个过程称为上下文切换,是非常耗时的操作。所以,频繁的上下文切换会降低系统性能。
第三幕:用户态线程的“夹缝求生”
用户态线程的调度就比较复杂了。因为内核根本不知道用户态线程的存在,它只知道进程的存在。用户态线程的调度完全由用户程序自己控制。
调度策略:
用户程序可以实现各种各样的调度策略,比如:
- 合作式调度(Cooperative Scheduling): 每个线程主动放弃CPU,让给其他线程。如果一个线程一直不放弃CPU,其他线程就永远无法执行。这种调度方式简单,但容易出现“线程饿死”的情况。
- 抢占式调度(Preemptive Scheduling): 线程库使用定时器中断,强制切换线程。这种调度方式更公平,但也更复杂。
代码示例(用户态线程库):
下面是一个简单的用户态线程库的示例,使用合作式调度:
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include <setjmp.h>
#include <signal.h>
#include <unistd.h>
#include <cstring>
using namespace std;
// 线程栈大小
const int STACK_SIZE = 1024 * 1024;
// 线程状态
enum ThreadState {
READY,
RUNNING,
BLOCKED,
FINISHED
};
// 线程控制块 (TCB)
struct ThreadContext {
jmp_buf jmp_buf; // 保存线程的上下文
ThreadState state;
void* stack; // 线程栈
function<void()> task; // 线程执行的任务
};
// 线程库
class UserThreadLibrary {
public:
UserThreadLibrary() {
// 初始化主线程
ThreadContext main_context;
main_context.state = RUNNING;
main_context.stack = nullptr; // 主线程使用系统栈
threads.push_back(main_context);
current_thread_id = 0;
}
// 创建线程
int createThread(function<void()> task) {
ThreadContext new_context;
new_context.state = READY;
new_context.stack = malloc(STACK_SIZE); // 分配线程栈
new_context.task = task;
// 初始化线程栈
char* stack_top = (char*)new_context.stack + STACK_SIZE;
memset(new_context.stack, 0, STACK_SIZE); // 清零栈
// 设置线程的上下文
if (setjmp(new_context.jmp_buf) == 0) {
// 初始化栈指针
stack_top -= 8; // 调整栈指针,模拟函数调用
uintptr_t* rsp = (uintptr_t*)stack_top;
*rsp = (uintptr_t)threadWrapper; // 设置返回地址为threadWrapper
// 设置寄存器
new_context.jmp_buf[0].__jmpbuf[4] = (uintptr_t)stack_top; // RSP
new_context.jmp_buf[0].__jmpbuf[5] = (uintptr_t)&task; // R13, 这里传函数指针
}
threads.push_back(new_context);
return threads.size() - 1; // 返回线程ID
}
// 调度线程
void yield() {
int current_id = current_thread_id;
int next_id = (current_id + 1) % threads.size();
// 保存当前线程的上下文
if (setjmp(threads[current_id].jmp_buf) == 0) {
// 切换到下一个线程
current_thread_id = next_id;
longjmp(threads[next_id].jmp_buf, 1);
}
}
private:
vector<ThreadContext> threads; // 线程列表
int current_thread_id; // 当前运行的线程ID
// 线程包装函数
static void threadWrapper(function<void()>* task) {
(*task)(); // 执行线程的任务
// 线程结束
int current_id = getCurrentThreadId();
threads[current_id].state = FINISHED;
free(threads[current_id].stack);
// 调度到下一个线程
yield();
}
// 获取当前线程ID
static int getCurrentThreadId() {
return instance->current_thread_id;
}
public:
static UserThreadLibrary* instance;
// 初始化静态成员变量
static void init() {
instance = new UserThreadLibrary();
}
};
UserThreadLibrary* UserThreadLibrary::instance = nullptr;
int main() {
// 初始化用户线程库
UserThreadLibrary::init();
UserThreadLibrary* utl = UserThreadLibrary::instance;
// 创建线程
int thread1_id = utl->createThread([]() {
for (int i = 0; i < 5; ++i) {
cout << "Thread 1: " << i << endl;
utl->yield(); // 主动放弃CPU
}
});
int thread2_id = utl->createThread([]() {
for (int i = 0; i < 5; ++i) {
cout << "Thread 2: " << i << endl;
utl->yield(); // 主动放弃CPU
}
});
// 运行主线程
for (int i = 0; i < 5; ++i) {
cout << "Main Thread: " << i << endl;
utl->yield(); // 主动放弃CPU
}
return 0;
}
代码解释:
ThreadContext
结构体: 存储线程的上下文信息,包括栈、状态、任务等。UserThreadLibrary
类: 模拟一个用户态线程库。createThread()
: 创建线程,分配栈空间,设置线程上下文。yield()
: 线程主动放弃CPU,切换到下一个线程。使用setjmp
和longjmp
实现上下文切换。threadWrapper()
: 线程的包装函数,负责执行线程的任务,并在线程结束后释放资源。
main()
函数: 创建线程并运行。
重点:setjmp
和 longjmp
这两个函数是实现用户态线程上下文切换的关键。
setjmp(jmp_buf)
: 保存当前线程的上下文到jmp_buf
中,返回0。longjmp(jmp_buf, value)
: 从jmp_buf
中恢复上下文,程序从setjmp
的地方继续执行,但setjmp
返回value
。
用户态线程的优势与劣势:
- 优势: 切换速度快,资源占用少,可以灵活定制调度策略。
- 劣势: 依赖于进程,一个线程阻塞会导致整个进程阻塞。而且,由于内核不知道用户态线程的存在,无法利用多核CPU的优势。
第四幕:M:N 线程模型——“左右逢源”
为了结合内核态线程和用户态线程的优点,出现了M:N线程模型。
- M:N 模型: 将M个用户态线程映射到N个内核态线程上。用户态线程的调度由用户程序自己控制,内核态线程的调度由内核控制。这样既可以利用多核CPU的优势,又可以减少上下文切换的开销。
举个例子: 假设有8个用户态线程,映射到4个内核态线程上。这4个内核态线程在4个CPU核心上并行运行,每个核心上可以运行2个用户态线程。
第五幕:C++11 与线程
C++11 引入了标准线程库 <thread>
,它底层通常使用内核态线程(例如 Linux 下的 pthread
)。虽然我们也可以自己实现用户态线程库,但在实际开发中,很少会这样做,因为标准线程库已经足够强大和方便了。
总结:
线程调度是一门复杂的学问,涉及到操作系统的底层机制。理解用户态线程和内核态线程的区别,以及各种调度策略,可以帮助我们更好地编写并发程序,提高程序的性能。
记住,选择哪种线程模型,取决于具体的应用场景。如果对性能要求非常高,可以考虑M:N模型或者自己实现用户态线程库。但大多数情况下,使用C++11的标准线程库就足够了。
这场线程的“宫斗戏”就先讲到这里,希望大家有所收获!