如何用CriticalSection保护GMS多线程中的共享变量与数组
GMS3.6多线程线程安全问题解决方案
问题描述
在之前的帖子《Is it possible to speed up scripts using multi-threading?》中,@BmyGuest提供的示例代码提到:
// 为确保线程安全,以下两行应使用临界区保护 // 但鉴于调用近乎原子性,此处省略。 number chunkIndex = lastChunk lastChunk++
显然lastChunk是全局共享变量。GMS3.6的F1帮助文档虽提供了CriticalSection的实现示例,但仍不清楚如何实际使用。
此外,编写的多线程加速计算原型脚本仅在线程启动间插入sleep(0.1)(第165行)时能正常运行。若不添加该延迟,共享变量thread_idx会出现线程安全问题,同时线程方法RunNthDataChunk还会访问全局共享数组rstTiles和rstData。目前脚本在小规模数据下正常,但数据量增大或计算复杂后可能出错,需指导如何正确处理多线程对共享变量/数组的访问与修改。
附原型脚本代码:
class VDH_MultiThread_Test : thread { number true, false; // boolean // number debug; // input image attributes image srcData; number sizeX, sizeY; // 3D tile conversion attributes number tileSize; number delta; // tile movement number nStepX, nStepY, nCount; // result image image rstTiles, rstData; // string fmt; // display format of process time // number nThreads; // number of threads number thread_idx; // index of thread to be lauched number thread_completed; // counter to check how many threads have been completed // object startSignal, cancelSignal, globalCS; // VDH_MultiThread_Test( object self ) { true = 1; false = 0; debug = false; // fmt = "%.3f sec."; nThreads = 3; // preset number of threads to 3 // default source image size sizeX = 1024; sizeY = sizeX; // default tile size and movement tileSize = 64; delta = 8; // critical sections globalCS = NewCriticalSection(); startSignal = NewSignal( false ); cancelSignal = NewSignal( false ); // if(debug) result( "\n\n\nVDH_MultiThread_Test [obj ID:" + self.ScriptObjectGetID() + "] constructured\n" ); }; ~VDH_MultiThread_Test( object self ) { if(debug) result( "VDH_MultiThread_Test [obj ID:" + self.ScriptObjectGetID() + "] destructured\n\n" ); }; object SetNumberOfThreads( object self, number N_thread ) {; nThreads = N_thread; return self; }; object UseFrontImageAsData( object self ) { // use front image as data image srcData := GetFrontImage(); srcData.GetSize( sizeX, sizeY ); // tileSize = floor( sizeX/16 ); delta = ceil(tileSize/8); // nStepX = (sizeX - tileSize - (sizeX-tileSize)%delta) / delta + 1; nStepY = (sizeY - tileSize - (sizeY-tileSize)%delta) / delta + 1; nCount = nStepX * nStepY; // return self; }; image Construct_srcData( object self ) { // construct a source data image with default values // srcData := exprsize( sizeX, sizeY, icol%(tileSize+irow) ); // create image with fun pattern srcData.SetName( "default data" ); // nStepX = (sizeX - tileSize - (sizeX-tileSize)%delta) / delta + 1; nStepY = (sizeY - tileSize - (sizeY-tileSize)%delta) / delta + 1; nCount = nStepX * nStepY; // return srcData; }; image CreateTileStack( object self ) { // create a 3D image stack of tiles number tickStart = GetHighResTickCount(); rstTiles := RealImage( "Tiles (Slice2)", 4, tileSize, tileSize, nCount); number tickStart2 = GetHighResTickCount(); for( number idy = 0; idy < nStepY; idy++ ) { for( number idx = 0; idx < nStepX; idx++ ) { rstTiles.slice2( 0,0,idx+idy*nStepX, 0,tileSize,1, 1,tileSize,1 ) = \ srcData.slice2( idx*delta,idy*delta,0, 0,tileSize,1, 1,tileSize,1 ); }; }; number tickEnd = GetHighResTickCount() Result("Slice2 & Loop: \n") Result("image creation : " + tickStart.CalcHighResSecondsBetween(tickStart2).format(fmt) + "\n" ) Result("slice2 loop : " + tickStart2.CalcHighResSecondsBetween(tickEnd).format(fmt) + "\n" ) Result("TOTAL time : " + tickStart.CalcHighResSecondsBetween(tickEnd).format(fmt) + "\n\n" ) // return rstTiles; }; image FlattenTileStack( object self ) { // flatten the 3D tile stack. This flatten image should resemble original input data number tic = GetHighResTickCount(); image rst := exprsize( tileSize*nStepX, tileSize*nStepY, rstTiles[ icol%tileSize, irow%tileSize, floor(icol/tileSize) + floor(irow/tileSize)*nStepX ] ); number toc = GetHighResTickCount(); Result("Flatten tiles : " + tic.CalcHighResSecondsBetween(toc).format(fmt) + "\n" ) // rst.SetName( "flattened" ); // return rst; }; void RunNthDataChunk( object self ) { // divide data into N theads. Also need to take care of orphans number NthThread = thread_idx; number chunk = floor( nCount/nThreads ); number tileIndex_st = chunk*NthThread; number tileIndex_ed = tileIndex_st + chunk; if( nCount - tileIndex_ed < chunk ) tileIndex_ed = nCount; // result( "Start thread #" + (NthThread+1) + ", # tiles:" + (tileIndex_ed - tileIndex_st) + \ ", tiles [" + tileIndex_st + "-" + tileIndex_ed + "]\n" ); // number tic = GetHighResTickCount(); for( number tileIndex = tileIndex_st; tileIndex < tileIndex_ed; tileIndex++ ) { number avg = rstTiles.Slice2( 0,0,tileIndex, 0,tileSize,1, 1,tileSize,1 ).mean(); rstData.SetPixel( tileIndex%(nStepX), floor( tileIndex/nStepX ), avg ); }; number toc = GetHighResTickCount(); Result("\tthread #" + (NthThread+1) + " completed: " + tic.CalcHighResSecondsBetween(toc).format(fmt) + "\n" ); thread_completed += 1; // return; }; void Task1_thread( object self ) { result( "\n=== check if TileStack is generated properly ===\n\n" ); self.Construct_srcData(); srcData.ShowImage(); srcData.SetWindowSize(384,384); // self.CreateTileStack(); image flt := self.FlattenTileStack(); flt.ShowImage(); flt.SetWindowSize(384,384); result( "\n================ end of task1 ================\n\n" ); // return; }; void Task2_thread( object self ) { number wait = true; if( TwoButtonDialog( "Wait a moment between thread launches?", "Yes, wait for 0.1s", "No" ) ) wait=true else wait = false; // result( "\n===Task2: Break tile stack into " + nThreads + " threads ===\n" ); if( wait) result( "\t(Idling 0.1s between thread launches)\n\n" ); else result( "\t(No idling between thread launches)\n\n" ); self.Construct_srcData(); self.CreateTileStack(); result( "total of " + nStepX + "x" + nStepY + " = " + nCount + " tiles\n" ); // number tic = GetHighResTickCount(); // rstData := exprsize( nStepX, nStepY, srcData.mean() ); rstData.ShowImage(); rstData.SetWindowSize( 384, 384 ); rstData.SetName( "final result" ); // thread_completed = 0; for( thread_idx = 0; thread_idx < nThreads; thread_idx++ ) { cancelSignal.ResetSignal(); self.StartThread( "RunNthDataChunk" ); if(wait) sleep(0.1); }; while( thread_completed < nThreads && !ShiftDown() ) sleep(0.1); // number toc = GetHighResTickCount(); result("\nTotal time with " + nThreads + " threads: " + tic.CalcHighResSecondsBetween(toc).format(fmt) + "\n" ); result( "================ end of task2 ================\n\n" ); return; }; }; { object obj = alloc( VDH_MultiThread_Test ); obj.SetNumberOfThreads( 3 ); // obj.StartThread( "Task1_thread" ); obj.StartThread( "Task2_thread" ); };
解决方案
1. CriticalSection核心用法
GMS中CriticalSection用于保证共享资源的互斥访问,同一时间仅允许一个线程进入临界区代码块。使用逻辑为:
- 访问共享资源前调用
EnterCriticalSection() - 访问完成后调用
LeaveCriticalSection()
2. 针对脚本的具体修改
脚本中有三个核心线程安全风险点,需分别处理:
(1)线程ID分配(解决thread_idx竞争)
原脚本通过主线程循环赋值thread_idx,多线程启动过快时会导致多个线程读取到相同ID。改为让线程自行从临界区获取唯一ID:
- 在类成员中新增
next_thread_idx变量:
number next_thread_idx;
- 修改
Task2_thread中的线程启动逻辑,移除sleep(0.1):
thread_completed = 0; next_thread_idx = 0; // 初始化线程ID计数器 for( number i = 0; i < nThreads; i++ ) { cancelSignal.ResetSignal(); self.StartThread( "RunNthDataChunk" ); };
- 修改
RunNthDataChunk方法,通过临界区获取线程ID:
void RunNthDataChunk( object self ) { number NthThread; // 临界区保护线程ID获取,确保每个线程拿到唯一ID globalCS.EnterCriticalSection(); NthThread = next_thread_idx++; globalCS.LeaveCriticalSection(); // 计算当前线程负责的tile范围 number chunk = floor( nCount/nThreads ); number tileIndex_st = chunk*NthThread; number tileIndex_ed = tileIndex_st + chunk; // 最后一个线程处理剩余所有tile,避免遗漏 if( NthThread == nThreads - 1 ) tileIndex_ed = nCount; result( "Start thread #" + (NthThread+1) + ", # tiles:" + (tileIndex_ed - tileIndex_st) + \ ", tiles [" + tileIndex_st + "-" + tileIndex_ed + "]\n" ); number tic = GetHighResTickCount(); for( number tileIndex = tileIndex_st; tileIndex < tileIndex_ed; tileIndex++ ) { number avg = rstTiles.Slice2( 0,0,tileIndex, 0,tileSize,1, 1,tileSize,1 ).mean(); // 临界区保护像素写入,避免多线程同时修改同一位置 globalCS.EnterCriticalSection(); rstData.SetPixel( tileIndex%(nStepX), floor( tileIndex/nStepX ), avg ); globalCS.LeaveCriticalSection(); }; number toc = GetHighResTickCount(); Result("\tthread #" + (NthThread+1) + " completed: " + tic.CalcHighResSecondsBetween(toc).format(fmt) + "\n" ); // 临界区保护完成计数器自增 globalCS.EnterCriticalSection(); thread_completed += 1; globalCS.LeaveCriticalSection(); return; };
(2)共享图像写入保护
rstData.SetPixel()是写入操作,多线程同时写入会导致数据覆盖,必须用临界区包裹,确保同一时间只有一个线程修改图像像素。
(3)只读资源说明
rstTiles由主线程提前创建并填充完成,线程中仅执行读取操作(计算均值),读取操作本身是线程安全的,无需额外保护。
3. 修改后脚本的优势
- 彻底移除
sleep(0.1)依赖,避免不必要的延迟 - 所有共享资源的访问都通过临界区保护,数据量增大或计算复杂时仍能保证正确性
- 线程ID分配逻辑更可靠,不会出现重复ID问题
内容的提问来源于stack exchange,提问作者KEVIVI
相关产品推荐
相关产品推荐

