gnugk5.5源码分析(1)之多线程模型

前言

gnugk是对H323协议的一个应用程序,其主要是实现了H323协议账号注册、呼叫代理转接等功能,其源码采用的编程语言是C++;本系列文章主要是希望分析以功能实现为划分,分析其源码实现,所采用的源码版本是5.5版本。

一、多线程角色划分

在gnugk5.5中,为了实现对特定的任务进行多线程执行,其内部实现了一个多线程模型。按角色划分为Agent(代理)、Worker(执行者)、Job(具体的任务),这三个角色;其相互之间的关系是Agent管理Worker,而Worker执行Job,其中Worker继承自Thread,为线程对象;从而实现了每个Job都在特定不同的线程上执行,即达到多线程的实现。其具体类关系图如下所示。

从图中可以看到Job是抽象基类,继承其的子类有4种,分别是Jobs类、RegularJob类、SimpleClassJob模板类、SimpleClassJobA模板类;其中Jobs类指的是执行一系列的任务,但每个任务只被执行一次;而ReqularJob指的是周期性地执行同一个任务,直到被调用停止;SimpleClassJob指的是执行某个其它类的成员函数,该函数无参数需求;SimpleClassJobA指的是执行某个其它类的成员函数,该函数需要传递一个参数。而Worker继承自PThread类,为一个线程类;Agent类继承自Singleton类,为单实例类。

我们可以用一段比较直白的话来描述下这个类图实现:有一个公司(Agent)成立了,其支持代理执行客户提交给它的任务(Job),具体来说,这家公司支持4种不同业务的工作(Jobs、ReqularJob、SimpleClassJob、SimpleClassJobA),当有任务要执行时,这个公司(Agent)会寻找一个空闲的工人(Worker)来执行,若此时没有空闲的工人,则再招一个工人进来(New Worker);而如果长时间没有新的工作,则把空闲很久的工人给裁员掉(Delete Worker)
在这里插入图片描述

二、多线程框架实现

整个框架实现由Job、Worker、Agent类搭建而成。
(1)由外部使用者创建一个Job实例A(或其子类实例),而后调用A.Execute()方法,开始执行。
(2)从Job::Execute实现,可以看到,这只是把这个Job对象提交给Agent。

void Job::Execute()
{
	Agent::Instance()->Exec(this);
}

(3)在Agent::Exec(Job * job)的内部,主要是找一个空闲的Worker,把这个Job提交给这个Worker。
具体上,Agent内部有两个列表,std::list<Worker*> m_idleWorkers表示内部当前已创建,而处于空闲的Worker线程;std::list<Worker*> m_busyWorkers表示当前正在忙碌的线程;Agent的逻辑是当通过Agent::Exec(Job * job)接收一个Job时,先判断m_idleWorkers内部有没有空闲的线程,若有取一个出来,接受这个Job任务,并且把这个Worker线程,放入m_busyWorkers中跟踪;若m_idleWorkers中没有空闲的Worker线程,则新建一个Worker,接受这个Job任务,也放入m_busyWorkers中跟踪;
最终调用worker->Exec(job),启动Worker线程异步执行Job任务。

(4)Worker::Exec(Job * job)是启动了异常执行Job任务,其本质就是发了一个信号量,m_wakeupSync.Signal(),而这个信号量的等待的地方就是这个线程的main处理内部(Worker::Main());此时发了一个信号量后,线程内部就可以正常往下执行。

bool Worker::Exec(Job * job)
{
	// fast check if there is no job being executed
	if (m_job == NULL && !m_closed) {
		PWaitAndSignal lock(m_jobMutex);
		// check again there is no job being executed
		if (m_job == NULL && !m_closed) {
			m_job = job;
			m_wakeupSync.Signal();
			return true;
		}
	}
	return false;
}

(5)Worker内部执行Job任务。

void Worker::Main()
{
	m_id = GetThreadId();
	PTRACE(5, "JOB\tWorker " << m_id << " started");

	while (!m_closed) {
		bool timedout = false;
		// wait for a new job or idle timeout expiration
		if (m_job == NULL) {
		    // 这里m_wakeupSync在执行超时等待,若在超时前,接受任务,Worker::Exec释放了信号量,这里就
		    // 可以接收信号,继续往下执行。
			timedout = !m_wakeupSync.Wait(m_idleTimeout);
			if (timedout) {
				PTRACE(5, "JOB\tIdle timeout for Worker " << m_id);
			}
		}
		// terminate this worker if closed explicitly or idle timeout expired
		// 而等待Job任务超时,而说明这个Worker已经空闲足够长时间了,可以释放掉Worker,避免一直占用系统
		// 资源
		if (m_closed || (timedout && m_job == NULL)) {
			m_closed = true;
			break;
		}

		if (m_job) {
			PTRACE(5, "JOB\tStarting Job " << m_job->GetName() << " at Worker thread " << m_id);
			
			// 若在超时时间内,正常接受了一个job任务,则调用Job::Run()执行这个Job。
			// 这里Job::Run()是一个纯虚函数,由Job的子类实现如何具体执行。
			// 可以看到对于Worker线程来说,Worker线程并不管一个Job对象具体是如何执行的,而是的Job自身
			// 来实现,这也是面向对象编程的思想体现之一,Worker与Job业务上脱耦。
			m_job->Run();

			{
				PWaitAndSignal lock(m_jobMutex);
				// 当Job任务执行完成后,Job对象由Worker内部自动删除,释放资源
				delete m_job;
				m_job = NULL;
			}

			// Worker线程通知Agent,当前Job任务已经执行完成,Agent把当前Worker从m_busyWorkers中删除
			// 添加回m_idleWorkers列表,以便于可以接受下一个Job任务。
			m_agent->JobDone(this);
		}
	}

	PTRACE(5, "JOB\tWorker " << m_id << " closed");

	// remove this Worker from the list of workers
	// 前面讲过了,如果当前这个worker空闲太久了,则可以删除掉,避免占用资源。
	m_agent->Remove(this);
	if (m_job) {
		PTRACE(1, "JOB\tActive Job " << m_job->GetName()
			<< " left at closing Worker thread " << m_id);
	}
}

总结一下,上述流程。可以得到下面这个流程图。
在这里插入图片描述

三、Job的子类

从前面的讲述,可以明确到两点:
第一:Job类目前有四个子类,Jobs、ReqularJob、SimpleClassJob、SimpleClassJobA;
第二:Job的子类,只需要实现Job::Run方法即可。这样就能起到一个多线程执行的框架实现。

3.1 Jobs类

Jobs类定义为执行一系列任务(Task类);因此Jobs类是依赖于Task类的。
Task是一个抽象类,其实现是为了把一系列任务串起来,因此该类有一个Task* m_next成员,用于指向下一个任务,而具体的任务要怎么被执行,由virtual void Task::Exec() = 0方法实现,该方法是一个纯虚函数,由其子类具体实现。

因此现在,我们可以看一下Jobs的run方法;可以很直观地看到,就是挨个的执行Task实例,调用Task::Exec方法完成执行,执行完成后,Task::DoNext获取下一个Task实例,继续执行。

void Jobs::Run()
{
	while (m_current) {
		m_current->Exec();
		m_current = m_current->DoNext();
	}
}

后续在需要这种单次运行一系列任务Task的场景时,只需要做两步:
第一:继承自Task,实现具体的每个任务要怎么执行,即实现Task::Exec()方法。
第二:把这个任务链的起始任务(Task实例)提交给Jobs类来执行。

3.2 RegularJob类

RegularJob类被定义为周期性执行某一个具体任务,直到任务被停止。
我们先看下RegularJob是如何实现Job::Run方法的。

void RegularJob::Run()
{
	OnStart();

	while (!m_stop)
		Exec();

	// lock to allow a member function that is calling Stop
	// return before OnStop is called and the object is deleted
	PWaitAndSignal lock(m_deletionPreventer);
	OnStop();
}

可以看到RegularJob::OnStart和RegularJob::OnStop分别在具体的任务被执行前后回调一次给其子类实现者,可以让子类完成相应的初始化和清理工作;同时RegularJob类通过约定void RegularJob::Exec() = 0方法,由子类具体实现相应的一个任务应该如何被执行;从代码可看到只要m_stop没有被置上,则永远在while执行;而标置位只在RegularJob::Stop方法中被设置,即只有上层应用者主动调用了这个方法才能退出这个任务执行的循环。

3.3 SimpleClassJob模板类和SimpleClassJobA模板类

SimpleClassJob模板类和SimpleClassJobA模板类本质上是一样的,定义为另起线程来执行某个其它类对象的某个成员函数,所不同的SimpleClassJob类没有传参,而SimpleClassJobA接受一个参数。以SimpleClassJob为例,看其实现代码。

template<class T>
class SimpleClassJob : public Job {
public:
	SimpleClassJob(T *_t, void (T::*_j)()) : t(_t), j(_j) { }
	virtual void Run() { (t->*j)(); }

private:
	T *t;
	void (T::*j)();
};

SimpleClassJob实例化过程就是接受一个类实例指针T _t和该类的一个成员函数void (T::_j)()并被保存在其内部的T *t成员和void (T::*j)()成员上。在实现Job::Run方法时,就是回调执行之前该类的成员函数,void Run() { (t->*j)(); }。

SimpleClassJobA是一样的,只是实例化时多传递了一个T类对象的_j方法的参数A而已。

而为了方便使用,还分别重载实现了两个调用方法,CreateJob。以其中一个来看。

template<class T>
void CreateJob(T *t, void (T::*j)(), const char *jobname)
{
	Job *newjob = new SimpleClassJob<T>(t, j);
	newjob->SetName(jobname);
	newjob->Execute();
}

template <class T, class A>
void CreateJob(T *t, void (T::*j)(A), A a, const char *jobname)
{
	Job *newjob = new SimpleClassJobA<T, A>(t, j, a);
	newjob->SetName(jobname);
	newjob->Execute();
}

调用方法也很简单,传递三个参数,T类实例指针t, t对象需要回调执行的函数j, 以及这个任务的名称jobname。
比如RasServer实例对其的调用:CreateJob(this, &RasServer::HouseKeeping, "HouseKeeping")

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值