搶佔式調度器(Preemptive Scheduler)-有鎖與無鎖實現

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

 

搶佔式調度器(Preemptive Scheduler)

N個業務系統生產作業加入到一個隊列裏面,隊列中的作業被 M個線程搶先消費。也就是說,N的業務系統搶着把生產出來的作業插入到隊列,同時 M個線程搶着消費該隊列的作業,對隊列的搶佔非常激烈。可簡單競爭抽象爲: N*M。

           push              pop
job 1 ---->|                 |##### thread 1
                |  queue     |##### thread 2
job 2 ---->| ======>  |##### thread 3
                |                 |...
job N ---->|                 |##### thread M

條件變量與互斥鎖方案

當隊列爲空時,各個線程由條件變量觸發掛起等待。有作業加入到隊列時,條件變量會喚醒一個等待中的線程,取出作業。隊列的push和pop,分別由兩個條件變量來協同 N個作業與 M個線程,會較爲頻繁進行"掛起-喚醒"的系統調用。可以將鎖的競爭概率抽象爲 N*M。此方案中的隊列,可以爲有界隊列,也就是說有作業數量上限,可以避免在極端情況下,隊列爆滿導致暴內存的問題。隊列中元素的數量範圍[0, C]。數量爲0,表示隊列爲空,掛起等待作業插入。數量爲C,表示隊列滿,掛起等待作業被處理。

部分實現代碼如下:

class PreemptiveScheduler : public SchedulerBase
{
public:
    	// @pn_thread_count, 進行搶佔的線程數量。
    	explicit PreemptiveScheduler(uint32 pn_thread_count = 8U);
    	~PreemptiveScheduler();
    	bool start() override;
     bool post(Task * pp_task) override;
     bool stop() override;
private:
	void loop_running();
private:
	const uint32 kThreadCount;
	SafeQueue< Task* > mc_queue;
	std::atomic<bool> mb_started;
	std::atomic<bool> mb_available;
	std::atomic<bool> mb_destroy;
	std::thread * mv_threads[kThreadMaxCount];
	std::mutex mo_mutex;
};
bool PreemptiveScheduler::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)
	{
		mv_threads[i] = new std::thread(std::bind(&PreemptiveScheduler::loop_running, this));
	}
	mb_available.store(true);
 	return true;
}
bool PreemptiveScheduler::post(Task * pp_task)
{
	return mc_queue.push(pp_task);
}
void PreemptiveScheduler::loop_running()
{
	Task * zf_task = 0;
	bool zb_had_task = false;
	while (true)
	{
		zb_had_task = mc_queue.pop(zf_task);
		if (zb_had_task)
		{
			//有任務可以處理
			zf_task->execute();
			zf_task->done();
		}
		else
		{
			//沒任務 - 在阻塞模式下會由條件變量掛起等待
		}
		if (!mb_started.load() && mc_queue.empty())
		{
			mb_available.store(false);
			break;
		}
	} //while
}

無鎖隊列方案

無鎖(lock free),其實是另一種多線程環境下的數據同步的方案。其應用上的特點是 push和pop兩個函數不會掛起等待。爲了不丟失數據(push的時候不會因爲隊列滿而掛起),一般都爲無界隊列,並有業務層來控制隊列中作業數量的上限。

當隊列爲空時,線程爲了不佔用CPU,通過調用 sleep(),來減少對CPU無效的消耗。注意,不使用 std::this_thread::yield()方式,因爲 yield只是讓出當前CPU時間片,下一時間片還是會進行搶佔,會導致CPU無效消耗。

部分實現代碼如下:

class LockfreePreemptiveScheduler : public SchedulerBase
{
public:
    	// @pn_thread_count, 進行搶佔的線程數量。
    	explicit LockfreePreemptiveScheduler(uint32 pn_thread_count = 8U);
    	~LockfreePreemptiveScheduler();
    	bool start() override;
    	bool post(Task * pp_task) override;
    	bool stop() override;
private:
	void loop_running();
private:
	const uint32 kThreadCount;
	LockfreeQueue mc_queue; // 無鎖隊列
	std::atomic<bool> mb_started;
	std::atomic<bool> mb_available;
	std::atomic<bool> mb_destroy;
	std::thread * mv_threads[kThreadMaxCount];
	std::mutex mo_mutex;
};
void LockfreePreemptiveScheduler::loop_running()
{
	Task * zf_task = 0;
	bool zb_had_task = false;
	while (true)
	{	
		zb_had_task = mc_queue.pop(zf_task);
		if (zb_had_task)
		{
			//有任務可以處理
			zf_task->execute();
			zf_task->done();
		}
		else
		{
			//無鎖實現, 避免過度佔用CPU,搶佔式使用 sleep(而不是yield) 來釋放CPU,減少競爭
			THIS_SLEEP_MILLISECONDS(1);
		}
		if (!mb_started.load() && mc_queue.empty())
		{
			mb_available.store(false);
			break;
		}

	} //while
}

性能測試

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

std::atomic<uint32> test1_counter = 0;
void test1_func()
{
    	std::string s;
    	for (uint32 i = 0; i < 10; ++i)
    	{
    		s.append(1, (char)i);
    	}
    	s.reserve();
    ++test1_counter;
}

主線程一次性插入 2000000條作業,由 3個線程消耗。

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

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

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

std::atomic<uint32> test2_counter = 0;
void test2_func(Scheduler * pp_schd, uint32 pn_index, boost::timer::cpu_timer * pp_timer)
{
    	std::string s;
    	for (uint32 i = 0; i < 10; ++i)
    	{
    		s.append(1, (char)i);
    	}
    	s.reserve();

	++test2_counter;
	if (test2_counter >= 2000000)
	{
		boost::timer::cpu_times time = pp_timer->elapsed();
		std::cout << std::endl << "end, use : " << (time.wall / 1000000U) 
			<< " millisecond, count " << test2_counter.load() << std::endl;
		return;
	}
	bool zb_ok = pp_schd->post(std::bind(&test2_func, pp_schd, pn_index + 1, pp_timer));
	if (!zb_ok)
	{
		std::cout << "test2_func ERROR " << pn_index << std::endl;
	}
}

 

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

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

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

總結

1、搶佔式調度器的實現,無鎖要比有鎖性能高,當線程數量越多無鎖比有鎖性能高越多。

2、有鎖隊列是有界的,更能控制邊界,避免內存爆滿。無鎖隊列,只能由業務系統來協調。

 

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