以一个简单的项目来学习面向对象编程(设计模式和多线程)

下面的项目是两年前学校老师布置的一个小项目,当时自己用了一种很笨拙的方式实现了,现在用面向对象的思想和多线程重构这个项目。

问题描述:

西宝高速仿真模拟
西安市到宝鸡市之间是我省主要的高速公路客运路线之一,经过简化后的客运路线端点、中途停靠点和里程如下图所示(括号里是简称,里程的单位是公里):
示意图

  • 限定条件
    (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++实现全部功能。

猜你喜欢

转载自blog.csdn.net/yang_yulei/article/details/44155487
今日推荐