You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity C#国际象棋引擎多线程搜索无法正常终止问题排查

国际象棋引擎线程终止随机失效问题排查

我在Unity中使用C#开发国际象棋引擎,引擎在独立线程中运行搜索逻辑以保障主线程正常响应。为避免计算超时,我通过设置cancellationRequested标志让引擎终止所有进程,但引擎会随机出现无法响应终止信号的情况。我认为代码中已覆盖所有终止检查,但仍无法定位问题(已添加Debug.Log但无有效线索)。

相关代码

EnginePlayer.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public static class EnginePlayer
{
    public static Board board;
    public static MoveMaker moveMaker;
    static Engine engine;
    public static bool isSearching = false;
    static bool cancelled = false;
    static CancellationTokenSource searchTimer;

    public static void Initialize()
    {
        board = Main.mainBoard;
        engine = Main.engine;
    }

    public static void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log(engine.IsSearching() + " | " + engine.cancellationRequested);
        }

        // Get Engine Moves
        if (Graphic.isInMatch && 
        ((EngineSettings.enableWhiteEngine && board.isWhiteTurn) || (EngineSettings.enableBlackEngine && !board.isWhiteTurn)))
        {
            if (EngineSettings.useThreading)
            {
                if (!isSearching)
                {
                    RequestSearch();
                }
            }
            else
            {
                SingleThreadedSearch();
            }
        }
    }

    public static void SearchFinished()
    {
        Debug.Log("EnginePlayer Search Finished Signal");

        if (!cancelled)
        {
            Debug.Log("Search Not Cancelled - Call Get Best Move");
            GetBestMove();
        }

        isSearching = false;
        cancelled = false;

        Debug.Log("-----");
    }

    public static void CancelSearch()
    {
        cancelled = true;

        EndSearch();
    }

    public static void EndSearch()
    {
        engine?.TimeOut();
        searchTimer?.Cancel();
        
        isSearching = false;
    }

    static void RequestSearch()
    {
        Debug.Log("EnginePlayer Request");
        ThreadingManager.RequestStartSearch();
    }

    public static void SearchStarted()
    {
        Debug.Log("EnginePlayer Search Started Signal");

        isSearching = true;
        searchTimer = new CancellationTokenSource();
        Task.Delay(EngineSettings.searchMs, searchTimer.Token).ContinueWith((t) => {TimeOutThreadedSearch();});
    }

    static void GetBestMove()
    {
        Move move = engine.GetMove();
        searchTimer.Cancel();
            
        PlayMove(move);
    }

    static void SingleThreadedSearch()
    {
        engine.StartSearch(EngineSettings.searchDepth);
        Move move = engine.GetMove();
            
        PlayMove(move);
    }

    static void PlayMove(Move move)
    {
        if (move.moveValue != 0)
        {
            Graphic.grabbedPieceObject = moveMaker.FindPieceObject(move.startSquare);

            moveMaker.MakeGraphicalMove(move, true);
        }
        else
        {
            Debug.Log("Null Move Returned");
        }
    }

    static void TimeOutThreadedSearch()
    {
        engine?.TimeOut();
    }
}

ThreadingManager.cs

using System.Threading.Tasks;
using TMPro;
using UnityEngine;

public static class ThreadingManager
{
    static bool searchStarted = false;
    static bool searchFinished = false;
    static bool startSearchRequested = false;
    static bool positionLoadingRequested = false;

    static void ThreadedSearch()
    {
        searchStarted = true;

        Main.engine.StartSearch(EngineSettings.searchDepth);

        searchFinished = true;
    }

    public static void RequestStartSearch()
    {
        Debug.Log("Threading REQ");

        if (startSearchRequested)
        {
            return;
        }

        startSearchRequested = true;
        
        Debug.Log("Threading Rq St");
        Task.Factory.StartNew (() => ThreadedSearch(), TaskCreationOptions.LongRunning);
    }

    public static void RequestPositionLoading()
    {
        if (positionLoadingRequested)
        {
            return;
        }

        // Not searching
        if (!Main.engine.IsSearching())
        {
            Main.positionLoader.AfterEngineCancelled();
            return;
        }

        positionLoadingRequested = true;

        EnginePlayer.CancelSearch();
    }

    // Main thread
    public static void Update()
    {
        if (searchStarted)
        {
            EnginePlayer.SearchStarted();
            startSearchRequested = false;
            searchStarted = false;
        }

        if (searchFinished)
        {
            EnginePlayer.SearchFinished();

            if (positionLoadingRequested)
            {
                Main.positionLoader.AfterEngineCancelled();
                positionLoadingRequested = false;
            }

            searchFinished = false;
        }
    }
}

Engine.cs

using System;
using System.Collections.Generic;
using UnityEngine;

public class Engine
{
    Board board;
    public TranspositionTable tt;

    readonly int ttSize = EngineSettings.ttSize;

    public Move bestMove;
    bool isSearching;
    public bool cancellationRequested;


    public Engine()
    {
        board = Main.mainBoard;
        tt = new TranspositionTable(board, ttSize);
        isSearching = false;
        cancellationRequested = false;
    }

    public void StartSearch(int maxDepth)
    {
        Debug.Log("Engine StartSearch");
        isSearching = true; 
        cancellationRequested = false;

        bestMove = MoveGen.GenerateMoves(board)[0];

        // Return Null Move
        if (maxDepth <= 0)
        {
            
        }
        else
        {
            // Iterative Deepening
            for (int depth = 1; depth <= maxDepth; depth++)
            {
                Debug.Log("Starting Iteration " + depth);

                int evalThisIteration = Search(depth, Infinity.negativeInfinity, Infinity.positiveInfinity, 0);

                if (cancellationRequested)
                {
                    Debug.Log("Engine Cancel - ITERATIVE BREAK | DEPTH " + depth);
                    break;
                }

                if (Evaluation.IsMateScore(evalThisIteration))
                {
                    break;
                }
            }
        }
        
        EndSearch();
        Debug.Log("Engine EndSearch");
    }

    int Search(int depth, int alpha, int beta, int plyFromRoot)
    {
        if (cancellationRequested)
        {
            Debug.Log("Engine Cancel - HEAD | DEPTH " + depth);
            return 0;
        }

        // Try looking up the current position in the transposition table.
        // If the same position has already been searched to at least an equal depth
        // to the search we're doing now,we can just use the recorded evaluation.
        if (plyFromRoot != 0)
        {
            int ttVal = tt.LookupEvaluation (depth, plyFromRoot, alpha, beta);
            if (ttVal != TranspositionTable.lookupFailed)
            {
                // The Transposition Table cannot store the repetition data, 
                // so whenever a position is repeated, the engine ends up in a threefold draw.
                // To prevent that, check if it's threefold once again!
                // For simplicity, just check if previous position is already reached
                if (board.positionHistory[board.currentZobristKey] > 1)
                {
                    return 0;
                }

                Move move = tt.GetStoredMove ();

                if (plyFromRoot == 0)
                {
                    bestMove = move;
                }

                return ttVal;
            }
        }

        if (depth == 0)
        {
            return QuiescenceSearch(alpha, beta);
        }

        List<Move> legalMoves = MoveGen.GenerateMoves(board);

        MateChecker.MateState mateState = MateChecker.GetPositionState(board, legalMoves);
        if (mateState != MateChecker.MateState.None)
        {
            if (mateState == MateChecker.MateState.Checkmate)
            {
                return -Evaluation.checkmateEval + plyFromRoot;
            }

            return 0;
        }

        MoveOrder.GetOrderedList(legalMoves);

        int evalType = TranspositionTable.UpperBound;

        foreach (Move move in legalMoves)
        {
            board.MakeMove(move);

            int eval = -Search(depth - 1, -beta, -alpha, plyFromRoot + 1);

            board.UnmakeMove(move);

            if (cancellationRequested)
            {
                Debug.Log("Engine Cancel - Body | DEPTH " + depth);
                return 0;
            }

            if (eval >= beta)
            {
                tt.StoreEvaluation (depth, plyFromRoot, beta, TranspositionTable.LowerBound, move);
                return beta;
            }

            if (eval > alpha)
            {
                alpha = eval;
                evalType = TranspositionTable.Exact;
                
                if (plyFromRoot == 0)
                {
                    bestMove = move;
                }
            }

            tt.StoreEvaluation (depth, plyFromRoot, alpha, evalType, bestMove);
        }

        return alpha;
    }

    int QuiescenceSearch(int alpha, int beta)
    {
        if (cancellationRequested)
        {
            Debug.Log("QSEARCH HEAD");
            return 0;
        }

        int ttVal = tt.LookupEvaluation (0, 0, alpha, beta);
        if (ttVal != TranspositionTable.lookupFailed)
        {
            return ttVal;
        }

        int standPat = Evaluation.Evaluate(board);

        if (standPat >= beta)
        {
            return beta;
        }
        if (alpha < standPat)
        {
            alpha = standPat;
        }

        List<Move> moves = MoveGen.GenerateMoves(board, true);
        MoveOrder.GetOrderedList(moves);

        foreach (Move move in moves)
        {
            board.MakeMove(move);

            int eval = -QuiescenceSearch(-beta, -alpha);

            board.UnmakeMove(move);

            if (eval >= beta)
            {
                return beta;
            }
            if (eval > alpha)
            {
                alpha = eval;
            }
        }

        return alpha;
    }

    public Move GetMove()
    {
        return bestMove;
    }

    void EndSearch()
    {
        isSearching = false;
        cancellationRequested = true;
    }

    public void TimeOut()
    {
        Debug.Log("TimeOut");
        cancellationRequested = true;
    }


    public bool IsSearching()
    {
        return isSearching;
    }

    void AfterThreadedSearch()
    {
        // ThreadingManager.SearchFinished();
    }
}

调试日志截图

调试日志截图

如截图所示,输出“TimeOut”日志后,引擎陷入停滞,甚至未输出“EnginePlayer Search Finished Signal”这类日志。


问题根源分析

  1. 线程可见性缺失:cancellationRequested是普通bool字段,多线程环境下CPU可能会将其缓存到本地核心,导致主线程设置的终止信号无法被引擎线程及时读取,引擎仍在执行旧逻辑。
  2. QuiescenceSearch终止检查不全:仅在QuiescenceSearch方法头部做了终止检查,但循环体内部没有添加检查。如果超时发生时引擎正处于该方法的深层递归循环,无法及时响应终止信号。
  3. 线程状态竞争:ThreadingManager中的searchStarted、startSearchRequested等状态变量没有线程安全保护,多线程操作可能导致状态混乱,主线程无法正确感知搜索线程的结束状态。
  4. EndSearch逻辑矛盾:Engine的EndSearch方法强制将cancellationRequested设为true,正常结束搜索时也会触发该标志,干扰后续逻辑;同时终止信号设置后,引擎的递归调用栈可能未立即退出,导致搜索线程无法及时结束。

修复方案

  • 保证线程可见性:将cancellationRequested改为volatile bool,或使用Interlocked类操作该变量,确保多线程下的状态同步。
  • 补全终止检查:在QuiescenceSearch的循环体中添加终止检查:
    foreach (Move move in moves)
    {
        if (cancellationRequested)
        {
            Debug.Log("QSEARCH BODY CANCEL");
            return alpha;
        }
        board.MakeMove(move);
        int eval = -QuiescenceSearch(-beta, -alpha);
        board.UnmakeMove(move);
        // 原有逻辑
    }
    
  • 线程安全状态管理:对ThreadingManager中的状态变量使用lock或Interlocked确保线程安全,避免状态竞争。
  • 调整EndSearch逻辑:区分正常结束和终止结束的路径,正常结束时不要设置cancellationRequested,确保引擎线程退出后正确触发searchFinished信号。
  • 确保搜索结束通知:在Engine的StartSearch方法末尾,无论是否被终止,都要确保ThreadingManager的searchFinished状态被正确设置,让主线程及时感知搜索结束。

内容的提问来源于stack exchange,提问作者E_ple

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 21:42:02