2017-04-26 67 views
1

我正在为NPC聊天编写一个服务器模拟器的脚本界面。玩家可以通过点击NPC启动NPC聊天。 NPC可以发送文本对话框。这些文本对话框还包含一个End Chat按钮,用于在脚本执行完成之前结束聊天,或者播放器可以继续正常的文本对话框直到它们结束。如何正确放弃某个线程?

当玩家中断聊天时,发送一个特殊的数据包。

我已经创建了一个名为WaitableResult类利用的ManualResetEvent阻止当前线程,直到给出结果,然后返回结果:

public sealed class WaitableResult<T> where T : struct 
{ 
    public T Value { get; private set; } 

    private ManualResetEvent mEvent; 

    public WaitableResult() 
    { 
     mEvent = new ManualResetEvent(false); 
    } 

    public void Wait() 
    { 
     mEvent.WaitOne(); 
    } 

    public void Set(T value) 
    { 
     mEvent.Set(); 

     this.Value = value; 
    } 
} 

这是我的脚本类:

internal sealed class NpcScript : ScriptBase 
{ 
    public WaitableResult<bool> BoolResult { get; private set; } 

    private Npc mNpc; 
    private Player mPlayer; 

    public NpcScript(Npc npc, Player player) 
     : base(string.Format(@"..\..\scripts\npcs\{0}.lua", npc.Script), true) 
    { 
     mNpc = npc; 
     mPlayer = player; 
     mPlayer.NpcConversation = this; 

     this.Expose("answer_no", false); 
     this.Expose("answer_yes", true); 
     this.Expose("answer_decline", false); 
     this.Expose("answer_accept", true); 

     this.Expose("say", new Func<string, bool>(this.Say)); 
     this.Expose("askYesNo", new Func<string, bool>(this.AskYesNo)); 
    } 

    public override void Dispose() 
    { 
     base.Dispose(); 

     mPlayer.NpcConversation = null; 
    } 

    private bool Say(string text) 
    { 
     this.BoolResult = new WaitableResult<bool>(); 

     using (OutPacket outPacket = mNpc.GetDialogPacket(ENpcDialogType.Standard, text, 0, 0)) 
     { 
      mPlayer.Client.SendPacket(outPacket); 
     } 

     this.BoolResult.Wait(); 

     return this.BoolResult.Value; 
    } 

    private bool AskYesNo(string text) 
    { 
     this.BoolResult = new WaitableResult<bool>(); 

     using (OutPacket outPacket = mNpc.GetDialogPacket(ENpcDialogType.YesNo, text)) 
     { 
      mPlayer.Client.SendPacket(outPacket); 
     } 

     this.BoolResult.Wait(); 

     return this.BoolResult.Value; 
    } 
} 

public abstract class ScriptBase 
{ 
    private string mPath; 
    private Thread mThread; 
    private MoonSharp.Interpreter.Script mScript; 


    public ScriptBase(string path, bool useThread = false, CoreModules modules = CoreModules.None) 
    { 
     mPath = path; 
     if (useThread) mThread = new Thread(new ThreadStart(() => mScript.DoFile(mPath))); 
     mScript = new MoonSharp.Interpreter.Script(modules); 
    } 

    public void Execute() 
    { 
     if (mThread != null) 
     { 
      mThread.Start(); 
     } 
     else 
     { 
      mScript.DoFile(mPath); 
     } 
    } 

    public virtual void Dispose() 
    { 
     if (mThread != null) 
     { 
      mThread.Abort(); 
      mThread = null; 
     } 
    } 

    protected void Expose(string key, object value) 
    { 
     mScript.Globals[key] = value; 
    } 
} 

这里是一个脚本的例子:

say('test') 
say('some more stuff') 
say('good bye') 

当玩家发起与NPC的聊天并完成而不会中断它(也就是使用End Chat按钮关闭它)时,线程应该自行中止(因为它完成了所有的指令)。

但是,当玩家在完成执行之前中止聊天,我打电话给Dispose按钮手动中止线程 - 但我不确定这是否正确。

每次玩家开始聊天时,我的记忆使用量也增加1 MB,所以这也很奇怪。

+3

你不能像一个进程那样真正地“杀死”一个线程。人们使用的模式是,在线程中运行的任何方法都需要不断检查“中止”标志,如果标志已设置,那么您的方法会逐字返回,从而停止执行。 .NET在使用'CancelToken'的任务中简化了这一点,例如,参见[这里](https://msdn.microsoft.com/en-us/library/dd997396(v = vs.110).aspx),你会请注意每次迭代*代码如何检查取消标记,如果取消标记被取消,那么它将停止执行(在这种情况下,通过抛出异常而不是返回)。 – Quantic

+4

至于'Thread.Abort'不好,[1](http://stackoverflow.com/questions/2251964/c-sharp-thread-termination-and-thread-abort):“唯一可以保证安全的东西终止一个不合作的线程是操作系统关闭它的整个过程。“,它链接到[2](http://stackoverflow.com/questions/1559255/whats-wrong-with-using-thread-abort/1560567# 1560567):“无法保证Thread.Abort的调用实际上会中止所讨论的线程,”“简而言之,Thread.Abort最多表示设计不好,可能不可靠,而且非常危险。 – Quantic

+3

而不是将其视为取消将其视为取消。相关阅读:https://msdn.microsoft.com/en-us/library/dd997364(v=vs.110).aspx –

回答

0

由于Quantic已经解决,通过Thread.Abort中止线程引起意外 - 甚至是危险的结果。 Quantic也解决了处理线程的首选模式,通常是线程循环和检查终止标志,如果您正在执行任意Lua脚本,这是不可能的。

你有一个选择是创建一个简约的调试器并将其附加到您的脚本。一旦连接了该调试器,将为运行脚本中的每条指令调用IDebugger.GetAction()方法。 如果在GetAction()调用中调用Exception,脚本将被调试器彻底终止。

Here is an example一个哨兵,简约调试器,使用这种技术停止长时间运行的脚本。正如你所看到的,大部分的实现都是空的。在你的情况下,你可能只需要一个布尔标志来代替使用运行指令计数器,该标志表示脚本必须被终止。

public DebuggerAction GetAction(int ip, SourceRef sourceref) 
{ 
    if(_abortScript)   
     throw new MyException(); // abort cleanly 

    // Proceed running the next statement 
    return new DebuggerAction() { 
     Action = DebuggerAction.ActionType.StepIn, 
    }; 
} 

关于内存增加您遇到,那就是人工产卵线程的不便之一,each thread initializes its own stack space and on a 32-bit process, the default is precisely 1MB。有一个Thread constructor overload,允许您自定义堆栈大小;根据脚本的复杂程度,可能会将此值设置为较小的值,而不会冒堆栈溢出情况。尽量减少内存使用的另一个选择是使用Task.Run而不是产生自己的线程(这使用线程池),但是然后避免使用Thread.Abort将是必需的,您不想去查杀池线程!