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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 15:44:54