下面的项目是两年前学校老师布置的一个小项目,当时自己用了一种很笨拙的方式实现了,现在用面向对象的思想和多线程重构这个项目。
问题描述:
西宝高速仿真模拟
西安市到宝鸡市之间是我省主要的高速公路客运路线之一,经过简化后的客运路线端点、中途停靠点和里程如下图所示(括号里是简称,里程的单位是公里):
- 限定条件
(1) 从XN始发至BJ的客车和从BJ始发至XN的客车均有两种车型:沃尔沃(限定乘客人数为40人);依维柯(限定乘客人数为21人)。沃尔沃的速度为2公里/分钟,依维柯的速度为1.4公里/分钟。
(2) 起始状态时,XN拥有沃尔沃和依维柯客车分别为XNW和XNY辆,BJ拥有沃尔沃和依维柯客车分别为BJW和BJY辆。
(3) 从XN至BJ和从BJ至XN的沃尔沃,均为上午8:30开始,每小时一班,最后一班为下午5:30;从XN至BJ和从BJ至XN的依维柯,均为上午8:00开始,每20分钟一班,最后一班为下午6:00。
(4) 从XN至BJ的客车到达BJ后,即成为从BJ至XN的客车,排在当时BJ同类车型的队尾,再按(3)确定发车时间;从BJ至XN的客车到达XN后的规则相同。
(5) 只考虑途中只有乘客下车、没有乘客上车的情况。
(6) 有乘客下车时,不论方向与车型,停车时间统一为2分钟。
(7) 乘坐从XN至BJ客车的乘客,其下车点为XY、XP、WG、CP、GZ和BJ的可能性分别为P_XBXY、P_XBXP、P_XBWG、P_XBCP、P_XBGZ和P_XBBJ。这些可能性之和为1;乘坐从BJ至XN客车的乘客,其下车点为GZ、CP、WG、XP、XY和XN的可能性分别为P_BXGZ、P_BXCP、P_BXWG、P_BXXP、P_BXXY和P_BXXN。这些可能性之和为1。
- 需仿真的活动
(1) 从上午7:30开始到下午5:59为止,每分钟分别在XN和BJ随机产生去往BJ和XN方向的新到达的乘客。每分钟达到的人数范围为0~PN人。
(2) 按照限定条件(7)的规定,随机产生新到达的乘客的目的地。
(3) 乘客按到达的先后顺序上最近一辆(依照限定条件(3)的规定)始发的客车,若该车客满则等候下一辆始发的客车。
(4) 若客车到达中途停靠站时有乘客在此下车,按限定条件(5)和(6)处理,否则不停车继续行驶。
我们逐步分析最关键的点:
我们先仅仅模拟一辆客车从西安到宝鸡的过程,中途遇到的中间站停车2分,没有乘客参与,仅仅是让这辆客车从西安跑到宝鸡。
这个简单的问题,直观的解决方案是:一个大循环,每次循环时间更新一次,在循环内更新客车的位置,判断客车时候到达中间站或终点站。这种解决方式思想简单,但是可扩展性差,若有新种类的客车,则我们需要重新改写主逻辑。
我们用面向对象的思维来分析这个简单的仿真模拟过程,实际上就是客车启动、行驶、中途停车、结束。这几个状态间的转化。可以用状态模式来解决这个问题。
思路:
客车类接收外界传入的时间,其初始时调用启动状态指针,并把自己作为参数传入,状态类根据外界条件(时间)和规则(客车时刻表),来判断出下个状态是什么(并更新客车类中保存的状态码)完成状态转换。
这样,客车只是一直调用其当前状态码对应的状态指针来运行逻辑,(状态类对象指针的函数悄悄地改变了客车类中的当前状态码,这样,在客车不知不觉地过程中,完成了状态的转换)
class Vehicle
{
public:
Vehicle()
{
_brand = "Volvo" ;
_identifier = 1 ;
_speed = 2 ;
_driveDirect = FORWARD ;
_curStateNo = 0 ;
_curPos = 0 ;
}
int Init(const Time& curTime) ;
//run
int Running(const Time& curTime) ;
//根据当前时间,返回vehicle当前状态:起点start、路上running、中间站停车midStop、终点endStop
int GetVehicleState(const Time& curTime) ;
private:
std::string _brand ; //Vehicle品牌(名字)
int _identifier ; //Vehicle的编号(不同品牌分别编号)
double _speed ; //车速(单位为:公里/分钟)
int _passengerNumLimit ; //载客量
int _curStateNo ; //当前Vehicle所处状态码
DirectKind _driveDirect ; //当前Vehicle的行驶方向
int _curPos ; //当前位置(离始发站的距离)
//每个Vehicle都有一张状态码和状态对象映射表,我们在Vehicle初始化的时候创建所有状态对象
std::map<int, VehicleState*> _vehicleStateMap ;
//Vehicle运行时间表(每一站的到达时间和发车时间)
std::vector<std::pair<Time, std::string> > _vehicleSchedule ;
//改变当前状态
VehicleState* ChangeState(int destStateNo) ;
//计算运行时刻表
int CalcVehicleSchedule(const Time& startTime, const DirectKind& driveDirect) ;
friend class VehicleState ;
} ;
客车对外的接口只有running();
而running所做的工作只是调用当前客车状态指针的process函数,并把自己和当前时间作为参数传入。
把主要的逻辑和处理交给客车状态对象去做。
int Vehicle::Running(const Time& curTime)
{
int ret ;
ret = _vehicleStateMap[_curStateNo]->Process(this, curTime);
if (ret == -1)
return -1 ;
return 0 ;
}
//客车状态类
//交通工具接口(抽象类)
class VehicleState
{
public:
VehicleState() {}
virtual int Process(Vehicle* pVehicle, const Time& curTime) = 0 ;
protected:
int ChangeState(Vehicle* pVehicle , int destStateNo);
} ;
//启动状态
class StartState : public VehicleState
{
public:
int Process(Vehicle* pVehicle, const Time& curTime) ;
} ;
//行驶状态
class RunningState : public VehicleState
{
public:
int Process(Vehicle* pVehicle, const Time& curTime) ;
} ;
//中途停车状态
class MidStopState : public VehicleState
{
public:
int Process(Vehicle* pVehicle, const Time& curTime) ;
} ;
//到站停车状态
class EndStopState : public VehicleState
{
public:
int Process(Vehicle* pVehicle, const Time& curTime) ;
} ;
在状态类的process函数中,所做的工作是:1、处理当前状态下的事情。2、根据逻辑改变客车的当前状态(所以,状态类是客车类的友元)
int RunningState::Process(Vehicle* pVehicle, const Time& curTime)
{
std::cout << "Run\n" ; //在当前运行状态下,我们仅仅代表性地输出Run。
//先判断当前情况下能否行车(是否到站,根据时间判断:初始发车时 车会获得一个发车时间和和乘客信息,此时计算运行时刻表,每次启动的时候都要计算)
Time nextTime = curTime ;
nextTime.AddTime(1) ;
int nextVehicleState = 0 ;
nextVehicleState = pVehicle->GetVehicleState(nextTime) ;
//转换到下一个状态(根据时间判断是否:中途停车、终点停车、在路上)
if (nextVehicleState == -1)
{
return -1 ;
}
if (nextVehicleState == MIDSTOP)
{
ChangeState(pVehicle,MIDSTOP) ;
}
else if (nextVehicleState == ENDSTOP)
{
ChangeState(pVehicle,ENDSTOP) ;
}
return 0 ;
}
我们把主要的逻辑写在状态类中,且状态的转化也是在状态类中完成的,客车类并不知道。
这样,在外部循环中,我们只需要调用客车的running函数且把时间传入即可,其中的运行和状态转化会自动进行。
状态模式
使用状态模式前,客户端外界需要介入改变状态,而状态改变的实现是琐碎或复杂的。
使用状态模式后,客户端外界可以直接使用事件Event实现,根本不必关心该事件导致如何状态变化,这些是由状态机等内部实现。
这是一种Event-condition-State,状态模式封装了condition-State部分。
每个状态形成一个子类,每个状态只关心它的下一个可能状态,从而无形中形成了状态转换的规则。如果新的状态加入,只涉及它的前一个状态修改和定义。
状态转换有几个方法实现:一个在每个状态实现next(),指定下一个状态(本文中就是使用这种方法);还有一种方法,设定一个StateOwner,在StateOwner设定stateEnter状态进入和stateExit状态退出行为。
状态从一个方面说明了流程,流程是随时间而改变,状态是截取流程某个时间片。
关于状态机的一个极度确切的描述是它是一个有向图形,由一组节点和一组相应的转移函数组成。状态机通过响应一系列事件而“运行”。每个事件都在属于“当前” 节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少有一个必须是终态。当到达终态, 状态机停止。
使用情景
State模式在实际使用中比较多,适合”状态的切换”.因为我们经常会使用If elseif else 进行状态切换, 如果针对状态的这样判断切换反复出现,我们就要联想到是否可以采取State模式了.
不只是根据状态,也有根据属性.如果某个对象的属性不同,对象的行为就不一样,这点在数据库系统中出现频率比较高,我们经常会在一个数据表的尾部,加上property属性含义的字段,用以标识记录中一些特殊性质的记录,这种属性的改变(切换)又是随时可能发生的,就有可能要使用State.
【注意:若是根据不同的条件有不同的处理,这种if-else不必用状态模式,直接用表驱动即可,用查表的方式设计更合理】
在实际使用,类似开关一样的状态切换是很多的,但有时并不是那么明显,取决于你的经验和对系统的理解深度.
这里要阐述的是”开关切换状态” 和” 一般的状态判断”是有一些区别的, ” 一般的状态判断”也是有 if..elseif结构,例如:
if (which==1) state="hello";
else if (which==2) state="hi";
else if (which==3) state="bye";
这是一个 ” 一般的状态判断”,state值的不同是根据which变量来决定的,which和state没有关系.
如果改成:
if (state.euqals("bye")) state="hello";
else if (state.euqals("hello")) state="hi";
else if (state.euqals("hi")) state="bye";
这就是 “开关切换状态”,是将state的状态从”hello”切换到”hi”,再切换到”“bye”;在切换到”hello”,好象一个旋转开关,这种状态改变就可以使用State模式了.
如果单纯有上面一种将”hello”–>”hi”–>”bye”–>”hello”这一个方向切换,也不一定需要使用State模式,因为State模式会建立很多子类,复杂化,但是如果又发生另外一个行为:将上面的切换方向反过来切换,或者需要任意切换,就需要State了.
多线程
刚才我们解决了一个核心问题,让客车动起来。现在我们要实现的是同时让多辆客车行驶起来。
我们可以用串行的方式来模拟这个过程:用同一时刻时间值来遍历所有的客车,激发客车的运行,模拟出在某时刻多辆客车运行的效果。
我们用多线程的方式来仿真这一过程,每一辆客车的运行由一个线程负责,在某时刻客车线程同时运行。
本项目中,使用的是Unix下的线程同步机制——条件变量,关于条件变量
条件变量(cond)
当我们遇到期待的条件尚未准备好时,我们应该怎么做?我们可以一次次的循环判断条件是否成立,每次给互斥锁解锁又上锁。这称为轮询(polling),是一种对CPU时间的浪费。
我们也许可以睡眠很短的一段时间,但是不知道该睡眠多久。
我们所需的是另一种类型的同步,它允许一个线程(或进程)睡眠到发生某个时间为止。
互斥量用于上锁,条件变量则用于等待。则两种不同类型的同步都是需要的。
条件变量是与互斥量一起使用的,因为条件本身是由互斥量保护的,线程在改变条件状态前必须首先锁住互斥量。
- API
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t mutex) ;
使用pthread_cond_wait等待条件变为真,传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁。pthread_cond_wait返回时,互斥量再次被锁住。
- 示范代码:
pthread_mutex_lock(&var.mutex) ;
while (条件为假)
{pthread_cond_wait(&var.cond, &var.mutex) ;}
修改条件
pthread_mutex_unlock(&var.mutex) ;
通知线程条件已满足:
int pthread_cond_signal (pthread_cond_t* cond) ;
//唤醒等待条件的某个线程
int pthread_cond_broadcast (pthread_cond_t* cond) ;
//唤醒等待该条件的所有线程
【代码示例】
struct
{
pthread_cond_t cond ;
pthread_mutex_t mutex ;
int continueRun ;
} oneready = {
PTHREAD_COND_INITIALIZER ,
PTHREAD_MUTEX_INITIALIZER,
0
} ;
struct
{
pthread_cond_t cond ;
pthread_mutex_t mutex ;
int pthreadNum ;
} allready = {
PTHREAD_COND_INITIALIZER ,
PTHREAD_MUTEX_INITIALIZER,
0
} ;
Time g_curTime ;
int g_curBusNum = 0 ;
pthread_mutex_t mutexTime = PTHREAD_MUTEX_INITIALIZER ;
pthread_mutex_t mutexBusNum = PTHREAD_MUTEX_INITIALIZER ;
//主线程
for (int i=0; i<130; ++i) { //130只模拟130分钟,此是为了示范而写
startStation.Run(g_curTime) ;//会根据时间表来生成客车线程
//等待所有线程完成一轮工作(若当前无线程则跳过)
pthread_mutex_lock(&allready.mutex) ;
while(allready.pthreadNum != -g_curBusNum)
{
//若所有的线程都销毁了,则本线程不能继续阻塞等待
pthread_mutex_lock(&mutexBusNum) ;
bool allEnded = (g_curBusNum == 0) ;
pthread_mutex_unlock(&mutexBusNum) ;
if (allEnded)
break ;
pthread_cond_wait(&allready.cond, &allready.mutex) ;
}
allready.pthreadNum = 0 ;
pthread_mutex_unlock(&allready.mutex) ;
//时间增加1
pthread_mutex_lock(&mutexTime) ;
g_curTime.AddTime(1) ;
pthread_mutex_unlock(&mutexTime) ;
//通知所有线程继续
if (g_curBusNum > 0)
{
pthread_mutex_lock(&oneready.mutex) ;
oneready.continueRun = 1 ;
pthread_mutex_unlock(&oneready.mutex) ;
pthread_cond_broadcast(&oneready.cond) ;
}
}
//客车线程
void* busrun(void* busArgv)
{
while (1) {
//做自己的事情
Vehicle* pBusArgv = (Vehicle*)busArgv ;
pthread_mutex_lock(&mutexTime) ;
g_curTime.Show(std::cout) ;
pthread_mutex_unlock(&mutexTime) ;
int retState = 0 ;
retState = pBusArgv->Running(g_curTime) ;
//若自己是最后一个完成的,则通知主控制线程
pthread_mutex_lock(&allready.mutex) ;
allready.pthreadNum-- ;
if (allready.pthreadNum == -g_curBusNum) {
if (retState == -1) //bus跑完全程,回收
{
pthread_mutex_lock(&mutexBusNum) ;
g_curBusNum-- ;
pthread_mutex_unlock(&mutexBusNum) ;
}
pthread_cond_signal(&allready.cond) ;
}
pthread_mutex_unlock(&allready.mutex) ;
//bus跑完全程,此线程结束
if (retState == -1)
break;
//等待可以继续运行的信号
pthread_mutex_lock(&oneready.mutex) ;
while(oneready.continueRun == 0)
{
pthread_cond_wait(&oneready.cond, &oneready.mutex) ;
}
oneready.continueRun = 0 ;
pthread_mutex_unlock(&oneready.mutex) ;
}
return NULL ;
}
startStation.Run(g_curTime) ;//根据当前时间判断是否到了发车时间,若到了发车时间,则生成一个客车线程。
至于乘客上下车,车站对客车的调度,实现不难,有兴趣的朋友可以自己用C++实现全部功能。