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.