bthread任务创建

前面工作组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(竞争状态)减少性能开销)

ParkingLotbrpc中管理TaskGroup或相关任务处理方面的优点:

  1. 减少竞争和提高性能:
    • ParkingLot通过减少线程间的竞争状态(race condition)来提高性能。在brpc中,存在多个TaskGroup(每个Worker线程对应一个),它们之间可能会争夺CPU资源或任务队列。通过引入多个ParkingLot(每个TaskGroup或一组TaskGroup共享一个),可以减少单个同步点上的竞争,从而提高系统的整体吞吐量。
    • 当任务被提交到某个TaskGroup的队列(如rqremote_rq)时,ParkingLot可以确保有任务可执行的Worker线程被及时唤醒,减少了线程的空闲时间。
  1. 灵活的唤醒机制:
    • ParkingLot基于futex(快速用户空间互斥锁)实现,提供了灵活的唤醒机制。当任务被添加到队列中时,可以通过ParkingLot的信号(signal)功能来唤醒一个或多个等待的Worker线程,从而确保任务能够尽快被执行。
    • 这种机制比传统的轮询(polling)或忙等待(busy waiting)更高效,因为它减少了CPU的无用功,只有当有新任务到来时才唤醒线程。
  1. 支持work stealing机制:
    • brpc中,如果某个TaskGroup的本地队列(rq)为空,Worker线程会尝试从其他TaskGroup的远程队列(remote_rq)中“窃取”任务来执行。这种work stealing机制有助于平衡不同TaskGroup之间的负载,防止某些Worker线程过载而其他线程空闲。
    • ParkingLot在这个过程中起到了同步和协调的作用,确保在尝试窃取任务时不会发生数据竞争或不一致的问题。
  1. 简化同步逻辑:
    • 通过使用ParkingLotbrpc可以简化任务提交和执行的同步逻辑。开发者不需要编写复杂的同步代码来处理线程间的等待和唤醒问题,而是可以依赖于ParkingLot提供的机制来自动完成这些任务。
    • 这不仅降低了开发难度和出错率,还提高了代码的可读性和可维护性。

综上所述,ParkingLotbrpc的线程模型中通过减少竞争、提供灵活的唤醒机制、支持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);

参数解析:

  1. uaddr指针指向一个整型,存储一个整数。

  2. op表示要执行的操作类型,比如唤醒(FUTEX_WAKE)、等待(FUTEX_WAIT)

  3. val表示一个值,注意:对于不同的op类型,val语义不同。

    1. 对于等待操作:如果uaddr存储的整型与val相同则继续休眠等待。等待时间就是timeout参数。
    2. 对于唤醒操作:val表示,最多唤醒val 个阻塞等待uaddr上的“消费者”(之前对同一个uaddr调用过FUTEX_WAIT,姑且称之为消费者,其实在brpc语境中,就是阻塞的worker)。
  4. timeout表示超时时间,仅对op类型为等待时有用。就是休眠等待的最长时间。

  5. uaddr2和val3可以忽略。

返回值解析:

  1. 对于等待操作:成功返回0,失败返回-1
  2. 对于唤醒操作:成功返回唤醒的之前阻塞在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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值