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六种,其中外部触发四种,内部触发两种(完成读取与完成写入,线程自己最先知道)。下面我们来分析分析状态与触发器的关系:
- 假如线程处于搁置状态,那么能接收两种触发,分别是运行、关闭,其中遇到运行触发,状态会变成待命;如果遇到关闭,那么不作处理
- 假如线程处于待命状态,那么能接收三种触发,分别是读、写、关闭,如果遇到读那么切换状态为读取中,如果遇到写那么状态为写入中,如果遇到关闭那么切换状态为搁置
- ……
然后我们来试试用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