抢占式调度器(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、有锁队列是有界的,更能控制边界,避免内存爆满。无锁队列,只能由业务系统来协调。

 

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