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”这类日志。
问题根源分析
- 线程可见性缺失:
cancellationRequested是普通bool字段,多线程环境下CPU可能会将其缓存到本地核心,导致主线程设置的终止信号无法被引擎线程及时读取,引擎仍在执行旧逻辑。 - QuiescenceSearch终止检查不全:仅在QuiescenceSearch方法头部做了终止检查,但循环体内部没有添加检查。如果超时发生时引擎正处于该方法的深层递归循环,无法及时响应终止信号。
- 线程状态竞争:ThreadingManager中的
searchStarted、startSearchRequested等状态变量没有线程安全保护,多线程操作可能导致状态混乱,主线程无法正确感知搜索线程的结束状态。 - 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
相关产品推荐
相关产品推荐

