前面工作组TaskGroup创建的准备工作完成,现在有新任务到来还是调用bthread_start_background,因为worker已经存在,所以调用start_background()。
if (g) {
// start from worker
return g->start_background<false>(tid, attr, fn, arg);
}
template <bool REMOTE>
int TaskGroup::start_background(bthread_t* __restrict th,
const bthread_attr_t* __restrict attr,
void * (*fn)(void*),
void* __restrict arg) {
// 检查函数指针是否为空,__builtin_expect是编译器的内建命令分支预测,返回!fn表达式的值,并给了一个预期值0,如果表达式值符合预期会提升性能,反之降低性能
if (__builtin_expect(!fn, 0)) {
return EINVAL;
}
// 获取当前时间的纳秒数
const int64_t start_ns = butil::cpuwide_time_ns();
// 使用传入的属性或默认属性
const bthread_attr_t using_attr = (attr ? *attr : BTHREAD_ATTR_NORMAL);
// 获取TaskMeta资源的槽位
butil::ResourceId<TaskMeta> slot;
// 获取TaskMeta资源
TaskMeta* m = butil::get_resource(&slot);
// 检查是否成功获取到TaskMeta资源
if (__builtin_expect(!m, 0)) {
return ENOMEM;
}
// 检查当前没有等待者
CHECK(m->current_waiter.load(butil::memory_order_relaxed) == NULL);
// 初始化TaskMeta对象的成员变量
m->stop = false;
m->interrupted = false;
m->about_to_quit = false;
m->fn = fn;
m->arg = arg;
CHECK(m->stack == NULL);
m->attr = using_attr;
m->local_storage = LOCAL_STORAGE_INIT;
m->cpuwide_start_ns = start_ns;
m->stat = EMPTY_STAT;
m->tid = make_tid(*m->version_butex, slot);
*th = m->tid;
// 如果启用了日志记录,打印日志
if (using_attr.flags & BTHREAD_LOG_START_AND_FINISH) {
LOG(INFO) << "Started bthread " << m->tid;
}
// 线程数*2
_control->_nbthreads << 1;
// 根据是否远程执行,调用不同的函数准备线程运行
if (REMOTE) {
ready_to_run_remote(m->tid, (using_attr.flags & BTHREAD_NOSIGNAL));
} else {
ready_to_run(m->tid, (using_attr.flags & BTHREAD_NOSIGNAL));
}
// 返回成功
return 0;
}
这里会创建TaskMeta并初始化赋值,接着调用ready_to_run。
void TaskGroup::ready_to_run(bthread_t tid, bool nosignal) {
push_rq(tid);
if (nosignal) {
++_num_nosignal;
} else {
const int additional_signal = _num_nosignal;
_num_nosignal = 0;
_nsignaled += 1 + additional_signal;
_control->signal_task(1 + additional_signal);
}
}
void TaskGroup::ready_to_run_remote(bthread_t tid, bool nosignal) {
_remote_rq._mutex.lock();
while (!_remote_rq.push_locked(tid)) {
flush_nosignal_tasks_remote_locked(_remote_rq._mutex);
LOG_EVERY_SECOND(ERROR) << "_remote_rq is full, capacity="
<< _remote_rq.capacity();
::usleep(1000);
_remote_rq._mutex.lock();
}
if (nosignal) {
++_remote_num_nosignal;
_remote_rq._mutex.unlock();
} else {
const int additional_signal = _remote_num_nosignal;// 获取当前的无信号任务计数
_remote_num_nosignal = 0;// 重置无信号任务计数
_remote_nsignaled += 1 + additional_signal;// 增加已发送信号的任务计数
_remote_rq._mutex.unlock();// 解锁互斥锁
_control->signal_task(1 + additional_signal);// 发送信号
}
}
ready_to_run()的操作相对简洁。ready_to_run_remote()先是给当前TaskGroup的remote_rq加互斥锁,然后remote_rq入队。
这里是一个while循环,入队失败就是执行flush_nosignal_tasks_remote_locked()刷新任务队列,记录一次错误日志表示队列已满。休眠1ms,再重复尝试入队。
入队失败的原因只有remote_rq队列满了。flush_nosignal_tasks_remote_locked()中会解锁,然后发出信号让remote_rq尽快消费。
在while结束后就表示新任务已经入队,else的操作就是还没通知的任务数+1执行signal_task()通知其他worker消费。在flush_nosignal_tasks_remote_locked()中也是调用TaskControl::signal_task()
这里就来到控制器TaskControl了。
因为TaskGroup的主体部分一直在循环等待任务,没有任务的时候就会工作窃取(work stealing)获取其他队列的任务。但是任务不一定一直有,而TaskGroup如果时刻不停地去check是否有任务。就太消耗资源。它在没有任务的时候也会“休眠”,当有任务的时候就会唤醒然后去消费任务。TaskControl (ParkingLot)就是实现这一功能的重要部分。
先看ParkingLot和TaskControl的关系:
static const int PARKING_LOT_NUM = 4;
ParkingLot _pl[PARKING_LOT_NUM];
TaskControl成员中有定义了有四个ParkingLot的一个数组。
然后是TaskGroup中的私有成员:
ParkingLot* _pl;
#ifndef BTHREAD_DONT_SAVE_PARKING_STATE
ParkingLot::State _last_pl_state;
#endif
有ParkingLot指针和ParkingLot::State对象。
在TaskGroup构造函数中会给_pl赋值,其实是从TaskControl的pl数组中取一个ParkingLot与TaskGroup绑定(计算方法是以pthread_id散列后除以ParkingLot大小得余数)
_pl = &c->_pl[butil::fmix64(pthread_numeric_id()) % TaskControl::PARKING_LOT_NUM];
一个TaskControl有4个ParkingLot,再对应多个TaskGroup,就可能有多个TaskGroup关联在一个ParkingLot上。可以看作TaskControl管理的TaskGroup根据ParkingLot分成四组(不是一个PL,大概率也是为了减少race condition(竞争状态)减少性能开销)
ParkingLot在brpc中管理TaskGroup或相关任务处理方面的优点:
- 减少竞争和提高性能:
-
ParkingLot通过减少线程间的竞争状态(race condition)来提高性能。在brpc中,存在多个TaskGroup(每个Worker线程对应一个),它们之间可能会争夺CPU资源或任务队列。通过引入多个ParkingLot(每个TaskGroup或一组TaskGroup共享一个),可以减少单个同步点上的竞争,从而提高系统的整体吞吐量。- 当任务被提交到某个
TaskGroup的队列(如rq或remote_rq)时,ParkingLot可以确保有任务可执行的Worker线程被及时唤醒,减少了线程的空闲时间。
- 灵活的唤醒机制:
-
ParkingLot基于futex(快速用户空间互斥锁)实现,提供了灵活的唤醒机制。当任务被添加到队列中时,可以通过ParkingLot的信号(signal)功能来唤醒一个或多个等待的Worker线程,从而确保任务能够尽快被执行。- 这种机制比传统的轮询(polling)或忙等待(busy waiting)更高效,因为它减少了CPU的无用功,只有当有新任务到来时才唤醒线程。
- 支持work stealing机制:
-
- 在
brpc中,如果某个TaskGroup的本地队列(rq)为空,Worker线程会尝试从其他TaskGroup的远程队列(remote_rq)中“窃取”任务来执行。这种work stealing机制有助于平衡不同TaskGroup之间的负载,防止某些Worker线程过载而其他线程空闲。 ParkingLot在这个过程中起到了同步和协调的作用,确保在尝试窃取任务时不会发生数据竞争或不一致的问题。
- 在
- 简化同步逻辑:
-
- 通过使用
ParkingLot,brpc可以简化任务提交和执行的同步逻辑。开发者不需要编写复杂的同步代码来处理线程间的等待和唤醒问题,而是可以依赖于ParkingLot提供的机制来自动完成这些任务。 - 这不仅降低了开发难度和出错率,还提高了代码的可读性和可维护性。
- 通过使用
综上所述,ParkingLot在brpc的线程模型中通过减少竞争、提供灵活的唤醒机制、支持work stealing机制以及简化同步逻辑等方面,间接地优化了TaskGroup的管理和任务执行效率。
回头看signal_task执行过程
void TaskControl::signal_task(int num_task) {
if (num_task <= 0) {
return;
}
// TODO(gejun): 当前算法不能保证创建的线程数量能满足调用者的请求。但另一方面,根据当前实现,也存在许多无用的信号。
// 限制并发数是一个在性能和调度及时性之间的良好平衡。
if (num_task > 2) {
num_task = 2;
}
// 根据线程ID计算起始索引
int start_index = butil::fmix64(pthread_numeric_id()) % PARKING_LOT_NUM;
// 发送信号给停车位的线程,并更新剩余任务数
num_task -= _pl[start_index].signal(1);
if (num_task > 0) {
for (int i = 1; i < PARKING_LOT_NUM && num_task > 0; ++i) {
if (++start_index >= PARKING_LOT_NUM) {
start_index = 0;
}
// 继续发送信号给下一个停车位的线程,并更新剩余任务数
num_task -= _pl[start_index].signal(1);
}
}
if (num_task > 0 &&
FLAGS_bthread_min_concurrency > 0 && // 测试最小并发数对性能的影响
// TODO: 减少这个锁的使用
// 测试当前并发数是否小于最大并发数
_concurrency.load(butil::memory_order_relaxed) < FLAGS_bthread_concurrency) {
// TODO: Reduce this lock
BAIDU_SCOPED_LOCK(g_task_control_mutex);
if (_concurrency.load(butil::memory_order_acquire) < FLAGS_bthread_concurrency) {
// 如果小于最大并发数,则添加工作线程
add_workers(1);
}
}
}
num_task小于1就返回,大于2就重置为2。作者也说明了是为了性能和调度的平衡。num_task大于2时表示有多个任务还没处理,过多的通知反而会影响性能。
start_index计算方法和TaskGroup关联ParkingLot一样,找到ParkingLot(应该是当前线程归属的)调用signal(1)通知。
找到ParkingLot的定义,及signal()函数
class BAIDU_CACHELINE_ALIGNMENT ParkingLot { __declspec(align(64)) //64字节对齐
public:
class State {
public:
State(): val(0) {}
bool stopped() const { return val & 1; }
private:
friend class ParkingLot;
State(int val) : val(val) {}
int val;
};
ParkingLot() : _pending_signal(0) {}
// Wake up at most `num_task' workers.
// Returns #workers woken up.
//* `butil::memory_order_release`:这是原子操作的内存顺序模型。`memory_order_release` 表示在修改操作前的所有读写操作都不能被重排序到修改操作之后,且该修改操作对其他线程可见。这对于确保多线程环境下的可见性和顺序性非常重要。
int signal(int num_task) { //入参是最多换新worker数,返回值是唤醒worker数
_pending_signal.fetch_add((num_task << 1), butil::memory_order_release);// T fetch_add(T v, memory_order o) { return ref().fetch_add(v, o); }
return futex_wake_private(&_pending_signal, num_task);
}
//线程安全的获取wait状态
State get_state() {
return _pending_signal.load(butil::memory_order_acquire);
}
//wait for tasks 如果和预期的状态不符会提前结束
void wait(const State& expected_state) {
futex_wait_private(&_pending_signal, expected_state.val, NULL);
}
// Wakeup suspended wait() and make them unwaitable ever.
void stop() {
_pending_signal.fetch_or(1);
futex_wake_private(&_pending_signal, 10000);
}
private:
// higher 31 bits for signalling, LSB for stopping.
butil::atomic<int> _pending_signal;
};
有一个内部类State,ParkingLot是友元,有个原子类型私有成员_pending_signal初始值为0。
在signal中fetch_add是一个封装的原子操作辅助函数,保证线程安全的操作一个数值。这里是左移1位(即*2),然后会调用futex_wake_private()
inline int futex_wake_private(void* addr1, int nwake) {
return syscall(SYS_futex, addr1, (FUTEX_WAKE | FUTEX_PRIVATE_FLAG),
nwake, NULL, NULL, 0);
}
这里是封装了一个SYS_futex的系统调用。futex是一种用户态和内核态混合的同步机制,在用户态检查如果没有竞争就不需要进入内核态
int futex(int *uaddr, int op, int val, const struct timespec *timeout, int *uaddr2, int val3);
参数解析:
-
uaddr指针指向一个整型,存储一个整数。
-
op表示要执行的操作类型,比如唤醒(FUTEX_WAKE)、等待(FUTEX_WAIT)
-
val表示一个值,注意:对于不同的op类型,val语义不同。
-
- 对于等待操作:如果uaddr存储的整型与val相同则继续休眠等待。等待时间就是timeout参数。
- 对于唤醒操作:val表示,最多唤醒val 个阻塞等待uaddr上的“消费者”(之前对同一个uaddr调用过FUTEX_WAIT,姑且称之为消费者,其实在brpc语境中,就是阻塞的worker)。
-
timeout表示超时时间,仅对op类型为等待时有用。就是休眠等待的最长时间。
-
uaddr2和val3可以忽略。
返回值解析:
- 对于等待操作:成功返回0,失败返回-1
- 对于唤醒操作:成功返回唤醒的之前阻塞在futex上的“消费者”个数。失败返回-1。
所以futex_wake_private()里面的syscall()等价于:
futex(&_pending_signal, (FUTEX_WAKE|FUTEX_PRIVATE_FLAG), num_task, NULL, NULL, 0);
FUTEX_WAKE是唤醒,FUTEX_PRIVATE_FLAG是一个标记表示不和其它进程共享。
返回值就是唤醒worker的个数,然后透传到signal()。
回到signal_task()
num_task -= _pl[start_index].signal(1);
num_task -=后就是未唤醒的worker个数,然后继续执行
if (num_task > 0) {
for (int i = 1; i < PARKING_LOT_NUM && num_task > 0; ++i) {
if (++start_index >= PARKING_LOT_NUM) {
start_index = 0;
}
// 继续发送信号给下一个ParkingLot,并更新剩余任务数
num_task -= _pl[start_index].signal(1);
}
}
num_task>0,则遍历下一个ParkingLot发信号唤醒,因为这里的start_index不是从0开始的,pl数组下标越界后需要重置0。
if (num_task > 0 &&
FLAGS_bthread_min_concurrency > 0 && // 测试最小并发数对性能的影响
// TODO: 减少这个锁的使用
// 测试当前并发数是否小于最大并发数
_concurrency.load(butil::memory_order_relaxed) < FLAGS_bthread_concurrency) {
// TODO: Reduce this lock
BAIDU_SCOPED_LOCK(g_task_control_mutex);
if (_concurrency.load(butil::memory_order_acquire) < FLAGS_bthread_concurrency) {
// 如果小于最大并发数,则添加工作线程
add_workers(1);
}
}
如果任务还有剩余(表示消费者不够用),并且全局TC的并发度(_concurrency)小于gflag中配置的bthread_min_concurrency,那么就调用add_workers()去增加worker的数量。
FLAGS_bthread_concurrency是worker(或者说是TG、pthread)个数的硬门槛。
回顾TaskGroup一直循环的wait_task
bool TaskGroup::wait_task(bthread_t* tid) {
do {
#ifndef BTHREAD_DONT_SAVE_PARKING_STATE
// 如果最后一次保存的parking lot状态为停止状态
if (_last_pl_state.stopped()) {
// 返回false,表示等待任务失败
return false;
}
// 在_last_pl_state的parking lot上等待
_pl->wait(_last_pl_state);
// 尝试从其他线程窃取任务
if (steal_task(tid)) {
// 如果窃取成功,返回true
return true;
}
#else /*
首先检查`_last_pl_state.stopped()`,如果返回`true`,则直接返回`false`,表示任务已经停止,无需继续等待。
调用`_pl->wait(_last_pl_state)`来等待某个条件。这里`_pl`可能是一个指向某种同步或线程池对象的指针,`wait`方法可能是用来让当前线程等待,直到有任务可用或某种条件满足。
调用`steal_task(tid)`尝试窃取一个任务。如果成功,返回`true`。
// 获取parking lot的当前状态
const ParkingLot::State st = _pl->get_state();
// 如果parking lot状态为停止状态
if (st.stopped()) {
// 返回false,表示等待任务失败
return false;
}
// 尝试从其他线程窃取任务
if (steal_task(tid)) {
// 如果窃取成功,返回true
return true;
}
// 在当前状态的parking lot上等待
_pl->wait(st);
*/
#endif
} while (true);
}
last_pl_state是TaskGroup的ParkingLot::State对象成员,TaskGroup初始化,_last_pl_state是无参构造val默认0。
bool stopped() const { return val & 1; }
stopped就是判断val是否为奇数。signal调用中_pending_signal.fetch_add((num_task << 1), butil::memory_order_release)会将_pending_signal赋值num_task*2,所以这步一般不会触发return false。(在ParkingLot中有个stop成员函数,调用后就会使得_pending_signal为奇数)
然后执行到_pl->wait(_last_pl_state),在ParkingLot中定义
//_pl->wait(_last_pl_state)
void wait(const State& expected_state) {
futex_wait_private(&_pending_signal, expected_state.val, NULL);
}
futex(&_pending_signal, (FUTEX_WAIT|FUTEX_PRIVATE_FLAG), expected_state.val, NULL, NULL, 0);
和前面futex的调用类似,只是这里是阻塞在&_pending_signal这里,因为expected_state实际传入的是_last_pl_state,所以该wait操作其预期值expected_state=_last_pl_state。_last_pl_state的val值和_pending_signal的值相同则阻塞(说明还没有任务)。
_pending_signal:这是一个指向用户空间中某个变量的指针,该变量用作futex的等待点。线程将在这个变量的值上等待或唤醒。FUTEX_WAIT:这个操作码告诉futex系统调用,调用线程应该等待直到某个条件满足。FUTEX_PRIVATE_FLAG:这个标志指定了操作应该在私有模式下执行,即只影响当前进程内的线程。expected_state.val:这是一个整数,表示调用线程期望在_pending_signal变量中看到的值。如果_pending_signal的当前值等于expected_state.val,则FUTEX_WAIT调用会立即返回,而不是阻塞线程。- 阻塞条件:根据上述参数,阻塞条件是
_pending_signal的当前值不等于expected_state.val。如果条件不满足(即值不相等),调用线程将阻塞,并进入与_pending_signal相关联的等待队列。线程将保持阻塞状态,直到另一个线程通过FUTEX_WAKE(或FUTEX_WAKE_OP)操作唤醒它,或者直到发生了超时(如果调用了带超时参数的futex变体)。 - 唤醒:当另一个线程通过
FUTEX_WAKE操作(或其他能够唤醒等待线程的操作)更改了_pending_signal的值,并且至少有一个线程因为该值而阻塞时,阻塞的线程之一(或多个,取决于FUTEX_WAKE的参数)将被唤醒。然而,被唤醒的线程在继续执行之前,通常会重新检查_pending_signal的值,以确保它们之前等待的条件现在已满足。
然后尝试从其他工作组窃取任务steal_task
bool steal_task(bthread_t* tid) {
if (_remote_rq.pop(tid)) {
return true;
}
#ifndef BTHREAD_DONT_SAVE_PARKING_STATE
_last_pl_state = _pl->get_state();
#endif
return _control->steal_task(tid, &_steal_seed, _steal_offset);
}
这个BTHREAD_DONT_SAVE_PARKING_STATE宏定义和wait_task()中是配套的。
当前TaskGroup没有任务时,rq队列pop失败返回false,然后执行_pl->get_state()获取_pending_signal最新值
/*T load(memory_order o) { return ref().load(o); }
atomic<T>& ref() {
// Suppress strict-alias warnings.
atomic<T>* p = reinterpret_cast<atomic<T>*>(&val);
return *p;
}*/
State get_state() {
return _pending_signal.load(butil::memory_order_acquire);
}
保存/刷新ParkingLot上一次的状态 _last_pl_state =_pending_signal。
_pending_signal存储的值不表示任务个数,它的变化只是表示一种状态 可以看作同步的工具
前面说了_pending_signal一般都是偶数,只有调用ParkingLot中stop成员函数才会让这个值变奇数,调用stop函数的地方就只在TaskControl的stop_and_join(),stop_and_join()只在bthread_stop_world()中调用。
-
- bthread_stop_world()
-
-
- TaskControl::stop_and_join()
-
-
-
-
- ParkingLot::stop()
-
-
正常我们都不会调用bthread_stop_world(),所以在_last_pl_state.stopped()在服务正常运转的情况下都不会为false。
void stop() {
_pending_signal.fetch_or(1);
futex_wake_private(&_pending_signal, 10000);
}
先会将_pending_signal设置成奇数。然后futex(&_pending_signal, (FUTEX_WAKE|FUTEX_PRIVATE_FLAG), 10000, NULL, NULL, 0)按参数可以理解为唤醒所有bthread。
949

被折叠的 条评论
为什么被折叠?



