竊取式調度器(Stealing Scheduler)-高併發

原文轉自:http://www.tanjp.com/archives/141 (即時修正和更新)

 

竊取式調度器(Stealing Scheduler)

N個業務系統生產作業加入到 M+1個隊列裏面(優先加入到當前線程所在隊列),隊列中的作業被 M個線程按一定的規則消費。M個線程都對應一個線程局部存儲的隊列,和一個公共的隊列。該規則按以下次序執行:

1、優先處理本線程生產的作業。

2、其次處理默認的隊列的作業。

3、竊取下一個線程隊列中的作業。

4、竊取上一個線程隊列中的作業。

也就是說,當作業量龐大時,各個線程忙着處理各自隊列,當線程自己的隊列處理完,才處理默認的隊列和從相鄰線程的竊取作業來執行。線程競爭抽象爲:M+(M*1+M*1+M*2)/3 = 2M。這是搶佔式與分配式兩種方案優點的結合。

         push             pop
job 1 ---->| ===== queue 1 > ##### thread 1
                |    ↓↑                      
job 2 ---->| ===== queue 2 > ##### thread 2
                 | ...↓↑                  
job N ---->| ===== queue M > ##### thread M
                 |    ↓↑ 
                 | ===== default queue

互斥鎖和無鎖方案

竊取式調度,都是要按以上的次序,逐個進行嘗試並取出作業來處理,所以都不能採用掛起等待的方式。也就是說,push和pop操作都是立即返回成功或失敗。爲了不丟失數據(push的時候不會因爲隊列滿而掛起),一般都爲無界隊列,並由業務層來控制隊列中作業數量的上限。竊取式的實現細節在於採用了線程局部存儲變量(只能被一個線程來讀寫)。

部分實現代碼:

class LockfreeStealingScheduler : public SchedulerBase
{
	typedef boost::lockfree::queue< Task *, boost::lockfree::fixed_sized<false> > LockfreeQueue;
public:
    	explicit LockfreeStealingScheduler(uint32 pn_thread_count = 8U);
    	~LockfreeStealingScheduler();
    	bool start() override;
    	bool post(Task * pp_optype) override;
    	bool stop() override;
private:
	void loop_running(uint16 pn_index);
private:
	const uint32 kThreadCount;
	LockfreeQueue mc_default_queue;
	LockfreeQueue * mc_queues[kThreadMaxCount]; //每個線程都有各自的隊列
	std::thread * mc_threads[kThreadMaxCount]; //線程集合
	std::atomic<bool> mb_started;
	std::atomic<bool> mb_available;
	std::atomic<bool> mb_destroy;
	std::mutex mo_mutex;
	static thread_local LockfreeQueue * mp_local_queue;
	static thread_local uint16 mn_local_index;
};
bool LockfreeStealingScheduler::start()
{
	std::lock_guard<std::mutex> lock(mo_mutex); //這個鎖主要保護,多個線程同時調用此函數
	if (mb_started.load() || mb_destroy.load())
	{
		return false;
	}
	mb_started.store(true);
	for (uint32 i = 0; i < kThreadCount; ++i)
	{
		mc_queues[i] = new LockfreeQueue(1024);
	}
	for (uint32 i = 0; i < kThreadCount; ++i)
	{
		mc_threads[i] = new std::thread(std::bind(&LockfreeStealingScheduler::loop_running, this, i));
	}
	mb_available.store(true);
	return true;
}

void LockfreeStealingScheduler::loop_running(uint16 pn_index)
{
	mn_local_index = pn_index;
	mp_local_queue = mc_queues[mn_local_index];
	Task * zf_task = 0;
	bool zb_had_task = false;
	while (true)
	{
		//線程局部存儲中取
		zb_had_task = mp_local_queue->pop(zf_task);
		if (!zb_had_task)
		{
			//全局中取
			zb_had_task = mc_default_queue.pop(zf_task);
		}
		if (!zb_had_task)
		{
			//從下一個線程存儲中竊取
			const uint16 zn_index = (mn_local_index + 1) % kThreadCount;
			zb_had_task = mc_queues[zn_index]->pop(zf_task);
		}
		if (!zb_had_task)
		{
			//從上一個線程存儲中竊取
			const uint16 zn_index = (mn_local_index + kThreadCount - 1) % kThreadCount;
			zb_had_task = mc_queues[zn_index]->pop(zf_task);
		}
		if (zb_had_task)
		{
			//有任務可以處理
			zf_task->execute();
			zf_task->done();
		}
		else
		{
			//沒有任務
			THIS_SLEEP_MILLISECONDS(1);
		}
		if (!mb_started.load() && mc_default_queue.empty() && mp_local_queue && mp_local_queue->empty())
		{
			break;
		}
	} //while
}
thread_local LockfreeStealingScheduler::LockfreeQueue * LockfreeStealingScheduler::mp_local_queue = nullptr;
thread_local uint16 LockfreeStealingScheduler::mn_local_index = 0;

性能測試

1生產者M消費者,主線程一次性插入 2000000條作業:

有鎖實現耗時(毫秒): 12456

無鎖實現耗時(毫秒): 8080

 

N生產者M消費者,測試作業內容如下:

主線程一次性插入 20條作業,並 3個線程消耗完後,遞歸插入新的作業,直到作業數量超過 2000000條。

有鎖實現耗時(毫秒): 9560

無鎖實現耗時(毫秒): 9810

 

總結

1、竊取式調度器的實現,有鎖與無鎖性能差距不大。

2、隊列都是無界的,只能由業務系統來協調,避免隊列爆滿的情況。

 

 

 

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