C# Finite State Machine Design Evolution for

概述

在开发证券交易系统两腿套利功能的时候,对于每一条两腿套利监控,都需要有一个状态变迁维护。最初的实现没有使用状态机,但是定义并支持了停止、正在监控、正在执行、已执行四种状态。后来面临设计更改,需要增加一种“暂停”状态。为了简化代码,提高可读性,实现了FSM状态机。

因为以前在通信底层嵌入式系统中对于TCP/IP的协议栈的状态机处理有过经验,便自行动手实现了状态机。实现之后,调研了网上关于C#实现状态机的方式,发现有很多新的技术可以用于简化并优化状态机设计。尤其是了解到,状态机和“行为树Behavior Tree (BT)”的发展和关系。之前未接触过行为树,此篇也着重在状态机,希望下一次可以找时间探讨行为树的应用。

Introduction of arbitrage trading

An arbitrage trading has 2 legs, which acn be executed simultaneously, or one after another (successively).
每个套利有两个参考品种,当价差达到预定条件时自动触发执行。
when created, it is in Stopped state, user can start Monitoring, or stop executing.
<pre>
public enum ArbitrageStatus
{
Monitoring,
Executing,
Executed,
Stopped,
Paused
}
</pre>

第一种实现方式

<pre>
#region FSM 状态机

    private bool FsmSetStatus(Arbitrage arbitrage, ArbitrageStatus to)
    {
        switch (arbitrage.ArbitrageStatus)
        {
            case ArbitrageStatus.暂停:
                switch (to)
                {
                    // 暂停状态->正在监控时,直接尝试转入正在执行状态
                    // 从暂停恢复,但是不允许从暂停状态立即执行
                    case ArbitrageStatus.正在监控:
                        return FsmFrom暂停to正在监控(arbitrage);
                    case ArbitrageStatus.停止:
                        return FsmFrom暂停to停止(arbitrage);
                    case ArbitrageStatus.已执行:
                        return FsmFrom暂停to已执行(arbitrage);
                }
                break;
            case ArbitrageStatus.停止:
                switch (to)
                {
                    //  停止状态只允许转变到正在监控状态
                    case ArbitrageStatus.正在监控:
                        return FsmFrom停止to正在监控(arbitrage);
                }
                break;
            case ArbitrageStatus.已执行:
                switch (to)
                {
                    case ArbitrageStatus.停止:
                        return FsmFrom已执行to停止(arbitrage);
                }
                break;
            case ArbitrageStatus.正在执行:
                switch (to)
                {
                    case ArbitrageStatus.正在监控:
                    case ArbitrageStatus.已执行:
                        return FsmFrom正在执行to正在监控已执行(arbitrage);
                    case ArbitrageStatus.暂停:
                        return FsmFrom正在执行to暂停(arbitrage);
                    case ArbitrageStatus.停止:
                        return FsmFrom正在执行to停止(arbitrage);
                }
                break;
            case ArbitrageStatus.正在监控:
                switch (to)
                {
                    case ArbitrageStatus.暂停:
                        return FsmFrom正在监控to暂停(arbitrage);
                    case ArbitrageStatus.停止:
                        return FsmFrom正在监控to停止(arbitrage);
                    case ArbitrageStatus.正在执行:
                        return FsmFrom正在监控to正在执行(arbitrage);
                    case ArbitrageStatus.正在监控:
                        return true;
                }
                break;
        }
        return false;
    }


    /// <summary>
    /// 从暂停恢复,但是不允许从暂停再到立即执行。
    /// </summary>
    /// <param name="arbitrage"></param>
    /// <returns></returns>
    private bool FsmFrom暂停to正在监控(Arbitrage arbitrage)
    {
        if (UnfinishedEntrustCounter(arbitrage) == 0)
        {
            arbitrage.ArbitrageStatus = ArbitrageStatus.正在监控;
            return true;
        }

        arbitrage.ArbitrageStatus = ArbitrageStatus.正在执行;
        return true;
    }
    private bool FsmFrom暂停to停止(Arbitrage arbitrage)
    {
        // 执行次数加1,清除委托统计信息。
        arbitrage.ExecutionTimesDone++;
        ClearEntrustCounter(arbitrage);
        arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
        return true;
    }

    private bool FsmFrom暂停to已执行(Arbitrage arbitrage)
    {
        // 执行次数加1,清除委托统计信息。
        arbitrage.ExecutionTimesDone++;
        ClearEntrustCounter(arbitrage);
        if(arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
            arbitrage.ArbitrageStatus = ArbitrageStatus.已执行;
        return true;
    }

    private bool FsmFrom正在监控to暂停(Arbitrage arbitrage)
    {
        arbitrage.ArbitrageStatus = ArbitrageStatus.暂停;
        return true;
    }

    private bool FsmFrom正在监控to停止(Arbitrage arbitrage)
    {
        arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
        return true;
    }

    private bool FsmFrom正在监控to正在执行(Arbitrage arbitrage)
    {
        if (!arbitrage.IgnoreCheckOnce)
        {
            if (!TsabUtility.CheckPriceContition(arbitrage))
                return false;
        }
        else
        {
            arbitrage.IgnoreCheckOnce = false;
        }

        List<List<Entrust>> legEntrusts;
        if (NextArbitrageBaskets(arbitrage, out legEntrusts))
        {
            arbitrage.ArbitrageStatus = ArbitrageStatus.正在执行;
            // issue 
            LegEntrustsHandler(arbitrage, legEntrusts);
        }
        return true;
    }

    /// <summary>
    /// 从正在执行状态的目标状态是“已执行”,“正在监控”是中间状态。
    /// </summary>
    /// <param name="arbitrage"></param>
    /// <returns></returns>
    private bool FsmFrom正在执行to正在监控已执行(Arbitrage arbitrage)
    {
        if (!FailedEntrustCheck(arbitrage))
        {
            ManualStopArbitrage(arbitrage, true);
            _lastError = "委托废单超出监控限定值,自动停止两腿套利!";
            if (AlarmHandler != null)
            {
                AlarmHandler(arbitrage);
            }
            ClearEntrustCounter(arbitrage);
            arbitrage.NeedWaitNextLeg = false;
            arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
            return true;
        }

        if (UnfinishedEntrustCounter(arbitrage) == 0)
        {
            ClearEntrustCounter(arbitrage);
            if (!arbitrage.NeedWaitNextLeg)
            {
                arbitrage.ExecutionTimesDone++;
                if (arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
                {
                    ClearEntrustCounter(arbitrage);
                    arbitrage.ArbitrageStatus = ArbitrageStatus.已执行;
                    return true;
                }
            }
            if (arbitrage.NeedWaitNextLeg || TsabUtility.CheckPriceContition(arbitrage))
            {
                // 发送下一腿或下一轮
                List<List<Entrust>> legEntrusts;
                if (NextArbitrageBaskets(arbitrage, out legEntrusts))
                {
                    // issue 
                    LegEntrustsHandler(arbitrage, legEntrusts);
                }
            }
            else
            {
                arbitrage.ArbitrageStatus = ArbitrageStatus.正在监控;
            }
        }

        return true;
    }

    private bool FsmFrom正在执行to暂停(Arbitrage arbitrage)
    {
        arbitrage.ArbitrageStatus = ArbitrageStatus.暂停;
        return true;
    }

    private bool FsmFrom正在执行to停止(Arbitrage arbitrage)
    {
        // 执行次数加1,清除委托统计信息。
        arbitrage.ExecutionTimesDone++;
        ClearEntrustCounter(arbitrage);
        arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
        return true;
    }

    private bool FsmFrom已执行to停止(Arbitrage arbitrage)
    {
        arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
        return true;
    }

    private bool FsmFrom停止to正在监控(Arbitrage arbitrage)
    {
        if (arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
        {
            _lastError = "已经超过计划执行次数!" + arbitrage;
            return false;
        }
        arbitrage.ArbitrageStatus = ArbitrageStatus.正在监控;
        return true;
    }


    #endregion

</pre>

这种方式没有把Command或Event的概念直接独立出来,调用接口使用过选择下一个状态来触发状态变迁,也就是说没有将“状态变化”封装在状态机内部。

调用方式:
<pre>
public bool ManualStartArbitrage(Arbitrage arbitrage, bool forceIssueNow)
{
var exists = ArbitrageList.Where(p => p.Id == arbitrage.Id);
if (!exists.Any())
{
_lastError = "启动两腿套利监控失败: 不存在的Id:" + arbitrage.Id;
return false;
}

        if (arbitrage.ExecutionTimesPlanned <= arbitrage.ExecutionTimesDone)
        {
            _lastError = "两腿监控策略已经达到最大执行次数!";
            return false;
        }

        if (!FsmSetStatus(arbitrage, ArbitrageStatus.正在监控))
        {
            _lastError = GetLastError();
            return false;
        }

        // 如果满足条件,应立即执行
        if (forceIssueNow || TsabUtility.CheckPriceContition(arbitrage))
        {
            arbitrage.IgnoreCheckOnce = true;
            var ret = FsmSetStatus(arbitrage, ArbitrageStatus.正在执行);
            arbitrage.IgnoreCheckOnce = false;
            return ret;
        }

        return true;
    }

</pre>

选择的参考实现方式

第一种参考实现方式

第一种参考实现方式来自于Stackoverflow,明确定义Command,通过状态变迁字典存储由Command触发状态变化的动作。
Let's start with this simple state diagram:

![simple state machine diagram][1]

We have:

  • 4 states (Inactive, Active, Paused, and Exited)
  • 5 types of state transitions (Begin Command, End Command, Pause Command, Resume Command, Exit Command).

You can convert this to C# in a handful of ways, such as performing a switch statement on the current state and command, or looking up transitions in a transition table. For this simple state machine, I prefer a transition table, which is very easy to represent using a Dictionary:

using System;
using System.Collections.Generic;

namespace Juliet
{
    public enum ProcessState
    {
        Inactive,
        Active,
        Paused,
        Terminated
    }

    public enum Command
    {
        Begin,
        End,
        Pause,
        Resume,
        Exit
    }

    public class Process
    {
        class StateTransition
        {
            readonly ProcessState CurrentState;
            readonly Command Command;

            public StateTransition(ProcessState currentState, Command command)
            {
                CurrentState = currentState;
                Command = command;
            }

            public override int GetHashCode()
            {
                return 17 + 31 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
            }

            public override bool Equals(object obj)
            {
                StateTransition other = obj as StateTransition;
                return other != null && this.CurrentState == other.CurrentState && this.Command == other.Command;
            }
        }

        Dictionary<StateTransition, ProcessState> transitions;
        public ProcessState CurrentState { get; private set; }

        public Process()
        {
            CurrentState = ProcessState.Inactive;
            transitions = new Dictionary<StateTransition, ProcessState>
            {
                { new StateTransition(ProcessState.Inactive, Command.Exit), ProcessState.Terminated },
                { new StateTransition(ProcessState.Inactive, Command.Begin), ProcessState.Active },
                { new StateTransition(ProcessState.Active, Command.End), ProcessState.Inactive },
                { new StateTransition(ProcessState.Active, Command.Pause), ProcessState.Paused },
                { new StateTransition(ProcessState.Paused, Command.End), ProcessState.Inactive },
                { new StateTransition(ProcessState.Paused, Command.Resume), ProcessState.Active }
            };
        }

        public ProcessState GetNext(Command command)
        {
            StateTransition transition = new StateTransition(CurrentState, command);
            ProcessState nextState;
            if (!transitions.TryGetValue(transition, out nextState))
                throw new Exception("Invalid transition: " + CurrentState + " -> " + command);
            return nextState;
        }

        public ProcessState MoveNext(Command command)
        {
            CurrentState = GetNext(command);
            return CurrentState;
        }
    }
    

    public class Program
    {
        static void Main(string[] args)
        {
            Process p = new Process();
            Console.WriteLine("Current State = " + p.CurrentState);
            Console.WriteLine("Command.Begin: Current State = " + p.MoveNext(Command.Begin));
            Console.WriteLine("Command.Pause: Current State = " + p.MoveNext(Command.Pause));
            Console.WriteLine("Command.End: Current State = " + p.MoveNext(Command.End));
            Console.WriteLine("Command.Exit: Current State = " + p.MoveNext(Command.Exit));
            Console.ReadLine();
        }
    }
}

As a matter of personal preference, I like to design my state machines with a GetNext function to return the next state <a href="http://en.wikipedia.org/wiki/Pure_function">deterministically</a>, and a MoveNext function to mutate the state machine.

第二种参考实现方式

使用C#语言的标签机制,实现了通用的状态机代码。
Sidneys1/GFSM

A Generic Finite State Machine in C#
Implementation is easy:

<pre>

// Entirely unnecessary, just here to implement a common DoWork()
public abstract class MyStateBase : IState {
public virtual void Enter() {}
public abstract void DoWork();
public virtual void Leave() {}
}

// Define a state and it's transitions
[Transition("next", typeof(EndState))]
public class StartState : MyStateBase {
public override void DoWork() => Console.WriteLine("In StartState");
public override void Leave() => Console.WriteLine("\tLeaving StartState");
}

[Transition("next", null)]
public class EndState : MyStateBase {
public override void Enter() => Console.WriteLine("\tEntered EndState");
public override void DoWork() => Console.WriteLine("In EndState");
public override void Leave() => Console.WriteLine("\tLeaving EndState");
}

internal class Program {
private static void Main() {
var fsm = new FiniteStateMachine<StartState>();

fsm.Transitioning += transition => Console.WriteLine($"Beginning transition: {transition}");
fsm.Transitioned += transition => {
  Console.WriteLine($"Done transitioning: {transition}");
  if (transition.To == null)
    Console.WriteLine("\nExited");
};

Console.WriteLine("Started\n");
fsm.GetCurrentState<MyStateBase>().DoWork();
fsm.DoTransition("next");
fsm.GetCurrentState<MyStateBase>().DoWork();
fsm.DoTransition("next");

Console.ReadLine();

}
}
/*
Will print:
Started

In StartState
Beginning transition: DemoApp.StartState + 'next' = DemoApp.EndState
Leaving StartState
Entered EndState
Done transitioning: DemoApp.StartState + 'next' = DemoApp.EndState
In EndState
Beginning transition: DemoApp.EndState + 'next' = null
Leaving EndState
Done transitioning: DemoApp.EndState + 'next' = null

Exited
*/
</pre>

第三种参考实现方式

eteeselink/YieldMachine

Some shameless self-promo here, but a while ago I created a library called YieldMachine which allows a limited-complexity state machine to be described in a very clean and simple way. For example, consider a lamp:

state machine of a lamp
state machine of a lamp

Notice that this state machine has 2 triggers and 3 states. In YieldMachine code, we write a single method for all state-related behavior, in which we commit the horrible atrocity of using goto for each state. A trigger becomes a property or field of type Action, decorated with an attribute called Trigger. I've commented the code of the first state and its transitions below; the next states follow the same pattern.

public class Lamp : StateMachine
{
    // Triggers (or events, or actions, whatever) that our
    // state machine understands.
    [Trigger]
    public readonly Action PressSwitch;

    [Trigger]
    public readonly Action GotError;

    // Actual state machine logic
    protected override IEnumerable WalkStates()
    {
    off:                                       
        Console.WriteLine("off.");
        yield return null;

        if (Trigger == PressSwitch) goto on;
        InvalidTrigger();

    on:
        Console.WriteLine("*shiiine!*");
        yield return null;

        if (Trigger == GotError) goto error;
        if (Trigger == PressSwitch) goto off;
        InvalidTrigger();

    error:
        Console.WriteLine("-err-");
        yield return null;

        if (Trigger == PressSwitch) goto off;
        InvalidTrigger();
    }
}

Short and nice, eh!

This state machine is controlled simply by sending triggers to it:

var sm = new Lamp();
sm.PressSwitch(); //go on
sm.PressSwitch(); //go off

sm.PressSwitch(); //go on
sm.GotError();    //get error
sm.PressSwitch(); //go off

Just to clarify, I've added some comments to the first state to help you understand how to use this.

    protected override IEnumerable WalkStates()
    {
    off:                                       // Each goto label is a state

        Console.WriteLine("off.");             // State entry actions

        yield return null;                     // This means "Wait until a 
                                               // trigger is called"

                                               // Ah, we got triggered! 
                                               //   perform state exit actions 
                                               //   (none, in this case)

        if (Trigger == PressSwitch) goto on;   // Transitions go here: 
                                               // depending on the trigger 
                                               // that was called, go to
                                               // the right state

        InvalidTrigger();                      // Throw exception on 
                                               // invalid trigger

        ...

This works because the C# compiler actually created a state machine internally for each method that uses yield return. This construct is usually used to lazily create sequences of data, but in this case we're not actually interested in the returned sequence (which is all nulls anyway), but in the state behaviour that gets created under the hood.

The StateMachine base class does some reflection on construction to assign code to each [Trigger] action, which sets the Trigger member and moves the state machine forward.

But you don't really need to understand the internals to be able to use it.

第四种参考实现方式

OmerMor/StateMachineToolkit
A generic state-machine framework, with support for active/passive machines, exposed events and rich exception handling.

通过注册EntryHandlerExitHandler定义状态Entry/Exit Methods
,通过AddTransition提供状态变迁和Action Methods。

第四种参考实现方式

Real-Serious-Games/Fluent-State-Machine

支持子状态嵌套。
不需要预先定义状态枚举值,通过字符串定义,非常灵活。

设计考量

不管是否利用了C#的语言特性,最重要的应该是从不同的实现方式中找到共性。

状态机的功能(不考虑语言特性,如Attribute等方式):

  • 多种状态之间变迁:定义两两可转换状态的映射(字典、二维数组等)
  • 是否支持active/passive machines(参考方式四)
  • 通过Command/Event触发状态变迁:将所有状态转换逻辑都封装在内部,通过Command触发变化。
  • hierarchical state machine?是否支持状态内部子状态。例如两腿套利时,两腿分开发送,需要等到第一腿的委托执行完毕之后再发送下一腿,此时状态仍然处于“正在执行”状态,仅将“等待下一腿”标识清空。

Eric LippertSO的一个问题中回答说:

If you want to write a state machine, just write a state machine. It's not hard. If you want to write a lot of state machines, write a library of useful helper methods that let you cleanly represent state machines, and then use your library. But don't abuse a language construct intended for something completely different that just happens to use state machines as an implementation detail. That makes your state machine code hard to read, understand, debug, maintain and extend.
(And incidentally, I did a double-take when reading your name. One of the designers of C# is also named Matt Warren!)
需要状态机的时候,直接去写。如果经常需要写状态机,可以自己建立一个库。千万不要太拘泥或追求形式,因为状态机并不复杂。

在我的第一种实现中,并没有考虑Command/Event,直接通过提供“设置下一状态”来实现状态变迁。这里是存在一些问题的:例如有些状态调用时有多种可能的下一种目标状态,这必须由外部Caller来明确进行多次调用SetStatus函数。最好的方式还是应该简单通过Command来改变状态。加入Command的设计其实就是封装了这种调用逻辑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容