状态机是什么?有什么用?


Warning: WP_Syntax::substituteToken(): Argument #1 ($match) must be passed by reference, value given in /www/wwwroot/fawdlstty.com/wp-content/plugins/wp-syntax/wp-syntax.php on line 383

Warning: WP_Syntax::substituteToken(): Argument #1 ($match) must be passed by reference, value given in /www/wwwroot/fawdlstty.com/wp-content/plugins/wp-syntax/wp-syntax.php on line 383

Warning: WP_Syntax::substituteToken(): Argument #1 ($match) must be passed by reference, value given in /www/wwwroot/fawdlstty.com/wp-content/plugins/wp-syntax/wp-syntax.php on line 383

Warning: WP_Syntax::substituteToken(): Argument #1 ($match) must be passed by reference, value given in /www/wwwroot/fawdlstty.com/wp-content/plugins/wp-syntax/wp-syntax.php on line 383

Warning: WP_Syntax::substituteToken(): Argument #1 ($match) must be passed by reference, value given in /www/wwwroot/fawdlstty.com/wp-content/plugins/wp-syntax/wp-syntax.php on line 383

Warning: WP_Syntax::substituteToken(): Argument #1 ($match) must be passed by reference, value given in /www/wwwroot/fawdlstty.com/wp-content/plugins/wp-syntax/wp-syntax.php on line 383

推荐一个状态机库,支持C++与C#,通过两种语言分别实现。链接:https://github.com/fawdlstty/SMLite

为了大家都能看懂,下面的代码以C#做示例,C++可以在项目里找到具体示例代码及用法。

回到最初的标题问题,我们来假设一下,假如碰到了一个需求,需求是实现一个半双工的网络处理程序,所谓半双工也就是上传时不能下载,下载时不能上传,另外也不能两块数据同时上传或下载。

看起来很简单是吧,一个线程专门做上传或下载操作。但如何让外部知道网络线程的状态呢?一个合适的方案是,定义一个枚举状态,有四种枚举值,Rest、Ready、Reading、Writing,分别代表搁置状态、待命状态、正在读取、正在写入。然后就是,外部的事件,比如打开、关闭、写入等命令,如何传达到线程呢?这儿可以定义触发器枚举值,Run、Close、Read、FinishRead、Write、FinishWrite六种,其中外部触发四种,内部触发两种(完成读取与完成写入,线程自己最先知道)。下面我们来分析分析状态与触发器的关系:

  1. 假如线程处于搁置状态,那么能接收两种触发,分别是运行、关闭,其中遇到运行触发,状态会变成待命;如果遇到关闭,那么不作处理
  2. 假如线程处于待命状态,那么能接收三种触发,分别是读、写、关闭,如果遇到读那么切换状态为读取中,如果遇到写那么状态为写入中,如果遇到关闭那么切换状态为搁置
  3. ……

然后我们来试试用C#梳理一下触发器与线程状态间的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
enum MyState { Rest, Ready, Reading, Writing };
enum MyTrigger { Run, Close, Read, FinishRead, Write, FinishWrite };
 
// ...
 
if (_state == MyState.Rest) {
    if (_trigger == MyTrigger.Run) {
        _state = MyState.Ready;
    } else if (_trigger == MyTrigger.Close) {
        //
    } else {
        throw new Exception ();
    }
} else if (_state == MyState.Ready) {
    if (_trigger == MyTrigger.Read) {
        _state = MyState.Reading;
    } else if (_trigger == MyTrigger.Write) {
        _state = MyState.Writing;
    } else if (_trigger == MyTrigger.Close) {
        _state = MyState.Rest;
    } else {
        throw new Exception ();
    }
} else if (_state == MyState.Reading) {
    if (_trigger == MyTrigger.FinishRead) {
        _state = MyState.Ready;
    } else if (_trigger == MyTrigger.Close) {
        _state = MyState.Rest;
    } else {
        throw new Exception ();
    }
} else if (_state == MyState.Writing) {
    if (_trigger == MyTrigger.FinishWrite) {
        _state = MyState.Ready;
    } else if (_trigger == MyTrigger.Close) {
        _state = MyState.Rest;
    } else {
        throw new Exception ();
    }
}

仅仅触发器与状态间的关系写一起,就有点乱了,对于状态更多、更复杂的需求,或者再在上面的代码里填上逻辑,具体做了哪些操作,这种混乱程度,几乎就能必现bug了。

好了,我们现在来了解一下通过状态机的思路来解决这个问题。首先,当然是下载并安装NuGet包“Fawdlstty.SMLite”,然后开始填代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var _smb = new SMLiteBuilder<MyState, MyTrigger> ();
_smb.Configure (MyState.Rest)
    .WhenChangeTo (MyTrigger.Run, MyState.Ready)
    .WhenIgnore (MyTrigger.Close);
_smb.Configure (MyState.Ready)
    .WhenChangeTo (MyTrigger.Read, MyState.Reading)
    .WhenChangeTo (MyTrigger.Write, MyState.Writing)
    .WhenChangeTo (MyTrigger.Close, MyState.Rest);
_smb.Configure (MyState.Reading)
    .WhenChangeTo (MyTrigger.FinishRead, MyState.Ready)
    .WhenChangeTo (MyTrigger.Close, MyState.Rest);
_smb.Configure (MyState.Writing)
    .WhenChangeTo (MyTrigger.FinishWrite, MyState.Ready)
    .WhenChangeTo (MyTrigger.Close, MyState.Rest);

不会状态机的话,可能这段代码看起来有点晕。这段代码含义是指定当状态机处在某一个状态时,可以触发什么事件,以及触发后做什么操作。比如上面代码中,如果状态机处在Rest状态时,可以触发Run与Close,等等。

看起来是不是要清爽多了?一下就少了三分之二的代码。后续我们只需要对这个状态机执行触发操作、获取状态等等。这儿我们详细讲讲触发器配置。

SMLite允许对状态配置6种同步处理方式以及4种异步处理方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 此处列出所有同步回调使用方式
_smb.Configure (MyState.Rest)
 
    // 如果状态由其他状态变成 MyState.Rest 状态,那么触发此方法,初始化状态机时指定的初始值不触发此方法
    .OnEntry (() => Console.WriteLine ("entry Rest"))
 
    // 如果状态由 MyState.Rest 状态变成其他状态,那么触发此方法
    .OnLeave (() => Console.WriteLine ("leave Rest"))
 
    // 如果触发 MyTrigger.Run,则将状态改为 MyState.Ready
    .WhenChangeTo (MyTrigger.Run, MyState.Ready)
 
    // 如果触发 MyTrigger.Run,忽略
    .WhenIgnore (MyTrigger.Close)
 
    // 如果触发 MyTrigger.Read,则调用回调函数,并将状态调整为返回值
    .WhenFunc (MyTrigger.Read, (MyState _state, MyTrigger _trigger) => {
        Console.WriteLine ("call WhenFunc callback");
        return MyState.Ready;
    })
 
    // 如果触发 MyTrigger.FinishRead,则调用回调函数,并将状态调整为返回值
    // 需注意,触发时候需传入参数,数量与类型必须完全匹配,否则抛异常
    .WhenFunc (MyTrigger.FinishRead, (MyState _state, MyTrigger _trigger, string _param) => {
        Console.WriteLine ($"call WhenFunc callback with param [{_param}]");
        return MyState.Ready;
    })
 
    // 如果触发 MyTrigger.Read,则调用回调函数(触发此方法回调不调整返回值)
    .WhenAction (MyTrigger.Read, (MyState _state, MyTrigger _trigger) => {
        Console.WriteLine ("call WhenAction callback");
    })
 
    // 如果触发 MyTrigger.FinishRead,则调用回调函数(触发此方法回调不调整返回值)
    // 需注意,触发时候需传入参数,数量与类型必须完全匹配,否则抛异常
    .WhenAction (MyTrigger.FinishRead, (MyState _state, MyTrigger _trigger, string _param) => {
        Console.WriteLine ($"call WhenAction callback with param [{_param}]");
    });

然后是异步处理方式,暂时只支持C#版本的库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 此处列出所有异步回调使用方式
_smb.Configure (MyState.Ready)
 
    // 与 OnEntry 效果一致,不过这函数指定异步方法,并且不能与 OnEntry 同时调用
    .OnEntryAsync (async () => {
        await Task.Yield ();
        Console.WriteLine ("entry Ready");
    })
 
    // 与 OnLeave 效果一致,不过这函数指定异步方法,并且不能与 OnLeave 同时调用
    .OnLeaveAsync (async () => {
        await Task.Yield ();
        Console.WriteLine ("leave Ready");
    })
 
    // 效果与 WhenFunc 一致,不过这函数指定异步方法
    .WhenFuncAsync (MyTrigger.Read, async (MyState _state, MyTrigger _trigger, CancellationToken _token) => {
        await Task.Yield ();
        Console.WriteLine ("call WhenFunc callback");
        return MyState.Ready;
    })
 
    // 效果与 WhenFunc 一致,不过这函数指定异步方法
    .WhenFuncAsync (MyTrigger.FinishRead, async (MyState _state, MyTrigger _trigger, CancellationToken _token, string _param) => {
        await Task.Yield ();
        Console.WriteLine ($"call WhenFunc callback with param [{_param}]");
        return MyState.Ready;
    })
 
    // 效果与 WhenAction 一致,不过这函数指定异步方法
    .WhenActionAsync (MyTrigger.Read, async (MyState _state, MyTrigger _trigger, CancellationToken _token) => {
        await Task.Yield ();
        Console.WriteLine ("call WhenAction callback");
    })
 
    // 效果与 WhenAction 一致,不过这函数指定异步方法
    .WhenActionAsync (MyTrigger.FinishRead, async (MyState _state, MyTrigger _trigger, CancellationToken _token, string _param) => {
        await Task.Yield ();
        Console.WriteLine ($"call WhenAction callback with param [{_param}]");
    });

下面开始真正使用到状态机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 生成状态机
var _sm = _smb.Build (MyState.Rest);
 
// 获取当前状态
assert (_sm.State == MyState.Rest);
 
// 触发事件,此事件将使得状态机的状态发生改变
_sm.Triggering (MyTrigger.Run);
assert (_sm.State == MyState.Ready);
 
_sm.Triggering (MyTrigger.Read);
assert (_sm.State == MyState.Reading);
 
_sm.Triggering (MyTrigger.FinishRead);
assert (_sm.State == MyState.Ready);
 
_sm.Triggering (MyTrigger.Write);
assert (_sm.State == MyState.Writing);
 
_sm.Triggering (MyTrigger.FinishWrite);
assert (_sm.State == MyState.Ready);
 
_sm.Triggering (MyTrigger.Close);
assert (_sm.State == MyState.Rest);

一个完整的状态机逻辑就完成了,不过上面有写到了带参数的触发函数定义与异步触发函数定义,下面我们看看这两种如何实现:

1
2
3
4
5
6
7
8
//仅作为方法讲解,不作为上面问题的答案
 
// 异步触发一个事件,并传入指定参数
await _sm.TriggeringAsync (MyTrigger.Run, "hello");
 
// 限定异步任务最长执行时间,超时取消
var _source = new CancellationTokenSource (TimeSpan.FromSeconds (10));
await _sm.TriggeringAsync (MyTrigger.Run, _source.Token, "hello");

很简单吧,方法带Async就是异步,参数传递直接写在后面。SMLite提供的状态机就是这么简单。不仅提供基本状态机的接口,还能实现参数传递,状态进入退出事件,另外对C#单独提供异步方法。

详细教程:https://github.com/fawdlstty/SMLite/blob/main/README.zh.md

发布者

fawdlstty

又一只萌萌哒程序猿~~

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注