brpc学习笔记(三)- bthread基本构成

bthread

  1. bthread是brpc使用的M:N线程库,广义的协程是M:1线程库。我们可以认为bthread是brpc定义的线程,M个bthread映射到N个实际线程pthread上,一般M远大于N。
  2. 在brpc中,bthread不是指某个具体的类,而是由多个模块、调度算法等构成的模型。bthread模型中有几个模块,TC(TaskControl),TG(TaskGroup),TM(TaskMeta)。TaskControl全局唯一,内有多个TaskGroup,TC负责TG调度等。这里可以认为,TG是可以处理bthread任务的worker,对应具体的线程,也就是TG=workor=pthread,TG的个数实际上就是前面提到的N。TM主要维护单个协程的具体信息,例如协程号、栈空间等。
  3. 实现bthread M:N模型的关键是Work Stealing算法
  4. 其他工具类:PL(ParkingLot),WorkStealingQueue

BThread

任务运行的单位,每有一个任务需要执行,则新起一个bthread执行任务,一般来说,执行完之后这个bthread的生命周期即终止了。bthread执行的任务具体来说就是一个函数。
注意bthread不是一个单独的数据结构,一般用bthread id来标识一个bthread,bthread内部数据都存放在bthread id对应的TaskMeta中。与bthread相关的主要是创建、销毁、中断、join等函数,这些函数也不是用成员函数定义,而是通过声明一些全局函数
在brpc服务中,线程可以简单分为两种:工作线程和非工作线程。工作线程用于执行bthread的任务,在brpc中称为worker,每个worker都会有一个对应的TaskGroup。剩余非工作线程可能是主线程,或者用户自己创建的线程。

主要函数

  1. start_from_non_worker
    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);
    }
    
    这个函数主要用于在非工作线程上,生成协程完成任务。主要逻辑就是从TaskControl(管理所有worker)中选择一个worker来执行该bthread任务。注意,不管是使用bthread_start_urgent(立即执行任务),还是
  2. bthread_start_urgent
    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); // 非工作线程
    }
    
    如果是工作线程,则使用当前worker立即执行,当前worker正在执行的任务放到队列中等待执行,否则通过start_from_non_worker选择一个worker来执行。
  3. bthread_start_background
    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_background和bthread_start_urgent不同的地方是,bthread_start_background不会直接占用当前worker,而是生成一个协程放在队列中等待worker执行。注意,因为bthread有Work Stealing算法,所以加到当前worker对应TG的队列里,不代表一定会被当前worker执行。bthread_start_background相当于pthread_create,生成一个协程(线程)来执行任务,不影响现在正在执行的协程(线程任务)。
  4. bthread_start_nosteal
    			// ****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);
    	}
    
    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很有可能被阻塞。

TaskControl

TaskControl是进程所有TaskGroup的管理者,负责生成、调度、管理TaskGroup。

主要成员变量

  1. TaskGroup** _groups; TG列表
  2. butil::atomic<size_t> _ngroup; _groups的长度
  3. butil::atomic<int> _concurrency;
  4. std::vector<pthread_t> _workers; 起的所有worker的pthread id
  5. ParkingLot _pl[PARKING_LOT_NUM]; PARKING_LOT_NUM=4,用于调度,后续在ParkingLot会详细描述

主要成员函数

  1. 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。

  2. 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;
    }
    
  3. 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;
    	}
    

    生成工作线程后的初始化工作。

  4. 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。

  1. 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使用频率没那么高,并发性不大。

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值