bthread
- bthread是brpc使用的M:N线程库,广义的协程是M:1线程库。我们可以认为bthread是brpc定义的线程,M个bthread映射到N个实际线程pthread上,一般M远大于N。
- 在brpc中,bthread不是指某个具体的类,而是由多个模块、调度算法等构成的模型。bthread模型中有几个模块,TC(TaskControl),TG(TaskGroup),TM(TaskMeta)。TaskControl全局唯一,内有多个TaskGroup,TC负责TG调度等。这里可以认为,TG是可以处理bthread任务的worker,对应具体的线程,也就是TG=workor=pthread,TG的个数实际上就是前面提到的N。TM主要维护单个协程的具体信息,例如协程号、栈空间等。
- 实现bthread M:N模型的关键是Work Stealing算法
- 其他工具类:PL(ParkingLot),WorkStealingQueue
BThread
任务运行的单位,每有一个任务需要执行,则新起一个bthread执行任务,一般来说,执行完之后这个bthread的生命周期即终止了。bthread执行的任务具体来说就是一个函数。
注意bthread不是一个单独的数据结构,一般用bthread id来标识一个bthread,bthread内部数据都存放在bthread id对应的TaskMeta中。与bthread相关的主要是创建、销毁、中断、join等函数,这些函数也不是用成员函数定义,而是通过声明一些全局函数
在brpc服务中,线程可以简单分为两种:工作线程和非工作线程。工作线程用于执行bthread的任务,在brpc中称为worker,每个worker都会有一个对应的TaskGroup。剩余非工作线程可能是主线程,或者用户自己创建的线程。
主要函数
- start_from_non_worker
这个函数主要用于在非工作线程上,生成协程完成任务。主要逻辑就是从TaskControl(管理所有worker)中选择一个worker来执行该bthread任务。注意,不管是使用BU`在这里插入代码片`TIL_FORCE_INLINE int start_from_non_worker(bthread_t* __restrict tid, const bthread_attr_t* __restrict attr, void * (*fn)(void*), void* __restrict arg) { TaskControl* c = get_or_new_task_control(); if (NULL == c) { return ENOMEM; } if (attr != NULL && (attr->flags & BTHREAD_NOSIGNAL)) { // Remember the TaskGroup to insert NOSIGNAL tasks for 2 reasons: // 1. NOSIGNAL is often for creating many bthreads in batch, // inserting into the same TaskGroup maximizes the batch. // 2. bthread_flush() needs to know which TaskGroup to flush. TaskGroup* g = tls_task_group_nosignal; if (NULL == g) { g = c->choose_one_group(); tls_task_group_nosignal = g; } return g->start_background<true>(tid, attr, fn, arg); } return c->choose_one_group()->start_background<true>( tid, attr, fn, arg); }
bthread_start_urgent
(立即执行任务),还是 - bthread_start_urgent
如果是工作线程,则使用当前worker立即执行,当前worker正在执行的任务放到队列中等待执行,否则通过start_from_non_worker选择一个worker来执行。int bthread_start_urgent(bthread_t* __restrict tid, const bthread_attr_t* __restrict attr, void * (*fn)(void*), void* __restrict arg) { bthread::TaskGroup* g = bthread::tls_task_group; if (g) { // 工作线程 // start from worker return bthread::TaskGroup::start_foreground(&g, tid, attr, fn, arg); } return bthread::start_from_non_worker(tid, attr, fn, arg); // 非工作线程 }
- bthread_start_background
bthread_start_background和bthread_start_urgent不同的地方是,bthread_start_background不会直接占用当前worker,而是生成一个协程放在队列中等待worker执行。注意,因为bthread有Work Stealing算法,所以加到当前worker对应TG的队列里,不代表一定会被当前worker执行。bthread_start_background相当于pthread_create,生成一个协程(线程)来执行任务,不影响现在正在执行的协程(线程任务)。int bthread_start_background(bthread_t* __restrict tid, const bthread_attr_t* __restrict attr, void * (*fn)(void*), void* __restrict arg) { bthread::TaskGroup* g = bthread::tls_task_group; if (g) { // start from worker return g->start_background<false>(tid, attr, fn, arg); } return bthread::start_from_non_worker(tid, attr, fn, arg); }
- bthread_start_nosteal
bthread_start_nosteal和bthread_start_background类似,都是新起协程执行fn,不会占用当前worker。bthread_start_background不同的是,这个待执行的bthread会根据请求的deliver_key选择一个TG,加到该TG的no stealing队列中,这个将在后面将TG的时候详细说明stealing和no stealing的区别。这里需要知道的是,加到no stealing队列中,该任务只会被该worker执行,并且相比bthread_start_background优先执行。如果短时间多次调用bthread_start_nosteal,并且fn执行时间相对较久且deliver_key映射到的TG相同,那么该worker很有可能被阻塞。// ****Warining:if you fn will do some block op,as bthread_start_nosteal will not steal between work threads // same deliver_key's fn will lead to big latency **** int bthread_start_nosteal(uint64_t deliver_key, bthread_t* __restrict tid, const bthread_attr_t* __restrict attr, void * (*fn)(void*), void* __restrict arg) { bthread::TaskControl* c = bthread::get_or_new_task_control(); if (NULL == c) { return ENOMEM; } //choose target task_goup by deliver_key bthread::TaskGroup *g = c->choose_one_group(deliver_key); if(!g) { LOG(FATAL) << "fatal err,should not happen,choose_one_group empty,deliver_key:" << deliver_key; return ENOMEM; } return g->start_with_nosteal(tid, attr, fn, arg); }
TaskControl
TaskControl是进程所有TaskGroup的管理者,负责生成、调度、管理TaskGroup。
主要成员变量
TaskGroup** _groups;
TG列表butil::atomic<size_t> _ngroup;
_groups的长度butil::atomic<int> _concurrency
;std::vector<pthread_t> _workers;
起的所有worker的pthread idParkingLot _pl[PARKING_LOT_NUM];
PARKING_LOT_NUM=4,用于调度,后续在ParkingLot会详细描述
主要成员函数
-
get_or_new_task_control
inline TaskControl* get_or_new_task_control() { butil::atomic<TaskControl*>* p = (butil::atomic<TaskControl*>*)&g_task_control; TaskControl* c = p->load(butil::memory_order_consume); if (c != NULL) { return c; } BAIDU_SCOPED_LOCK(g_task_control_mutex); c = p->load(butil::memory_order_consume); if (c != NULL) { return c; } c = new (std::nothrow) TaskControl; if (NULL == c) { return NULL; } int concurrency = FLAGS_bthread_min_concurrency > 0 ? FLAGS_bthread_min_concurrency : FLAGS_bthread_concurrency; if (c->init(concurrency) != 0) { LOG(ERROR) << "Fail to init g_task_control"; delete c; return NULL; } p->store(c, butil::memory_order_release); return c; }
获取全局唯一的TaskControl,如果没有则new一个新的,并调用init函数初始化这个TaskControl。初始化时,会传入一个并发数,关系到会初始化的worker数量,初始化时会生成尽量少的worker,这里的concurrency优先使用FLAGS_bthread_min_concurrency的值。后续如果发现worker不够用,并且worker数小于FLAGS_bthread_concurrency,还可以继续增加worker。
-
init(代码省略数据上报、参数校验部分)
int TaskControl::init(int concurrency) { _concurrency = concurrency; // 注意,前面省略了_concurrency==0的判断,也就是说只能init一次。 // Make sure TimerThread is ready. if (get_or_create_global_timer_thread() == NULL) { LOG(ERROR) << "Fail to get global_timer_thread"; return -1; } _workers.resize(_concurrency); // _concurrency数即worker数,理论上_ngroup也等于_concurrency for (int i = 0; i < _concurrency; ++i) { const int rc = pthread_create(&_workers[i], NULL, worker_thread, this); // worker_thread函数即初始化TG,每生成一个worker线程,则会在该线程中声明一个对应的TG,后续worker_thread函数中详细说明。 if (rc) { LOG(ERROR) << "Fail to create _workers[" << i << "], " << berror(rc); return -1; } } // Wait for at least one group is added so that choose_one_group() // never returns NULL. // TODO: Handle the case that worker quits before add_group while (_ngroup == 0) { // 这里是一个比较简单粗暴的操作,避免当前线程生成worker线程后立即退出,worker线程还没有执行create_group成功,这样会导致在使用时choose_one_group返回的是NULL。注意,在brpc中使用choose_one_group获得TC后会直接调用该TC的成员函数,如果返回NULL会崩。 usleep(100); // TODO: Elaborate } return 0; }
-
worker_thread(省略部分非主要代码和log)
void* TaskControl::worker_thread(void* arg) { // 参数传入的arg是创建pthread的TaskControl地址。 TaskControl* c = static_cast<TaskControl*>(arg); TaskGroup* g = c->create_group(); // 生成TG,并且会塞到_groups中 TaskStatistics stat; if (NULL == g) { LOG(ERROR) << "Fail to create TaskGroup in pthread=" << pthread_self(); return NULL; } //set worker id into TaskGroup, for no steal bthread queue create do right signal g->set_worker_thread_id(pthread_numeric_id()); g->set_worker_tid(syscall(SYS_gettid)); tls_task_group = g; // tls_task_group是每个线程声明一个,在创建worker时都会生成TG并赋值给线程的全局TG变量,所以也可以认为如果当前线程的tls_task_group非NULL,则表示当前线程是worker。 c->_nworkers << 1; g->run_main_task(); // 这一步该线程将陷入循环等待,直到该worker被销毁,才会退出 stat = g->main_stat(); BT_VLOG << "Destroying worker=" << pthread_self() << " bthread=" << g->main_tid() << " idle=" << stat.cputime_ns / 1000000.0 << "ms uptime=" << g->current_uptime_ns() / 1000000.0 << "ms"; tls_task_group = NULL; g->destroy_self(); // 会调用TaskGroup的_destroy_group函数,主要是在锁里修改_ngroup、_groups c->_nworkers << -1; return NULL; }
生成工作线程后的初始化工作。
-
choose_one_group
TaskGroup* TaskControl::choose_one_group() { const size_t ngroup = _ngroup.load(butil::memory_order_acquire); if (ngroup != 0) { return _groups[butil::fast_rand_less_than(ngroup)]; } CHECK(false) << "Impossible: ngroup is 0"; return NULL; } TaskGroup* TaskControl::choose_one_group(uint64_t deliver_key) { const size_t ngroup = _ngroup.load(butil::memory_order_acquire); if (ngroup != 0) { return _groups[butil::fmix64(deliver_key)%ngroup]; } CHECK(false) << "Impossible: ngroup is 0"; return NULL; }
如果没有hash key,则随机选一个TG,有则根据hash key选一个TG。
- steal_task
bool TaskControl::steal_task(bthread_t* tid, size_t* seed, size_t offset) { // 1: Acquiring fence is paired with releasing fence in _add_group to // avoid accessing uninitialized slot of _groups. const size_t ngroup = _ngroup.load(butil::memory_order_acquire/*1*/); if (0 == ngroup) { return false; } // NOTE: Don't return inside `for' iteration since we need to update |seed| bool stolen = false; size_t s = *seed; for (size_t i = 0; i < ngroup; ++i, s += offset) { TaskGroup* g = _groups[s % ngroup]; // 从_groups中根据传入的随机数seed获取对应的TG // g is possibly NULL because of concurrent _destroy_group if (g) { if (g->_rq.steal(tid)) { // 先从该TG的_rq中偷 stolen = true; break; } if (g->_remote_rq.pop(tid)) { // 再从_remote_rq中偷 stolen = true; break; } } } *seed = s; // 最后将seed更新成steal成功的TG下标 return stolen; }
这里先从该TG的_rq中偷,再从_remote_rq中偷,是因为想优先执行worker自己push到队列中的bthread,再执行其他线程push给自己的bthread。
TaskMeta
- bthread中TaskMeta的创建和回收都是用ResourcePool<TaskMeta>管理。因为一般情况下同时运行的bthread不会超过2^32,所以这里认为申请资源获取到的ResourceId都是32位的,然后和bthread version一起组合成bthread_t
- TaskMeta存储一个任务的元信息,存储了任务的栈空间、bthread id等等。
一些其他辅助类
ParkingLot
- 参与bthread调度,本质上就是基于futex的wait/signal操作(Futex是一种用户态和内核态混合的同步机制)
- parkinglot成员变量只有一个_pending_signal,atomic类型,存储的是调用futex_wait/futex_wake需传入的共享地址uaddr。
- 如果worker当前无可执行任务(包括steal操作也无法获取任务),则会通过futex wait操作陷入等待。当调用bthread_start_background等新建协程函数时,又会通过futex signal操作唤起worker,执行任务。
- 成员函数,比较简单不详细讲
// Wake up at most `num_task' workers.
// Returns #workers woken up.
int signal(int num_task) {
// TG中每次唤醒后会记录最新_pending_signal的值,即state。如果在下次wait时_pending_signal的值仍然跟state一致,则表示中间没有调用过signal,没有新任务过来,则可以陷入等待。
// 这里修改_pending_signal表示有新任务过来,不能陷入等待。
_pending_signal.fetch_add((num_task << 1), butil::memory_order_release);
// 最多唤醒num_task个等待在futex word上的线程。
return futex_wake_private(&_pending_signal, num_task);
}
// Wait for tasks.
// If the `expected_state' does not match, wait() may finish directly.
void wait(const State& expected_state) {
// FUTEX_WAIT 如果futex word中仍然保存着参数val给定的值,那么当前线程则进入睡眠,等待FUTEX_WAKE的操作唤醒它。
futex_wait_private(&_pending_signal, expected_state.val, NULL);
}
WorkStealingQueue
- 跟Work Stealing算法相关,后续会详细讲解该算法。这里只需知道,该队列实际上就是一个双端队列,保存的是bthread id。push和pop都在bottom侧,是队列所属线程使用的,steal在top侧,是其他线程使用的,这里就会有多线程竞争问题,pop和steal在两端,也是为了一定程度减少竞争情况。workstealing队列的实现主要在于解决并发问题。这里使用atomic配合memory order保证获取到的数据一致性。
- 成员变量
butil::atomic<size_t> _bottom; // 指向队列末尾
size_t _capacity; // 队列最大容量
T* _buffer; // 数据首元素地址
butil::atomic<size_t> BAIDU_CACHELINE_ALIGNMENT _top; // 指向队列头部
- 成员函数
// push和pop都是在单线程中执行的,push、steal可能同时执行,push修改_bottom,steal修改_top,二者没有竞争,所以只用保证填充_buffer操作在修改_bottom之前即可
// 可能出现的并发场景:1. 获取b和t后,此时队列已满,同时steal减少了库存,导致push失败,但外层有循环重复尝试,问题不大。2. 获取b和t后,steal减少了库存,修改了t,这里只会增加b,不影响t的存取,并且存取top位置数据时,也会严格校验
bool push(const T& x) {
const size_t b = _bottom.load(butil::memory_order_relaxed);
const size_t t = _top.load(butil::memory_order_acquire);
if (b >= t + _capacity) { // Full queue.
return false;
}
_buffer[b & (_capacity - 1)] = x; // buffer是一个存储数据的数组,容量是_capacity。在bottom侧加入数据
_bottom.store(b + 1, butil::memory_order_release);
return true;
}
// push和pop是线性的,pop和steal可能同时执行。
// 注意,因为修改bottom的操作都是线性的,所以用memory_order_relaxed没问题。只是跟top有关的需要更强的memory order。
bool pop(T* val) {
const size_t b = _bottom.load(butil::memory_order_relaxed);
size_t t = _top.load(butil::memory_order_relaxed);
if (t >= b) { // 为空时会出现,粗略判断一次
// fast check since we call pop() in each sched.
// Stale _top which is smaller should not enter this branch.
return false;
}
const size_t newb = b - 1;
// 先尝试修改_bottom,因为bottom的使用是线性的,即使修改后有问题也没关系,及时修正即可
_bottom.store(newb, butil::memory_order_relaxed);
// 这里保证fence前面和后面的顺序不能更改。_bottom此时全局可见了,steal中如果在此时进行load操作,可以保证load到此时_bottom写入的(或之后写入的)值。(否则因为寄存器等即使是之后发生的也有可能load到之前的值)
butil::atomic_thread_fence(butil::memory_order_seq_cst);
t = _top.load(butil::memory_order_relaxed);
if (t > newb) { // 已经被steal偷走啦,加回_bottom的值,这个过程中_bottom值是有问题的,但不会有其它地方访问,所以无影响
_bottom.store(b, butil::memory_order_relaxed);
return false;
}
*val = _buffer[newb & (_capacity - 1)];
if (t != newb) { // 取走之后队列仍然有数据,那么没有竞争问题,可以直接返回。但是当这是最后一个数据时,那么steal和pop则有竞争。
return true;
}
// Single last element, compete with steal()
// 此时一个从top拿,一个从_bottom拿可能导致同个数据被两个地方获取,所以只能从一边竞争,这里都从top拿,哪边先执行_top=t+1成功,则取这最后一个数据成功
const bool popped = _top.compare_exchange_strong(
t, t + 1, butil::memory_order_seq_cst, butil::memory_order_relaxed);
_bottom.store(b, butil::memory_order_relaxed); // 不管抢没抢成功,_bottom都要加回去,因为此时不是从_bottom拿数据了
return popped;
}
// 跟push和pop都有可能并发,steal跟steal也有可能并发。
bool steal(T* val) {
size_t t = _top.load(butil::memory_order_acquire);
size_t b = _bottom.load(butil::memory_order_acquire);
if (t >= b) { // 粗略判断一次是不是空,在判断之前可能有变更,并不一定是空的,失败了会继续steal其他TG,问题不大
// Permit false negative for performance considerations.
return false;
}
do {
butil::atomic_thread_fence(butil::memory_order_seq_cst);
b = _bottom.load(butil::memory_order_acquire);
if (t >= b) {// 如果这个时候有pop,b的load操作发生在_bottom的store操作后,那么会直接return;如果b的load操作发生在store操作之前,那么将一起竞争_top=_top+1
return false;
}
*val = _buffer[t & (_capacity - 1)];
// 如果不能令_top=_top+1,证明本次抢_top位置数据失败,重新获取最新的_top再次抢占
} while (!_top.compare_exchange_strong(t, t + 1,
butil::memory_order_seq_cst,
butil::memory_order_relaxed));
return true;
}
RemoteTaskQueue
简单来说是个栈,提供push和pop操作。TG中的_remote_rq(非worker中创建的bthread队列)都是用RemoteTaskQueue存储的。与WorkStealingQueue不同的是,RemoteTaskQueue中push和pop是使用锁来解决并发问题的,相比WorkStealingQueue性能更差些。猜测是因为_remote_rq使用频率没那么高,并发性不大。