skynet消息調度機制

上一節討論了c服務的創建,現在來討論消息的派發和消費,本節會討論skynet的消息派發和消費,以及它如何實現線程安全,要徹底弄清楚這些內容,需要先理解以下四種鎖。

  • 互斥鎖(mutex lock : mutual exclusion lock)
  1. 概念:互斥鎖,一條線程加鎖鎖住臨界區,另一條線程嘗試訪問改臨界區的時候,會發生阻塞,並進入休眠狀態。臨界區是鎖lock和unlock之間的代碼片段,一般是多條線程能夠共同訪問的部分。
  2. 具體說明:假設一臺機器上的cpu有兩個核心core0和core1,現在有線程A、B、C,此時core0運行線程A,core1運行線程B,此時線程B使用Mutex鎖,鎖住一個臨界區,當線程A試圖訪問該臨界區時,因爲線程B已經將其鎖住,因此線程A被掛起,進入休眠狀態,此時core0進行上下文切換,將線程A放入休眠隊列中,然後core0運行線程C,當線程B完成臨界區的流程並執行解鎖之後,線程A又會被喚醒,core0重新運行線程A

  • 自旋鎖(spinlock)
  1. 概念:自旋鎖,一條線程加鎖鎖住臨界區,另一條線程嘗試訪問該臨界區的時候,會發生阻塞,但是不會進入休眠狀態,並且不斷輪詢該鎖,直至原來鎖住臨界區的線程解鎖。
  2. 具體說明:假設一臺機器上有兩個核心core0和core1,現在有線程A、B、C,此時core0運行線程A,core1運行線程B,此時線程B調用spin lock鎖住臨界區,當線程A嘗試訪問該臨界區時,因爲B已經加鎖,此時線程A會阻塞,並且不斷輪詢該鎖,不會交出core0的使用權,當線程B釋放鎖時,A開始執行臨界區邏輯


  • 讀寫鎖(readers–writer lock)
  1. 概述:讀寫鎖,一共三種狀態
    • 讀狀態時加鎖,此時爲共享鎖,當一個線程加了讀鎖時,其他線程如果也嘗試以讀模式進入臨界區,那麼不會發生阻塞,直接訪問臨界區
    • 寫狀態時加鎖,此時爲獨佔鎖,當某個線程加了寫鎖,那麼其他線程嘗試訪問該臨界區(不論是讀還是寫),都會阻塞等待
    • 不加鎖
  2. 注意:
    • 某線程加讀取鎖時,允許其他線程以讀模式進入,此時如果有一個線程嘗試以寫模式訪問臨界區時,該線程會被阻塞,而其後嘗試以讀方式訪問該臨界區的線程也會被阻塞
    • 讀寫鎖適合在讀遠大於寫的情形中使用

  • 條件變量(condition variables)
概述:假設A,B,C三條線程,其中B,C線程加了cond_wait鎖並投入睡眠,而A線程則在某個條件觸發時,會通過signal通知B,C線程,從 而喚醒B和C線程。


  • 消費消息流程

  1. 概述:
    skynet在啓動時,會創建若干條worker線程(由配置指定),這些worker線程被創建以後,會不斷得從global_mq裏pop出一個次級消息隊列來,每個worker線程,每次只pop一個次級消息隊列,然後再從次級消息隊列中,pop一到若干條消息出來(受權重值影響),最後消息將作爲參數傳給對應服務的callback函數(每個服務只有一個專屬的次級消息隊列),當callback執行完時,worker線程會將次級消息隊列push回global_mq裏,這樣就完成了消息的消費。
    在這個過程中,因爲每個worker線程會從global_mq裏pop一個次級消息隊列出來,此時其他worker線程就不能從global_mq裏pop出同一個次級消息隊列,也就是說,一個服務不能同時在多個worker線程內調用callback函數,從而保證了線程安全。
  2. worker線程的創建與運作
    要理解skynet的消息調度,首先要理解worker線程的創建流程,基本運作以及線程安全。worker線程的數量由配置的“thread”字段指定,skynet節點啓動時,會創建配置指定數量的worker線程,我們可以再skynet_start.c的start函數中找到這個創建流程:
    // skynet_start.c
    static void
    start(int thread) {
    	pthread_t pid[thread+3];
    
    	struct monitor *m = skynet_malloc(sizeof(*m));
    	memset(m, 0, sizeof(*m));
    	m->count = thread;
    	m->sleep = 0;
    
    	m->m = skynet_malloc(thread * sizeof(struct skynet_monitor *));
    	int i;
    	for (i=0;i<thread;i++) {
    		m->m[i] = skynet_monitor_new();
    	}
    	if (pthread_mutex_init(&m->mutex, NULL)) {
    		fprintf(stderr, "Init mutex error");
    		exit(1);
    	}
    	if (pthread_cond_init(&m->cond, NULL)) {
    		fprintf(stderr, "Init cond error");
    		exit(1);
    	}
    
    	create_thread(&pid[0], thread_monitor, m);
    	create_thread(&pid[1], thread_timer, m);
    	create_thread(&pid[2], thread_socket, m);
    
    	static int weight[] = { 
    		-1, -1, -1, -1, 0, 0, 0, 0,
    		1, 1, 1, 1, 1, 1, 1, 1, 
    		2, 2, 2, 2, 2, 2, 2, 2, 
    		3, 3, 3, 3, 3, 3, 3, 3, };
    	struct worker_parm wp[thread];
    	for (i=0;i<thread;i++) {
    		wp[i].m = m;
    		wp[i].id = i;
    		if (i < sizeof(weight)/sizeof(weight[0])) {
    			wp[i].weight= weight[i];
    		} else {
    			wp[i].weight = 0;
    		}
    		create_thread(&pid[i+3], thread_worker, &wp[i]);
    	}
    
    	for (i=0;i<thread+3;i++) {
    		pthread_join(pid[i], NULL); 
    	}
    
    	free_monitor(m);
    }
    
    skynet所有的線程都在這裏被創建,在創建完monitor線程,timer線程和socket線程以後,就開始創建worker線程。每條worker線程會被指定一個權重值,這個權重值決定一條線程一次消費多少條次級消息隊列裏的消息,當權重值< 0,worker線程一次消費一條消息(從次級消息隊列中pop一個消息);當權重==0的時候,worker線程一次消費完次級消息隊列裏所有的消息;當權重>0時,假設次級消息隊列的長度爲mq_length,將mq_length轉成二進制數值以後,向右移動weight(權重值)位,結果N則是,該線程一次消費次級消息隊列的消息數。在多條線程,同時運作時,每條worker線程都要從global_mq中pop一條次級消息隊列出來,對global_mq進行pop和push操作的時候,會用自旋鎖鎖住臨界區,
    // skynet_mq.c
    void 
    skynet_globalmq_push(struct message_queue * queue) {
    	struct global_queue *q= Q;
    
    	SPIN_LOCK(q)
    	assert(queue->next == NULL);
    	if(q->tail) {
    		q->tail->next = queue;
    		q->tail = queue;
    	} else {
    		q->head = q->tail = queue;
    	}
    	SPIN_UNLOCK(q)
    }
    
    struct message_queue * 
    skynet_globalmq_pop() {
    	struct global_queue *q = Q;
    
    	SPIN_LOCK(q)
    	struct message_queue *mq = q->head;
    	if(mq) {
    		q->head = mq->next;
    		if(q->head == NULL) {
    			assert(mq == q->tail);
    			q->tail = NULL;
    		}
    		mq->next = NULL;
    	}
    	SPIN_UNLOCK(q)
    
    	return mq;
    }
    
    這樣出隊操作,只能同時在一條worker線程裏進行,而其他worker線程只能夠進入阻塞狀態,在開的worker線程很多的情況下,始終有一定數量線程處於阻塞狀態,降低服務器的併發處理效率,這裏這麼做第1-4條worker線程,每次只消費一個次級消息隊列的消息,第5-8條線程一次消費整個次級消息隊列的消息,第9-16條worker線程一次消費的消息數目大約是整個次級消息隊列長度的一半,第17-24條線程一次消費的消息數大約是整個次級消息隊列長度的四分之一,而第25-32條worker線程,則大約是次級消息總長度的八分之一。這樣做的目的,大概是希望避免過多的worker線程爲了等待spinlock解鎖,而陷入阻塞狀態(因爲一些線程,一次消費多條甚至全部次級消息隊列的消息,因此在消費期間,不會對global_mq進行入隊和出隊操作,入隊和出隊操作時加自旋鎖的,因此就不會嘗試去訪問spinlock鎖住的臨界區,該線程就在相當一段時間內不會陷入阻塞),進而提升服務器的併發處理能力。這裏還有一個細節值得注意,就是前四條線程,每次只是pop一個次級消息隊列的消息出來,這樣做也在一定程度上保證了沒有服務會被餓死。
    正如本節概述所說,一個worker線程被創建出來以後,則是不斷嘗試從global_mq中pop一個次級消息隊列,並從次級消息隊列中pop消息,進而通過服務的callback函數來消費該消息:
    // skynet_start.c
    static void
    wakeup(struct monitor *m, int busy) {
    	if (m->sleep >= m->count - busy) {
    		// signal sleep worker, "spurious wakeup" is harmless
    		pthread_cond_signal(&m->cond);
    	}
    }
    
    static void *
    thread_timer(void *p) {
    	struct monitor * m = p;
    	skynet_initthread(THREAD_TIMER);
    	for (;;) {
    		skynet_updatetime();
    		CHECK_ABORT
    		wakeup(m,m->count-1);
    		usleep(2500);
    	}
    	// wakeup socket thread
    	skynet_socket_exit();
    	// wakeup all worker thread
    	pthread_mutex_lock(&m->mutex);
    	m->quit = 1;
    	pthread_cond_broadcast(&m->cond);
    	pthread_mutex_unlock(&m->mutex);
    	return NULL;
    }
    
    static void *
    thread_worker(void *p) {
    	struct worker_parm *wp = p;
    	int id = wp->id;
    	int weight = wp->weight;
    	struct monitor *m = wp->m;
    	struct skynet_monitor *sm = m->m[id];
    	skynet_initthread(THREAD_WORKER);
    	struct message_queue * q = NULL;
    	while (!m->quit) {
    		q = skynet_context_message_dispatch(sm, q, weight);
    		if (q == NULL) {
    			if (pthread_mutex_lock(&m->mutex) == 0) {
    				++ m->sleep;
    				// "spurious wakeup" is harmless,
    				// because skynet_context_message_dispatch() can be call at any time.
    				if (!m->quit)
    					pthread_cond_wait(&m->cond, &m->mutex);
    				-- m->sleep;
    				if (pthread_mutex_unlock(&m->mutex)) {
    					fprintf(stderr, "unlock mutex error");
    					exit(1);
    				}
    			}
    		}
    	}
    	return NULL;
    }
    
    // skynet_server.c
    struct message_queue * 
    skynet_context_message_dispatch(struct skynet_monitor *sm, struct message_queue *q, int weight) {
    	if (q == NULL) {
    		q = skynet_globalmq_pop();
    		if (q==NULL)
    			return NULL;
    	}
    
    	uint32_t handle = skynet_mq_handle(q);
    
    	struct skynet_context * ctx = skynet_handle_grab(handle);
    	if (ctx == NULL) {
    		struct drop_t d = { handle };
    		skynet_mq_release(q, drop_message, &d);
    		return skynet_globalmq_pop();
    	}
    
    	int i,n=1;
    	struct skynet_message msg;
    
    	for (i=0;i<n;i++) {
    		if (skynet_mq_pop(q,&msg)) {
    			skynet_context_release(ctx);
    			return skynet_globalmq_pop();
    		} else if (i==0 && weight >= 0) {
    			n = skynet_mq_length(q);
    			n >>= weight;
    		}
    		int overload = skynet_mq_overload(q);
    		if (overload) {
    			skynet_error(ctx, "May overload, message queue length = %d", overload);
    		}
    
    		skynet_monitor_trigger(sm, msg.source , handle);
    
    		if (ctx->cb == NULL) {
    			skynet_free(msg.data);
    		} else {
    			dispatch_message(ctx, &msg);
    		}
    
    		skynet_monitor_trigger(sm, 0,0);
    	}
    
    	assert(q == ctx->queue);
    	struct message_queue *nq = skynet_globalmq_pop();
    	if (nq) {
    		// If global mq is not empty , push q back, and return next queue (nq)
    		// Else (global mq is empty or block, don't push q back, and return q again (for next dispatch)
    		skynet_globalmq_push(q);
    		q = nq;
    	} 
    	skynet_context_release(ctx);
    
    	return q;
    }
    
    static void
    dispatch_message(struct skynet_context *ctx, struct skynet_message *msg) {
    	assert(ctx->init);
    	CHECKCALLING_BEGIN(ctx)
    	pthread_setspecific(G_NODE.handle_key, (void *)(uintptr_t)(ctx->handle));
    	int type = msg->sz >> MESSAGE_TYPE_SHIFT;
    	size_t sz = msg->sz & MESSAGE_TYPE_MASK;
    	if (ctx->logfile) {
    		skynet_log_output(ctx->logfile, msg->source, type, msg->session, msg->data, sz);
    	}
    	if (!ctx->cb(ctx, ctx->cb_ud, type, msg->session, msg->source, msg->data, sz)) {
    		skynet_free(msg->data);
    	} 
    	CHECKCALLING_END(ctx)
    }
    
    整個worker線程的消費流程是:
    a) worker線程每次,從global_mq中彈出一個次級消息隊列,如果次級消息隊列爲空,則該worker線程投入睡眠,timer線程每隔2.5毫秒會喚醒一條睡眠中的worker線程,並重新嘗試從全局消息隊列中pop一個次級消息隊列出來,當次級消息隊列不爲空時,進入下一步
    b) 根據次級消息的handle,找出其所屬的服務(一個skynet_context實例)指針,從次級消息隊列中,pop出n條消息(受weight值影響),並且將其作爲參數,傳給skynet_context的cb函數,並調用它
    c) 當完成callback函數調用時,就從global_mq中再pop一個次級消息隊列中,供下一次使用,並將本次使用的次級消息隊列push回global_mq的尾部
    d) 返回第a步
  3. 線程安全
    • 整個消費流程,每條worker線程,從global_mq取出的次級消息隊列都是唯一的,並且有且只有一個服務與之對應,取出之後,在該worker線程完成所有callback調用之前,不會push回global_mq中,也就是說,在這段時間內,其他worker線程不能拿到這個次級消息隊列所對應的服務,並調用callback函數,也就是說一個服務不可能同時在多條worker線程內執行callback函數,從而保證了線程安全
      image
    • 不論是global_mq也好,次級消息隊列也好,他們在入隊和出隊操作時,都有加上spinlock,這樣多個線程同時訪問mq的時候,第一個訪問者會進入臨界區並鎖住,其他線程會阻塞等待,直至該鎖解除,這樣也保證了線程安全。global_mq會同時被多個worker線程訪問,這個很好理解,因爲worker線程總是在不斷嘗試驅動不同的服務,要驅動服務,首先要取出至少一個消息,要獲得消息,就要取出一個次級消息隊列,而這個次級消息隊列要從全局消息隊列裏取。雖然一個服務的callback函數,只能在一個worker線程內被調用,但是在多個worker線程中,可以向同一個次級消息隊列push消息,即便是該次級消息隊列所對應的服務正在執行callback函數,由於次級消息隊列不是skynet_context的成員(skynet_context只是包含了次級消息隊列的指針),因此改變次級消息隊列不等於改變skynet_context上的數據,不會影響到該服務自身內存的數據,次級消息隊列在進行push和pop操作的時候,會加上一個spinlock,當多個worker線程同時向同一個次級消息隊列push消息時,第一個訪問的worker線程,能夠進入臨界區,其他worker線程就阻塞等待,直至該臨界區解鎖,這樣保證了線程安全。
    • 我們在通過handle從skynet_context list裏獲取skynet_context的過程中(比如派發消息時,要要先獲取skynet_context指針,再調用該服務的callback函數),需要加上一個讀寫鎖,因爲在skynet運作的過程中,獲取skynet_context,比創建skynet_context的情況要多得多,因此這裏用了讀寫鎖:
      struct skynet_context * 
      skynet_handle_grab(uint32_t handle) {
      	struct handle_storage *s = H;
      	struct skynet_context * result = NULL;
      
      	rwlock_rlock(&s->lock);
      
      	uint32_t hash = handle & (s->slot_size-1);
      	struct skynet_context * ctx = s->slot[hash];
      	if (ctx && skynet_context_handle(ctx) == handle) {
      		result = ctx;
      		skynet_context_grab(result);
      	}
      
      	rwlock_runlock(&s->lock);
      
      	return result;
      }
      
      這裏加上讀寫鎖的意義在於,多個worker線程,同時從skynet_context列表中獲取context指針時,沒有一條線程是會被阻塞的,這樣提高了併發的效率,而此時,嘗試往skyent_context裏表中,添加新的服務的線程將會被阻塞住,因爲添加新的服務可能會導致skynet_context列表(也就是代碼裏的slot列表)可能會被resize,因此讀的時候不允許寫入,寫的時候不允許讀取,保證了線程安全。
  • 發送消息流程
    向一個服務發送消息的本質,就是向該服務的次級消息隊列裏push消息,多個worker線程可能會同時向同一個服務的次級消息隊列push一個消息,正如上節所說的那樣,次級消息隊列push和pop操作,都有加一個spinlock,從而保證了線程安全,上節已經說明了,這裏不再贅述。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章