Tutorial de SDL – Parte 47 – Semáforos

Continuando nossa série de artigos traduzidos do site lazyfoo, agora veremos como duas threads podem compartilhar dados sem acessa-los ao mesmo tempo.Até agora, o único programa multithread que criamos tinha apenas a thread principal e uma segunda thread onde cada uma fazia suas próprias coisas. Na maioria dos casos, threads terão que compartilhar dados e com semáforos você pode evitar que duas threads  acidentalmente acessem o mesmo dado ao mesmo tempo.

//Our worker thread function
int worker( void* data );

Aqui temos nossa função de thread. Iremos ter duas thread onde cada uma executará uma cópia desse código.

//Data access semaphore
SDL_sem* gDataLock = NULL;
//The "data buffer"
int gData = -1;

O objeto gDataLock é o nosso semáforo, que irá trabar o buffer gData. Um único inteiro não representa muito algo que seja preciso proteger, mas como iremos ter duas threads que irão estar lendo e escrevendo nesse buffer, iremos ter que certificar que ele é acessado por uma thread por vez.

bool loadMedia()
{
    //Initialize semaphore
    gDataLock = SDL_CreateSemaphore( 1 );
    //Loading success flag
    bool success = true;
    //Load splash texture
    if( !gSplashTexture.loadFromFile( "47_semaphores/splash.png" ) )
    {
        printf( "Failed to load splash texture!\n" );
        success = false;
    }
    return success;
}

Para criar um semáforo, chamamos SDL_CreateSemaphore com um valor inicial para o semáforo. O valor inicial controla quantas vezes o código pode passar pelo semáforo antes que ele seja bloqueado.

Por exemplo, digamos que você queira que apenas 4 threads sejam executadas por vez, pois você está executando o programa em um hardware que possui 4 núcleos. Você fornece ao semáforo um valor inicial de 4 para certificar-se de que não mais do que 4 threads sejam executadas ao mesmo tempo. No exemplo desse artigo, queremos que apenas 1 thread acesse o buffer de dados por vez, assim o mutex é iniciado com o valor 1.

void close()
{
    //Free loaded images
    gSplashTexture.free();
    //Free semaphore
    SDL_DestroySemaphore( gDataLock );
    gDataLock = NULL;
    //Destroy window
    SDL_DestroyRenderer( gRenderer );
    SDL_DestroyWindow( gWindow );
    gWindow = NULL;
    gRenderer = NULL;
    //Quit SDL subsystems
    IMG_Quit();
    SDL_Quit();
}

Quando o semáforo não foi mais necessário, chamamos SDL_DestroySemaphore.

int worker( void* data )
{
    printf( "%s starting...\n", data );
    //Pre thread random seeding
    srand( SDL_GetTicks() );

Aqui iniciamos a nossa thread. Uma coisa importante para tomar conhecimento é que geração de um valor aleatório é feito por thread, assim se certifique que isso seja feito para cada thread que for executada.

    //Work 5 times
    for( int i = 0; i < 5; ++i )
    {
        //Wait randomly
        SDL_Delay( 16 + rand() % 32 );
        //Lock
        SDL_SemWait( gDataLock );
        //Print pre work data
        printf( "%s gets %d\n", data, gData );
        //"Work"
        gData = rand() % 256;
        //Print post work data
        printf( "%s sets %d\n\n", data, gData );
        //Unlock
        SDL_SemPost( gDataLock );
        //Wait randomly
        SDL_Delay( 16 + rand() % 640 );
    }
    printf( "%s finished!\n\n", data );
    return 0;
}

O que cada thread faz é ser atrasada por uma quatidade alea´tória de tempo, imprime o dado que ela tinha quando começou a ser executada, associa um número aleatório à ela, exibe o número associada ao buffer e é atrasada um pouco mais antes de ser executada novamente. A razão pela qual precisamos bloquear o dado é porque não queremos duas threads lendo ou escrevendo em nosso dado compartilhada ao mesmo tempo.

Observe as chamadas para SDL_SemWait e SDL_SemPost. O que está entre elas é a seção crítica ou o código que queremos que apenas uma thread tenha acesso por vez. SDL_SemWait diminui o valor de semáforo e, como o valor inicial é 1, irá bloquear o dados. Após as seções críticas serem executadas, chamamos SDL_SemPost para aumentar o valor do semáforo e desbloquear o dado.

Se tivermos uma situação onde a thread A bloqueia o semáforo e então a thread B tentar bloquea-lo, a threadB irá esperar até que a thread A termine de executar a seção crítica e desbloqueie o semáforo. Com a seção crítica protegida por um par de semáforos, apenas uma thread ode executar a seção crítica por vez.

            //Main loop flag
            bool quit = false;
            //Event handler
            SDL_Event e;
            //Run the threads
            srand( SDL_GetTicks() );
            SDL_Thread* threadA = SDL_CreateThread( worker, "Thread A", (void*)"Thread A" );
            SDL_Delay( 16 + rand() % 32 );
            SDL_Thread* threadB = SDL_CreateThread( worker, "Thread B", (void*)"Thread B" );

Na função principal, antes de entrarmos no loop principal, disparamos duas threads com um atraso aleatório entre elas. Não haverá nenhuma garantia de que a thread A ou B irá ser executada primeiro, mas como o dado que elas compartilham está protegido, saberemos que elas não irão tentar executar o mesmo código ao mesmo tempo.

            //While application is running
            while( !quit )
            {
                //Handle events on queue
                while( SDL_PollEvent( &e ) != 0 )
                {
                    //User requests quit
                    if( e.type == SDL_QUIT )
                    {
                        quit = true;
                    }
                }
                //Clear screen
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
                SDL_RenderClear( gRenderer );
                //Render splash
                gSplashTexture.render( 0, 0 );
                //Update screen
                SDL_RenderPresent( gRenderer );
            }
            //Wait for threads to finish
            SDL_WaitThread( threadA, NULL );
            SDL_WaitThread( threadB, NULL );

Aqui, a thread principal é executada enquando as threads disparadas fazem seu trabalho. Se o loop principal é encerrado antes das threads terminarem, esperamos por elas com SDL_WaitThread.

Baixe os arquivos de mídia e do código fonte desse artigo aqui.