C# Finite State Machine Design Evolution for

概述

在開發(fā)證券交易系統(tǒng)兩腿套利功能的時候,對于每一條兩腿套利監(jiān)控蝗锥,都需要有一個狀態(tài)變遷維護(hù)妓灌。最初的實現(xiàn)沒有使用狀態(tài)機(jī)轨蛤,但是定義并支持了停止、正在監(jiān)控虫埂、正在執(zhí)行祥山、已執(zhí)行四種狀態(tài)。后來面臨設(shè)計更改掉伏,需要增加一種“暫头炫唬”狀態(tài)。為了簡化代碼斧散,提高可讀性岳颇,實現(xiàn)了FSM狀態(tài)機(jī)。

因為以前在通信底層嵌入式系統(tǒng)中對于TCP/IP的協(xié)議棧的狀態(tài)機(jī)處理有過經(jīng)驗颅湘,便自行動手實現(xiàn)了狀態(tài)機(jī)话侧。實現(xiàn)之后,調(diào)研了網(wǎng)上關(guān)于C#實現(xiàn)狀態(tài)機(jī)的方式闯参,發(fā)現(xiàn)有很多新的技術(shù)可以用于簡化并優(yōu)化狀態(tài)機(jī)設(shè)計瞻鹏。尤其是了解到,狀態(tài)機(jī)和“行為樹Behavior Tree (BT)”的發(fā)展和關(guān)系鹿寨。之前未接觸過行為樹新博,此篇也著重在狀態(tài)機(jī),希望下一次可以找時間探討行為樹的應(yīng)用脚草。

Introduction of arbitrage trading

An arbitrage trading has 2 legs, which acn be executed simultaneously, or one after another (successively).
每個套利有兩個參考品種赫悄,當(dāng)價差達(dá)到預(yù)定條件時自動觸發(fā)執(zhí)行。
when created, it is in Stopped state, user can start Monitoring, or stop executing.
<pre>
public enum ArbitrageStatus
{
Monitoring,
Executing,
Executed,
Stopped,
Paused
}
</pre>

第一種實現(xiàn)方式

<pre>
#region FSM 狀態(tài)機(jī)

    private bool FsmSetStatus(Arbitrage arbitrage, ArbitrageStatus to)
    {
        switch (arbitrage.ArbitrageStatus)
        {
            case ArbitrageStatus.暫停:
                switch (to)
                {
                    // 暫停狀態(tài)->正在監(jiān)控時馏慨,直接嘗試轉(zhuǎn)入正在執(zhí)行狀態(tài)
                    // 從暫凸』矗恢復(fù),但是不允許從暫停狀態(tài)立即執(zhí)行
                    case ArbitrageStatus.正在監(jiān)控:
                        return FsmFrom暫停to正在監(jiān)控(arbitrage);
                    case ArbitrageStatus.停止:
                        return FsmFrom暫停to停止(arbitrage);
                    case ArbitrageStatus.已執(zhí)行:
                        return FsmFrom暫停to已執(zhí)行(arbitrage);
                }
                break;
            case ArbitrageStatus.停止:
                switch (to)
                {
                    //  停止?fàn)顟B(tài)只允許轉(zhuǎn)變到正在監(jiān)控狀態(tài)
                    case ArbitrageStatus.正在監(jiān)控:
                        return FsmFrom停止to正在監(jiān)控(arbitrage);
                }
                break;
            case ArbitrageStatus.已執(zhí)行:
                switch (to)
                {
                    case ArbitrageStatus.停止:
                        return FsmFrom已執(zhí)行to停止(arbitrage);
                }
                break;
            case ArbitrageStatus.正在執(zhí)行:
                switch (to)
                {
                    case ArbitrageStatus.正在監(jiān)控:
                    case ArbitrageStatus.已執(zhí)行:
                        return FsmFrom正在執(zhí)行to正在監(jiān)控已執(zhí)行(arbitrage);
                    case ArbitrageStatus.暫停:
                        return FsmFrom正在執(zhí)行to暫停(arbitrage);
                    case ArbitrageStatus.停止:
                        return FsmFrom正在執(zhí)行to停止(arbitrage);
                }
                break;
            case ArbitrageStatus.正在監(jiān)控:
                switch (to)
                {
                    case ArbitrageStatus.暫停:
                        return FsmFrom正在監(jiān)控to暫停(arbitrage);
                    case ArbitrageStatus.停止:
                        return FsmFrom正在監(jiān)控to停止(arbitrage);
                    case ArbitrageStatus.正在執(zhí)行:
                        return FsmFrom正在監(jiān)控to正在執(zhí)行(arbitrage);
                    case ArbitrageStatus.正在監(jiān)控:
                        return true;
                }
                break;
        }
        return false;
    }


    /// <summary>
    /// 從暫托戳ィ恢復(fù)倔撞,但是不允許從暫停再到立即執(zhí)行。
    /// </summary>
    /// <param name="arbitrage"></param>
    /// <returns></returns>
    private bool FsmFrom暫停to正在監(jiān)控(Arbitrage arbitrage)
    {
        if (UnfinishedEntrustCounter(arbitrage) == 0)
        {
            arbitrage.ArbitrageStatus = ArbitrageStatus.正在監(jiān)控;
            return true;
        }

        arbitrage.ArbitrageStatus = ArbitrageStatus.正在執(zhí)行;
        return true;
    }
    private bool FsmFrom暫停to停止(Arbitrage arbitrage)
    {
        // 執(zhí)行次數(shù)加1慕趴,清除委托統(tǒng)計信息痪蝇。
        arbitrage.ExecutionTimesDone++;
        ClearEntrustCounter(arbitrage);
        arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
        return true;
    }

    private bool FsmFrom暫停to已執(zhí)行(Arbitrage arbitrage)
    {
        // 執(zhí)行次數(shù)加1鄙陡,清除委托統(tǒng)計信息。
        arbitrage.ExecutionTimesDone++;
        ClearEntrustCounter(arbitrage);
        if(arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
            arbitrage.ArbitrageStatus = ArbitrageStatus.已執(zhí)行;
        return true;
    }

    private bool FsmFrom正在監(jiān)控to暫停(Arbitrage arbitrage)
    {
        arbitrage.ArbitrageStatus = ArbitrageStatus.暫停;
        return true;
    }

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

    private bool FsmFrom正在監(jiān)控to正在執(zhí)行(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.正在執(zhí)行;
            // issue 
            LegEntrustsHandler(arbitrage, legEntrusts);
        }
        return true;
    }

    /// <summary>
    /// 從正在執(zhí)行狀態(tài)的目標(biāo)狀態(tài)是“已執(zhí)行”躏啰,“正在監(jiān)控”是中間狀態(tài)趁矾。
    /// </summary>
    /// <param name="arbitrage"></param>
    /// <returns></returns>
    private bool FsmFrom正在執(zhí)行to正在監(jiān)控已執(zhí)行(Arbitrage arbitrage)
    {
        if (!FailedEntrustCheck(arbitrage))
        {
            ManualStopArbitrage(arbitrage, true);
            _lastError = "委托廢單超出監(jiān)控限定值,自動停止兩腿套利给僵!";
            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.已執(zhí)行;
                    return true;
                }
            }
            if (arbitrage.NeedWaitNextLeg || TsabUtility.CheckPriceContition(arbitrage))
            {
                // 發(fā)送下一腿或下一輪
                List<List<Entrust>> legEntrusts;
                if (NextArbitrageBaskets(arbitrage, out legEntrusts))
                {
                    // issue 
                    LegEntrustsHandler(arbitrage, legEntrusts);
                }
            }
            else
            {
                arbitrage.ArbitrageStatus = ArbitrageStatus.正在監(jiān)控;
            }
        }

        return true;
    }

    private bool FsmFrom正在執(zhí)行to暫停(Arbitrage arbitrage)
    {
        arbitrage.ArbitrageStatus = ArbitrageStatus.暫停;
        return true;
    }

    private bool FsmFrom正在執(zhí)行to停止(Arbitrage arbitrage)
    {
        // 執(zhí)行次數(shù)加1毫捣,清除委托統(tǒng)計信息。
        arbitrage.ExecutionTimesDone++;
        ClearEntrustCounter(arbitrage);
        arbitrage.ArbitrageStatus = ArbitrageStatus.停止;
        return true;
    }

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

    private bool FsmFrom停止to正在監(jiān)控(Arbitrage arbitrage)
    {
        if (arbitrage.ExecutionTimesDone >= arbitrage.ExecutionTimesPlanned)
        {
            _lastError = "已經(jīng)超過計劃執(zhí)行次數(shù)想际!" + arbitrage;
            return false;
        }
        arbitrage.ArbitrageStatus = ArbitrageStatus.正在監(jiān)控;
        return true;
    }


    #endregion

</pre>

這種方式?jīng)]有把Command或Event的概念直接獨立出來培漏,調(diào)用接口使用過選擇下一個狀態(tài)來觸發(fā)狀態(tài)變遷溪厘,也就是說沒有將“狀態(tài)變化”封裝在狀態(tài)機(jī)內(nèi)部胡本。

調(diào)用方式:
<pre>
public bool ManualStartArbitrage(Arbitrage arbitrage, bool forceIssueNow)
{
var exists = ArbitrageList.Where(p => p.Id == arbitrage.Id);
if (!exists.Any())
{
_lastError = "啟動兩腿套利監(jiān)控失敗: 不存在的Id:" + arbitrage.Id;
return false;
}

        if (arbitrage.ExecutionTimesPlanned <= arbitrage.ExecutionTimesDone)
        {
            _lastError = "兩腿監(jiān)控策略已經(jīng)達(dá)到最大執(zhí)行次數(shù)!";
            return false;
        }

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

        // 如果滿足條件,應(yīng)立即執(zhí)行
        if (forceIssueNow || TsabUtility.CheckPriceContition(arbitrage))
        {
            arbitrage.IgnoreCheckOnce = true;
            var ret = FsmSetStatus(arbitrage, ArbitrageStatus.正在執(zhí)行);
            arbitrage.IgnoreCheckOnce = false;
            return ret;
        }

        return true;
    }

</pre>

選擇的參考實現(xiàn)方式

第一種參考實現(xiàn)方式

第一種參考實現(xiàn)方式來自于Stackoverflow畸悬,明確定義Command侧甫,通過狀態(tài)變遷字典存儲由Command觸發(fā)狀態(tài)變化的動作。
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 >deterministically</a>, and a MoveNext function to mutate the state machine.

第二種參考實現(xiàn)方式

使用C#語言的標(biāo)簽機(jī)制蹋宦,實現(xiàn)了通用的狀態(tài)機(jī)代碼披粟。
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>

第三種參考實現(xiàn)方式

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.

第四種參考實現(xiàn)方式

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

通過注冊EntryHandlerExitHandler定義狀態(tài)Entry/Exit Methods
,通過AddTransition提供狀態(tài)變遷和Action Methods冷冗。

第四種參考實現(xiàn)方式

Real-Serious-Games/Fluent-State-Machine

支持子狀態(tài)嵌套守屉。
不需要預(yù)先定義狀態(tài)枚舉值,通過字符串定義蒿辙,非常靈活拇泛。

設(shè)計考量

不管是否利用了C#的語言特性,最重要的應(yīng)該是從不同的實現(xiàn)方式中找到共性思灌。

狀態(tài)機(jī)的功能(不考慮語言特性俺叭,如Attribute等方式):

  • 多種狀態(tài)之間變遷:定義兩兩可轉(zhuǎn)換狀態(tài)的映射(字典、二維數(shù)組等)
  • 是否支持active/passive machines(參考方式四)
  • 通過Command/Event觸發(fā)狀態(tài)變遷:將所有狀態(tài)轉(zhuǎn)換邏輯都封裝在內(nèi)部泰偿,通過Command觸發(fā)變化熄守。
  • hierarchical state machine?是否支持狀態(tài)內(nèi)部子狀態(tài)耗跛。例如兩腿套利時裕照,兩腿分開發(fā)送,需要等到第一腿的委托執(zhí)行完畢之后再發(fā)送下一腿调塌,此時狀態(tài)仍然處于“正在執(zhí)行”狀態(tài)牍氛,僅將“等待下一腿”標(biāo)識清空。

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!)
需要狀態(tài)機(jī)的時候烟阐,直接去寫搬俊。如果經(jīng)常需要寫狀態(tài)機(jī)紊扬,可以自己建立一個庫。千萬不要太拘泥或追求形式唉擂,因為狀態(tài)機(jī)并不復(fù)雜餐屎。

在我的第一種實現(xiàn)中,并沒有考慮Command/Event玩祟,直接通過提供“設(shè)置下一狀態(tài)”來實現(xiàn)狀態(tài)變遷腹缩。這里是存在一些問題的:例如有些狀態(tài)調(diào)用時有多種可能的下一種目標(biāo)狀態(tài),這必須由外部Caller來明確進(jìn)行多次調(diào)用SetStatus函數(shù)空扎。最好的方式還是應(yīng)該簡單通過Command來改變狀態(tài)藏鹊。加入Command的設(shè)計其實就是封裝了這種調(diào)用邏輯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末转锈,一起剝皮案震驚了整個濱河市盘寡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌撮慨,老刑警劉巖竿痰,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異砌溺,居然都是意外死亡影涉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進(jìn)店門规伐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蟹倾,“玉大人,你說我怎么就攤上這事猖闪∠侍模” “怎么了?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵萧朝,是天一觀的道長岔留。 經(jīng)常有香客問我,道長检柬,這世上最難降的妖魔是什么献联? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮何址,結(jié)果婚禮上里逆,老公的妹妹穿的比我還像新娘。我一直安慰自己用爪,他們只是感情好原押,可當(dāng)我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著偎血,像睡著了一般诸衔。 火紅的嫁衣襯著肌膚如雪盯漂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天笨农,我揣著相機(jī)與錄音就缆,去河邊找鬼。 笑死谒亦,一個胖子當(dāng)著我的面吹牛竭宰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播份招,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼切揭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了锁摔?” 一聲冷哼從身側(cè)響起廓旬,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鄙漏,沒想到半個月后嗤谚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棺蛛,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡怔蚌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了旁赊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桦踊。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖终畅,靈堂內(nèi)的尸體忽然破棺而出籍胯,到底是詐尸還是另有隱情,我是刑警寧澤离福,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布杖狼,位于F島的核電站,受9級特大地震影響妖爷,放射性物質(zhì)發(fā)生泄漏蝶涩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一絮识、第九天 我趴在偏房一處隱蔽的房頂上張望绿聘。 院中可真熱鬧,春花似錦次舌、人聲如沸熄攘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挪圾。三九已至浅萧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間哲思,已是汗流浹背惯殊。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留也殖,地道東北人土思。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像忆嗜,于是被迫代替她去往敵國和親己儒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,652評論 2 354

推薦閱讀更多精彩內(nèi)容