Continuando nossa série de artigos traduzidos do site lazyfoo, veremos agora como definir condições para a liberação de recursos bloqueados por uma thread.
Não apenas é possível bloquear seções críticas em threads, mas com mutexes e condições é possível que threads informem a outras threads quando efetuar o bloqueio.
//Our worker functions int producer( void* data ); int consumer( void* data ); void produce(); void consume();
No exemplo desse artigo, temos duas threads: uma produtora que preenche um buffer e uma consumidora que esvazia o buffer. Não apenas as duas threads não usam o mesmo buffer ao mesmo tempo, mas a consumidora não pode ler um buffer vazio e a produtora não pode preencher um buffer que já está cheio.
Usaremos um mutex (mutualmente exclusivo) para evitar que as duas threads usem o mesmo dado e condições para que as threads saibam quando podem consumir e produzir dados.
//The protective mutex SDL_mutex* gBufferLock = NULL; //The conditions SDL_cond* gCanProduce = NULL; SDL_cond* gCanConsume = NULL; //The "data buffer" int gData = -1;
Aqui declaramos globalmente o mutex e as condições que serão usadas pelas threads.
bool loadMedia() { //Create the mutex gBufferLock = SDL_CreateMutex(); //Create conditions gCanProduce = SDL_CreateCond(); gCanConsume = SDL_CreateCond(); //Loading success flag bool success = true; //Load splash texture if( !gSplashTexture.loadFromFile( "49_mutexes_and_conditions/splash.png" ) ) { printf( "Failed to load splash texture!\n" ); success = false; } return success; }
Para alocar as mutex e as condições usamos SDL_CreateMutex e SDL_CreateCond respectivamente.
void close() { //Free loaded images gSplashTexture.free(); //Destroy the mutex SDL_DestroyMutex( gBufferLock ); gBufferLock = NULL; //Destroy conditions SDL_DestroyCond( gCanProduce ); SDL_DestroyCond( gCanConsume ); gCanProduce = NULL; gCanConsume = NULL; //Destroy window SDL_DestroyRenderer( gRenderer ); SDL_DestroyWindow( gWindow ); gWindow = NULL; gRenderer = NULL; //Quit SDL subsystems IMG_Quit(); SDL_Quit(); }
E para desalocar mutex e condições usamos SDL_DestroyMutex e SDL_DestroyCond.
int producer( void *data ) { printf( "\nProducer started...\n" ); //Seed thread random srand( SDL_GetTicks() ); //Produce for( int i = 0; i < 5; ++i ) { //Wait SDL_Delay( rand() % 1000 ); //Produce produce(); } printf( "\nProducer finished!\n" ); return 0; } int consumer( void *data ) { printf( "\nConsumer started...\n" ); //Seed thread random srand( SDL_GetTicks() ); for( int i = 0; i < 5; ++i ) { //Wait SDL_Delay( rand() % 1000 ); //Consume consume(); } printf( "\nConsumer finished!\n" ); return 0; }
Aqui temos nossas duas threads. A produtora tenta produzir 5 vezes e a consumidora tenta consumir 5 vezes.
void produce() { //Lock SDL_LockMutex( gBufferLock ); //If the buffer is full if( gData != -1 ) { //Wait for buffer to be cleared printf( "\nProducer encountered full buffer, waiting for consumer to empty buffer...\n" ); SDL_CondWait( gCanProduce, gBufferLock ); } //Fill and show buffer gData = rand() % 255; printf( "\nProduced %d\n", gData ); //Unlock SDL_UnlockMutex( gBufferLock ); //Signal consumer SDL_CondSignal( gCanConsume ); } void consume() { //Lock SDL_LockMutex( gBufferLock ); //If the buffer is empty if( gData == -1 ) { //Wait for buffer to be filled printf( "\nConsumer encountered empty buffer, waiting for producer to fill buffer...\n" ); SDL_CondWait( gCanConsume, gBufferLock ); } //Show and empty buffer printf( "\nConsumed %d\n", gData ); gData = -1; //Unlock SDL_UnlockMutex( gBufferLock ); //Signal producer SDL_CondSignal( gCanProduce ); }
Aqui temos as funções que produzem e consumem. Produzir um buffer significa gerar um número aleatório e consumir significa resetar o número gerado. A melhor maneira de mostrar como isso funciona é através de um exemplo. Vamos dizer que o produtor é iniciado primeiro e bloqueia o mutex com SDL_LockMutex de forma análoga ao que um semáforo faria com um valor 1:
O buffer está vazio, de forma que ele segue adiante e produz um valor:
Então sai da função para desbloquear a seção crítica com SDL_UnlockMutex de forma que o consumidor pode consumir o valor:
Idealmente, queremos que o consumidor consuma o valor, mas imagine que o produtor seja iniciado novamente:
E após o produto bloquear a seção crítica o consumidor tenta obter o valor mas a seção crítica já está bloqueada pelo produtor:
Se tivessemos apenas um semáforo binário, isso seria um problema pois o produto não conseguiria produzir um buffer completo e o consumitor estaria bloqueado peo um mutex. Porém, mutexes tem a habilidade de ser usados com condições.
O que a condição permite que façamos é, se o buffer já estiver cheio, podemos esperar por uma condição com SDL_CondWait e desbloquear o mutex para outras threads:
Agora que o consumidor está desbloqueado, pode seguir para consumir o buffer:
E assim que estiver feito, sinaliza para o produtor com SDL_CondSignal para que ele produza novamente:
E assim pode continuar o processo:
Com a seção crítica protegida por um mutex, e a habilidade das threads de conversar entre si, as threads irá poder trabalhar mesmo que não saibamos em que ordem elas serão executadas.
Baixe os arquivos de mídia e do código donte desse artigo aqui aqui.