最近在搞毕设,重新复习了一遍leveldb的源代码,看了一下其中的后台线程调度模型. 先说一个大概的流程:

整个的模型非常的简单,大体就分为3步.
- 先是在有可能需要进行compaction时调用MaybeScheduleCompaction()
- 随后MaybeScheduleCompaction()会调用不同环境中的schedlue()函数
- 在schedule()函数中创建后台线程,执行相应的任务
接下来来讲一下其中的细节.
首先是http://db_impl.cc 里面的MaybeScheduleCompaction()函数,作用是启动或调用后台线程执行compaction task.
void DBImpl::MaybeScheduleCompaction() {
mutex_.AssertHeld();
if (background_compaction_scheduled_) {
// 后台已经进行了schedule,无需compaction
} else if (shutting_down_.load(std::memory_order_acquire)) {
// DB 正在被删除,无需进行compaction
} else if (!bg_error_.ok()) {
// 发生error,无需继续进行compaction
} else if (imm_ == nullptr && manual_compaction_ == nullptr &&
!versions_->NeedsCompaction()) {
// 不满足需要compaction的条件,无需compaction
//条件如下:
//如果没有immutable table, 也没有设置手动compaction,并且也不满足needcompaction的条件
//则不调用后台线程来进行compaction task
} else {
background_compaction_scheduled_ = true;
env_->Schedule(&DBImpl::BGWork, this);//调用后台线程进行compaction
}
}
MaybeScheduleCompaction() 会在当compaction有可能需要进行的时候被调用,调用之后会进行一些基本的条件判断: 如上面代码所示,判断是否已经进行了cheldule,是否正在删除DB,是否在调度之前就已经发生了错误,是否满足compaction所基本需要的条件,在进行这些条件判断之后便决定是否调用后台background_thread ,如果这些条件都满足,那么就可以成功调用 env_->Schedule() 来使用后台线程.
下面是http://env_posix.cc 文件中 PosixEnv类中的 Schedule()函数.
void PosixEnv::Schedule(
void (*background_work_function)(void* background_work_arg),
void* background_work_arg) {
//加锁
background_work_mutex_.Lock();
if (!started_background_thread_) {
//如果是第一次调用Schedule, 那么此时后台线程还没有被创建,需要新创建这个后台线程
started_background_thread_ = true;
std::thread background_thread (PosixEnv::BackgroundThreadEntryPoint, this);
background_thread.detach();
}
// 如果此时bg_work_queue为空,则说明此时线程正在cv变量上被阻塞,唤醒它
if (background_work_queue_.empty()) {
background_work_cv_.Signal();
}
//在bg_work_queue中插入bg_work函数(其实就是compaction函数)
background_work_queue_.emplace(background_work_function, background_work_arg);
background_work_mutex_.Unlock(); //解锁
}
Schedule()函数基本干的就是下面几件事情:
- 首先查看后台线程是否启动,如果没有启动过的话,就启动后台线程,然后执行detach()函数,detach()函数的作用是使其和父线程脱离,在父亲线程结束之后,依然继续运行.
- 查看bgworkqueue是否为空,如果为空则该线程则可能在cv上阻塞,那么可以唤醒它
- 在bgworkqueue中插入一个新的元素(实际上是compaction函数)
从上面我们可以看到,后台线程只有一个,他承担了所有compaction的工作.
接下来来看一下这个后台线程具体是什么样子的.
BackgroundThreadMain()函数,在http://env_posix.cc 文件中,它被BackgroundThreadEntryPoint()所调用,也就是在Schedule()中创建的后台线程.
static void BackgroundThreadEntryPoint(PosixEnv* env) {
env->BackgroundThreadMain();
}
下面是BackgroundThreadMain()的本体.
void PosixEnv::BackgroundThreadMain() {
while (true) {
//死循环,保证后台线程在进程活着的情况下永远不会结束
background_work_mutex_.Lock();
// 如果bg_work_queue为空,则后台线程阻塞
// 此处不能用if是因为c++的pthread有bug
while (background_work_queue_.empty()) {
background_work_cv_.Wait();
}
assert(!background_work_queue_.empty());// assert there is work to be done
auto background_work_function = background_work_queue_.front().function;
void* background_work_arg = background_work_queue_.front().arg;
background_work_queue_.pop();
//取出队首任务,pop,解锁,执行
background_work_mutex_.Unlock();
background_work_function(background_work_arg);
}
}
后台线程是一个永远执行的死循环, 这就保证了在第一次调用之后自始至终都一直存在,直到leveldb 这个进程结束之后才会被销毁(如果不是死循环的话, 根据detach()的特性,他会在运行结束之后被系统回收)
在后台线程中,如果bgworkqueue为空的话,则说明没有任务执行需要执行,那么后台线程将会被阻塞,直到在父线程中加入新的任务并唤醒它.随后便可以取出队首的函数并执行.(注意这里解锁和执行的顺序)
后台线程只有一个,由他来完成所有的compaction工作, 当它由于compaction的I/O工作而阻塞时,它并不能执行其他的compaction任务(毕竟只有一个),直到这个compaction任务彻底结束,它才能继续从work_queue中取出下一个来执行,类似于流水线式的操作.