理解alivc_framework 状态机

背景:

对与现在组件状态现状以及转换过程有点疑惑,自己边熟悉代码边思考。c++标准库以及Boost库并没有提供这种关于状态机类似的基类模板,原因是因为外面类复杂多样,不同类的状态没有共性。但是聚焦到音视频模块,这些类有着类似的特性,因此我们可以简单的总结和基类化一些基类模板,供模块继承使用。

正文:

这里指的模块类,一般是比较大的功能模块的接口类或者代理类(常见的如decode,encode等)。一般下面有对应的impl实现类,功能附带缓冲类等。由于是模块类是一个模块整体对外的表现,因此接口,状态也比较多。因此从上层角度出发,统一规划所有模块类进行统一表现,是个比较好的实现。同时也经常会遇到特殊不符合统一的设计的情况。需要特殊特化处理。

1.个人理解模块状态分类:

a)构造态,(未初始化态uninit

模块类一般比较大,成员较多。个人习惯构造函数不进行业务处理,不传参数,不抛异常,仅仅对模块类内部成员进行引用,bind等互相关联。完成构造过程。

b)初始化态,(inited

一般是外面调用,如果仅仅是自己构造函数调用就比较鸡肋了(还不如写到构造函数里面)。可以接受外部传参数。根据外部参数返回Init成功或者失败,或者抛异常,这都可行的。

c) 运行态,(run/start

一般是外部调用,外面再初始化设置参数,或者初始化前后设置参数,开始运行。严格意义来说运行时期是不接受参数改变的(部分特殊支持运行态改参数也是可以的)。

d) 暂停态,(pause)

个人理解这也是非运行态,只不过保留了运行态的上下文而已。

以上就是我觉得一个组件必须有的4个必要的状态,或者说是用户必须需要关心的状态。说明一点的就是,这里是异步的情况,当然如果有些同步组件设计比较简单,比如是同步的接口,就是 init/doTask/uninit 就完成了工作,那肯定就没有运行态和暂停态了。但是考虑到模块都是负责比较复杂的业务,如果是纯同步的情况没必要设计成模块,或者说纯同步的模块实在不多我们暂不考虑。

2.模块切换函数

  1. constructor:模块肯定是由constructor构造了。这里值得说明一下的就是,如果该模块是Service类型的,也就是说全局单例,则构造函数不是由业务逻辑控制的。这种单例的情况,般默认认为已经构造好了。一般是通过静态函数getInstance来获取类即可。
  2. initpara:通过调用init函数,实现uninit => inited 状态切换,如果是service,应该提供初始化形参,service返回初始化上下文context.
  3. start():通过调用start,实现inited=>running 状态切换
  4. pause():通过调用pause,实现running=>pause状态切换。
  5. flush():此函数不切换状态,只是一个同步标识。调用后,组建所有异步操作都给出与同步操作相同的结果。
  6. clear():与flush类似,此函数不切合状态,是同步标识。调用后,组建直接清除所有异步缓存。
  7. resume(): pause=>running状态切换。
  8. stop():running=>inited 状态切换。注意如果是pause=>inited 情况,也是stop。值得一提的是,stop之后,不需要将组建缓存和条件变量恢复到初始状态。也就是说,内含flush() 或者 clear()操作。因为stop之后是inited状态,允许上下缓存堆积。
  9. uninitinited=>uninit状态。个人感觉调用习惯是不用兼容stop功能。也就是说如果组件是runningpause状态的话,可以不响应uninit函数。客户如果想无论什么情况直接析构,可以组合调用stop()和 uninit()
  10. destructor: 析构组件。为了防止内存泄露,一般会内置调用stop() uninit()

另外,这里需要对reset做一下说明,有时候客户希望的reset是恢复到running的初始状态,但是也有客户resetuninit 状态。我个人觉得uninit 可以完成第二种业务。

总结一下如下图所示:

理解alivc_framework 状态机

3.两种处理状态方法

处理状态无非就是看状态和消息是否匹配if(state can proess this msg)。有两种处理方法,第一种是为每个状态写一个处理函数,这个处理函数判断是否响应该消息:

int processInitstateMsg(const Msg& msg)
{
    switch (msg.type) {
        case start:
            start();
            state = start_state;
            break;
        case uninit:
            ...
        default:
            // 状态不对,不处理
            break;
    }
}

另一种方法,也可以给每个消息写一个响应处理函数(alivc_framework现在的实现)

int processMsg(const Msg& msg)
{
    switch (msg.type) {
            case start:
                return onStartMsg(msg);
            case ...
        default:
            // 未知消息
            Log.e("unknown msg type.");
    }
}

int onStartMsg(const Msg& startMsg)
{
    if(state ok)
    {
        start();
    }
    else
    {
        return state invalid.
    }
}

对应这两种设计,谁优谁劣很明显可以看出。由于组件对于自身状态类型是明确的,对于外部处理的消息类型是未知的。如果先按照状态进行筛选,则只需判断一次即可(是否这个消息符合我的状态),如果按照第二种实现,那就需要判断两次了。第一次是processMsg里面判断消息类型(switch msg type) 然后调用每个响应消息的处理函数,然后在每个函数里面判断状态(switch state),进而再进行消息处理。

当然第二种做法并不是一无是处。它可以提供一个stateBase进行抽象,处理公共的消息。

4.状态基类

由于状态很多,我们可以通过构造一个基类stateBase,来管理状态。stateBase暴露模块切换函数,内部有三个逻辑组成:1.检查状态,2.具体实现,3.切换状态。以stop为例:

StateBase::Stop()
{
	//1.check state
	if(state != pause && state != running)
	{
		return false;
	}

	// 2.call virtual func impl;
	StopImpl();
	
	// 3.make state
	state = inited;
	return true;
}

这样派生类就可以直接实现impl 而无需关心状态了。

不满足状态条件的组件如何处理?

实际应用中,有很多不满足使用基类条件的情况。比如类内部有多个线程,并且多个线程有相互依赖关系,并不是一个简单的StartStop能解决的; 又或者某个类是封装的第三方库的类,而且这个第三方库定义的状态机与我们之前假设的状态机不同(例如AndroidMediaCodec Flush状态是不能跳转到Running态,必须要先ClearInit态等等)。如果遇到这些比较棘手的情况,应该抛弃状态基类的方法,自己实现状态机,并且在对外接口中说明状态切换的时机以及特性。

总结

从类设计角度来讲,设计一个状态机,维护自身状态来保证调用和返回的结果是必要的。

由于每个类具体情况不同,需要每个组件自己实现维护自己的状态机。

由于音视频编排组件比较类似,可以提供统一的接口呈现状态转换。

编排组件可以抽象成一个基类统一维护,也可以自己处理,各有优劣。

上一篇:云数据库ClickHouse资源隔离-弹性资源队列


下一篇:个人音视频常用工具介绍