Variáveis de condição, semáforos e impasses

As travas impedem que threads interfiram umas nas outras, mas elas também precisam esperar umas pelas outras. Este material mostra como variáveis de condição e semáforos fazem uma thread dormir até que dados estejam prontos, uma posição esteja livre ou outra thread tenha terminado, e os usa para resolver os problemas do produtor e consumidor e dos leitores e escritores. Em seguida, aborda o que dá errado em código concorrente real: violações de atomicidade e de ordem, além de impasses, com formas de preveni-los, evitá-los e detectá-los. Por fim, apresenta a concorrência baseada em eventos, na qual uma única thread executa um laço de eventos em vez de usar muitas threads. Os exemplos usam C com threads POSIX e são acompanhados passo a passo.

IntermediárioAula 6 de 9 do curso Curso de Sistemas operacionais

O que você vai aprender

  1. Faça uma thread dormir até que uma condição sobre dados compartilhados se torne verdadeira, usando uma variável de condição junto com uma trava e uma variável de estado, e use esse padrão para construir um buffer limitado correto.
    • Aguarde uma condição em vez de usar espera ocupada
      Explique por que uma trava sozinha não pode fazer uma thread esperar por um evento e use wait e signal com uma trava e uma variável de estado para que nenhum despertar seja perdido.
    • Verifique novamente as condições e escolha entre signal e broadcast
      Aplique a semântica de Mesa verificando novamente a condição em um laço while, escolha entre signal e broadcast e reconheça as mesmas regras em monitores e em Java.
    • Resolva o problema do produtor e consumidor com variáveis de condição
      Construa um buffer limitado compartilhado por threads produtoras e consumidoras, acompanhe por que versões ingênuas falham e corrija-as com laços while e duas variáveis de condição.
  2. Use o semáforo, uma única primitiva, como trava, como sinal entre threads e como contador de recursos, e aplique-o ao buffer limitado e a uma trava de leitores e escritores.
    • Defina um semáforo e use-o como trava
      Descreva como o valor de um semáforo muda com wait e post, escolha seu valor inicial e use um semáforo binário para exclusão mútua.
    • Ordene threads e limite a concorrência com semáforos
      Use um semáforo que começa em zero para fazer uma thread esperar por outra e um semáforo contador para limitar quantas threads usam um recurso ao mesmo tempo.
    • Construa um buffer limitado com semáforos
      Resolva o problema do produtor e consumidor com dois semáforos contadores e um mutex e explique por que a ordem em que são adquiridos determina se o buffer funciona ou entra em impasse.
    • Compartilhe dados com uma trava de leitores e escritores
      Permita vários leitores ou um escritor por vez, construa essa trava com semáforos e explique seu problema de inanição e seus custos.
  3. Identifique violações de atomicidade e de ordem, os dois padrões por trás de quase todos os erros de concorrência sem impasse encontrados em um estudo de software real, e corrija cada um com a ferramenta adequada.
    • Encontre e corrija violações de atomicidade
      Reconheça código que supõe que um grupo de acessos executa sem interrupção, mostre a intercalação que o compromete e proteja todo o grupo com uma única trava.
    • Encontre e corrija violações de ordem
      Reconheça código que supõe que um acesso acontece antes de outro e garanta essa ordem com uma variável de condição, um semáforo ou a espera pelo término de uma thread, em vez de depender do tempo de execução.
  4. Explique como um impasse surge de quatro condições, previna-o eliminando uma delas no projeto e descreva como sistemas evitam impasses com conhecimento prévio ou os detectam e se recuperam deles.
    • Explique como um impasse surge
      Defina impasse, mostre a intercalação que o causa, nomeie as quatro condições necessárias e use um grafo de espera para decidir se threads estão em impasse.
    • Previna impasses eliminando uma condição necessária
      Projete o código para que uma das quatro condições nunca possa ocorrer: ordene a aquisição de travas, adquira todas as travas de uma vez, recue com trylock ou evite travas com instruções atômicas.
    • Evite impasses com escalonamento e o algoritmo do banqueiro
      Use o conhecimento prévio do que as threads podem solicitar para conceder apenas requisições que mantenham o sistema em um estado seguro, acompanhando o algoritmo do banqueiro com números reais.
    • Detecte impasses e recupere o sistema
      Permita que impasses ocorram, encontre-os buscando ciclos, recupere o sistema encerrando uma thread ou revertendo-a e compare essa abordagem com prevenção, evitação e não fazer nada.
  5. Estruture um programa em torno de um laço de eventos que reage à prontidão para entrada e saída, evite bloqueios com entrada e saída assíncrona e preservação de estado e compare eventos e threads.
    • Execute um laço de eventos com select ou poll
      Escreva um laço de eventos de uma única thread que pergunte ao sistema operacional quais descritores estão prontos, processe cada evento até a conclusão e nunca bloqueie.
    • Evite bloqueios com entrada e saída assíncrona e preservação de estado
      Emita operações de entrada e saída sem esperar por elas, descubra quando foram concluídas e preserve nesse intervalo o estado necessário a um tratador.
    • Escolha entre eventos e threads
      Compare projetos baseados em eventos e em threads quanto a controle, memória, uso de múltiplos núcleos e facilidade de manutenção, e reconheça os erros que permanecem no código baseado em eventos.

Guia de estudo de Variáveis de condição e semáforos

5 capítulos

1

Aguarde uma mudança no estado compartilhado

Uma thread muitas vezes precisa de mais do que acesso exclusivo: ela precisa que outra thread termine um trabalho ou disponibilize dados. Variáveis de condição permitem que ela durma durante essa espera. Seu funcionamento correto depende de uma variável de estado compartilhada, um mutex e uma passagem cuidadosa entre a verificação e o início da espera. Comece com uma thread pai esperando por uma filha e depois use o mesmo padrão para coordenar produtores e consumidores sem desperdiçar tempo de CPU ou perder despertares.

1.1 Estado, um mutex e um lugar para dormir

Um mutex responde “Quem pode acessar estes dados agora?” Ele não responde “O trabalho terminou?” Para esperar por um evento, uma thread precisa de um registro do evento e de uma forma de dormir até que esse registro mude.

Por que a espera ocupada é uma forma ruim de esperar

O laço while (done == 0) ; gasta tempo de CPU verificando repetidamente um indicador. Em uma única CPU, esse trabalho atrasa a filha que precisa definir o indicador. Em C, uma leitura não sincronizada de uma variável comum enquanto outra thread escreve nela também é uma corrida de dados. O compilador não precisa recarregar o indicador a cada iteração.

Uma variável de condição fornece uma fila de espera às threads. O POSIX fornece pthread_cond_t, inicializado com PTHREAD_COND_INITIALIZER ou pthread_cond_init(). A fila é separada do estado que está sendo testado: done registra a conclusão; a variável de condição fornece um lugar para dormir.

Mantenha a verificação e a espera juntas

pthread_cond_wait(&c, &m) exige que quem a chama mantenha o mutex m adquirido. Ela libera esse mutex e começa a esperar atomicamente. Antes de retornar, adquire o mutex novamente. Isso elimina o intervalo perigoso no qual outra thread poderia alterar o estado e sinalizar antes que a thread em espera tivesse realmente começado a esperar.

Veja uma thread pai esperando a conclusão de uma filha. Os nomes lock, wait, signal e unlock abreviam as chamadas correspondentes da biblioteca de threads.

/* Parent */
lock(&m);
while (done == 0)
    wait(&c, &m);
unlock(&m);

/* Child */
lock(&m);
done = 1;
signal(&c);
unlock(&m);

Inicialmente, done é 0. Se a thread pai executar primeiro, ela adquire a trava, encontra trabalho não concluído e espera. A espera libera m, permitindo que a filha o adquira, defina done e sinalize. A pai acaba adquirindo m novamente, verifica done outra vez e continua.

Se a filha executar primeiro, ela registra a conclusão antes de a pai chegar. Seu sinal não desperta ninguém, mas a pai, ao chegar depois, vê done == 1 e não espera. O funcionamento correto deve resistir às duas ordens.

Sinais não armazenam eventos

pthread_cond_signal(&c) desbloqueia pelo menos uma thread em espera quando há threads esperando; o POSIX permite que mais de uma desperte. Sem threads em espera, ela não tem efeito. pthread_cond_broadcast(&c) desperta todas as threads que estão esperando naquele momento. Nenhuma das chamadas guarda uma notificação para threads futuras.

Omitir done compromete, portanto, o padrão de espera pelo término: uma filha pode sinalizar antes de sua pai esperar, após o que a pai dorme sem que venha outro sinal. Isso é um despertar perdido. Verificar done fora do mutex cria outra janela para um despertar perdido: a pai verifica e encontra falso, a filha define como verdadeiro e sinaliza, e então a pai começa a esperar.

Proteja a verificação e toda alteração de estado com o mesmo mutex. Mantenha-o adquirido desde a verificação até a chamada de espera. Uma sequência simples de sinalização altera o estado e sinaliza enquanto ainda mantém esse mutex adquirido. O POSIX permite sinalizar sem mantê-lo adquirido, mas o protocolo de estado compartilhado ainda deve impedir um despertar perdido.

Principais conclusões

  • O estado registra o evento; a variável de condição fornece uma fila de espera.
  • Chame wait com o mutex adquirido; wait o libera atomicamente e o adquire novamente antes de retornar.
  • Proteja a verificação e a alteração de estado com o mesmo mutex.
  • Um sinal enviado sem threads em espera não é guardado.

1.2 Verifique novamente após despertar

Retornar da espera significa que a thread pode tentar novamente. Isso não significa que a condição de que ela precisa ainda seja verdadeira. Essa distinção importa sempre que várias threads competem pelos mesmos dados.

Semânticas de Mesa e Hoare
SemânticaO que um sinal fornece à thread em espera
MesaProntidão para executar; a thread em espera deve adquirir o mutex novamente e verificar o estado outra vez.
HoareUma passagem imediata da trava e da execução, preservando a condição sinalizada nesse momento.

POSIX, Java, variáveis de condição do C++ e condições do Python seguem o comportamento de Mesa. Os monitores de Hoare introduziram o modelo de passagem mais forte; a abordagem de Mesa é a escolha prática comum.

Suponha que C1 espere um buffer de uma posição deixar de estar vazio. Um produtor o preenche e sinaliza C1. C1 agora está pronta, mas C2 pode adquirir o mutex primeiro e retirar o item. Quando C1 finalmente adquire o mutex novamente, o buffer está vazio outra vez.

lock(&m);
while (count == 0)
    wait(&filled, &m);
v = get();
unlock(&m);

O while repete o teste após cada retorno. Um if testa apenas antes da primeira espera e permitiria que C1 chamasse get() em um buffer vazio. Mesmo com um produtor e um consumidor, use while: o POSIX também permite despertares espúrios, nos quais wait retorna sem um sinal correspondente.

Despertar uma thread ou todas?

Um sinal é adequado quando as threads em espera precisam da mesma condição e qualquer uma pode aproveitar a mudança. Um broadcast é útil quando várias podem prosseguir ou quando diferentes threads em espera precisam de quantidades diferentes e quem sinaliza não consegue selecionar a correta.

Considere um alocador com 0 bytes livres. Ta precisa de 100 bytes e Tb precisa de 10. Outra thread libera 50 bytes. Um sinal pode despertar Ta, que ainda não consegue prosseguir, deixando Tb dormindo mesmo havendo o suficiente para sua requisição. Um broadcast desperta ambas. Elas adquirem o mutex novamente uma por vez; Tb pode alocar, enquanto Ta volta a esperar. Essa é uma condição abrangente: a notificação abrange todas as threads em espera potencialmente aptas a prosseguir.

Um broadcast pode causar despertares extras, trocas de contexto e disputa pelo mutex. Variáveis de condição separadas para predicados distintos podem tornar a sinalização mais seletiva. Em um buffer, produtores e consumidores não devem compartilhar uma única fila de sinais indiferenciada.

A relação com monitores

Um monitor reúne dados compartilhados com operações que admitem uma thread por vez e variáveis de condição para espera. Objetos Java fornecem uma trava e um conjunto de espera. Dentro de synchronized, uma thread pode chamar wait(), notify() ou notifyAll(); chamá-los sem possuir o monitor desse objeto lança IllegalMonitorStateException.

A espera libera apenas o mutex associado ou a trava do objeto Java. Se uma thread também mantiver outra trava adquirida, essa segunda trava permanece adquirida. Um produtor que precise dela pode não conseguir tornar a condição verdadeira. Verifique todas as travas adquiridas ao esperar, não apenas a indicada na chamada.

Principais conclusões

  • Na semântica de Mesa, o despertar deixa uma thread pronta; ela ainda compete pelo mutex.
  • Espere em um laço while para lidar com mudanças de estado e despertares espúrios.
  • Use broadcast quando um único sinal puder deixar adormecida uma thread em espera apta a prosseguir.
  • Condições de espera distintas podem usar variáveis de condição separadas.
  • A espera libera apenas sua trava associada.

1.3 Construa um buffer limitado com duas filas de espera

Produtores adicionam itens a um buffer de tamanho fixo; consumidores os removem. Um buffer limitado correto impede a sobrescrita de um buffer cheio, a remoção de um buffer vazio e alterações concorrentes em seus dados de controle. Uma fila de requisições de servidor web e o canal de comunicação entre grep error log.txt e wc -l seguem esse padrão.

Acompanhe os índices circulares

Um buffer circular usa um array e dois índices. fill_ptr indica a próxima escrita; use_ptr indica a próxima leitura. Ambos avançam módulo MAX. Um count separado distingue vazio de cheio, mesmo quando os dois índices são iguais.

void put(int v) {
    buffer[fill_ptr] = v;
    fill_ptr = (fill_ptr + 1) % MAX;
    count++;
}
int get(void) {
    int v = buffer[use_ptr];
    use_ptr = (use_ptr + 1) % MAX;
    count--;
    return v;
}

Comece com capacidade 3 e ambos os índices em 0. Inserir 7, 8 e 9 preenche as posições 0, 1 e 2. O índice de escrita retorna a 0 e count chega a 3. A retirada retorna 7 e avança o índice de leitura para 1. Inserir 10 reutiliza a posição 0. O array agora é [10, 8, 9], mas a remoção FIFO segue o índice de leitura: primeiro vem 8, depois 9 e depois 10.

Direcione cada sinal ao papel oposto

Use um mutex, empty para produtores que esperam por espaço e fill para consumidores que esperam por um item. Inicialize ambas as variáveis de condição e o mutex antes de iniciar as threads.

/* Producer: repeat for each item v */
lock(&m);
while (count == MAX)
    wait(&empty, &m);
put(v);
signal(&fill);
unlock(&m);

/* Consumer */
lock(&m);
while (count == 0)
    wait(&fill, &m);
v = get();
signal(&empty);
unlock(&m);

A verificação e a atualização do buffer permanecem em uma única seção crítica. Caso contrário, dois produtores poderiam ver a mesma posição livre e sobrescrever o trabalho um do outro. A espera libera m, permitindo que o papel oposto altere o buffer.

Por que uma única fila pode parar

Com uma variável de condição e sinais, produtores e consumidores podem despertar o papel errado. Para capacidade 1, C1 e C2 primeiro dormem porque o buffer está vazio. P insere um item, desperta C1 e então espera porque o buffer está cheio. C1 remove o item e sinaliza, mas o sinal pode despertar C2 em vez de P. C1 espera novamente; C2 também encontra o buffer vazio e espera. P continua dormindo embora haja espaço disponível. Duas filas direcionam cada sinal ao papel que agora pode prosseguir. Um broadcast em uma fila compartilhada pode evitar esse problema de seleção, ao custo de despertares extras.

Uma capacidade maior absorve rajadas e permite a sobreposição do trabalho de produção e consumo fora da curta seção crítica do buffer. As próprias atualizações do buffer continuam serializadas. A capacidade 1 força a alternância de cada inserção e remoção.

Para um encerramento correto, primeiro espere que todos os produtores terminem de inserir seus itens reais. Depois, enfileire um item reservado de fim dos dados por consumidor, como -1 quando os itens reais não são negativos. Cada consumidor sai após retirar um deles. Enviar esses marcadores enquanto os produtores ainda estão ativos pode deixar itens reais sem consumidor.

Principais conclusões

  • Avance os índices circulares módulo a capacidade; a contagem distingue vazio de cheio.
  • Produtores esperam por espaço e sinalizam itens; consumidores esperam por itens e sinalizam espaço.
  • Use laços while e um único mutex para proteger o predicado e a atualização do buffer.
  • Uma fila de sinais compartilhada pode despertar o papel errado e deixar todas as threads dormindo.
  • Aguarde todos os produtores terminarem antes de enfileirar um marcador de fim por consumidor.
  • A ordem FIFO segue o índice de leitura, não a ordem física do array.
2

Conte permissões com semáforos

Um semáforo registra permissões que as threads adquirem e devolvem. Seu valor inicial permite que a mesma primitiva garanta acesso exclusivo, lembre um evento ou limite um conjunto de recursos. Acompanhe seu valor ao longo de operações wait e post e depois combine contagens de permissões com um mutex para construir um buffer limitado. Um projeto de leitores e escritores acrescenta outro uso: compartilhar o acesso entre leitores mantendo a exclusividade para escritores, com um custo de justiça a examinar.

2.1 Escolha o valor inicial de um semáforo

Um semáforo mantém um inteiro alterado por operações atômicas wait e post. Dijkstra o apresentou na década de 1960 como P e V; o POSIX chama as operações de sem_wait() e sem_post(). Atômico significa que duas threads não podem realizar parcialmente a mesma operação de contador ao mesmo tempo.

As unidades disponíveis determinam o valor inicial

sem_init(&s, 0, X) cria um semáforo sem nome com valor X. O segundo argumento 0 seleciona o compartilhamento entre threads de um processo. Para compartilhamento entre processos, um argumento diferente de zero exige que o semáforo fique em memória compartilhada.

FinalidadeValor inicialSignificado
Trava1Uma thread pode entrar imediatamente.
Ordenação de eventos0O evento ainda não ocorreu.
Conjunto de recursosNN unidades podem ser adquiridas agora.

Um semáforo binário usado como trava envolve uma seção crítica com wait e post. Iniciá-lo em 0 bloqueia a primeira thread que tenta entrar. Iniciá-lo em 2 permite a entrada de duas threads e não fornece exclusão mútua.

Dois modelos equivalentes de contagem

Na definição de Dijkstra, o valor permanece não negativo: wait dorme até poder decrementar sem ficar abaixo de zero, e post adiciona uma unidade. O Linux segue essa convenção de valores não negativos.

Para acompanhamentos manuais, use o modelo de contador com sinal: wait decrementa e bloqueia se o novo valor for negativo; post incrementa e desperta uma thread em espera quando há uma bloqueada. Nesse modelo, o módulo de um valor negativo corresponde ao número de threads bloqueadas em espera. É uma convenção de raciocínio, não um valor negativo a esperar da API de semáforos do Linux.

Acompanhe uma trava inicializada em 1:

  1. T0 executa wait: 1 passa a 0, então T0 entra.
  2. T1 executa wait: 0 passa a −1, então T1 bloqueia.
  3. T0 executa post: −1 passa a 0, permitindo que T1 continue.
  4. T1 termina e executa post: 0 passa a 1.

O despertar transfere a permissão existente a T1; não é necessário outro wait antes de T1 entrar em sua seção crítica.

Um post é lembrado

Sem nenhuma thread em espera, um post aumenta a contagem disponível. Um wait futuro pode consumir essa permissão. Isso é útil quando uma thread termina antes que a thread que espera por ela tenha começado. Um sinal de variável de condição não tem essa memória.

Semáforos não têm dono: uma thread pode executar wait e outra pode executar post. Um mutex pertence à thread que o adquiriu; liberar um mutex POSIX padrão de outra thread é comportamento indefinido. O semáforo sem dono permite a sinalização, mas também dificulta o diagnóstico de um post extra.

Um semáforo pode ser construído com um mutex, uma variável de condição e um inteiro não negativo. Wait adquire a trava, executa um laço enquanto value <= 0, espera, decrementa e libera a trava. Post adquire a trava, incrementa, sinaliza e libera a trava. O mutex torna atômicas as operações no contador; o laço lida com despertares que não deixam uma unidade disponível. Travas com variáveis de condição e semáforos podem implementar uns aos outros, embora uma implementação correta de variável de condição com semáforos seja mais complexa.

Principais conclusões

  • Inicialize um semáforo com o número de unidades disponíveis imediatamente.
  • Um semáforo usado como trava começa em 1 e combina wait antes da seção com post depois dela.
  • O modelo de acompanhamento com contador com sinal usa valores negativos para threads bloqueadas em espera; os valores no Linux permanecem não negativos.
  • Operações post são lembradas como permissões, mesmo sem threads esperando no momento.
  • Um semáforo não tem dono; um mutex tem.

2.2 Registre eventos e limite o uso de recursos

O mesmo par wait/post pode representar um evento ou uma quantidade limitada de recursos. Decida o que uma permissão significa antes de escolher a contagem inicial e onde cada operação deve ficar.

Faça a conclusão preceder o uso

Para uma thread pai que espera por uma filha, inicialize finished em 0. A filha executa post após terminar seu trabalho. A pai executa wait antes de usar o resultado.

/* Child */
produce_result();
sem_post(&finished);

/* Parent */
sem_wait(&finished);
use_result();

Se a pai chegar primeiro, o valor com sinal muda de 0 para −1 e ela dorme. O post da filha retorna o valor a 0 e a desperta. Se a filha chegar primeiro, seu post muda 0 para 1; o wait posterior muda 1 para 0 sem bloquear. Um evento concluído permite um wait bem-sucedido.

Conte um conjunto de recursos

Um conjunto de quatro conexões de banco de dados usa um semáforo inicializado em 4. Cada cliente executa wait antes de obter uma conexão e post depois de devolvê-la. Um quinto cliente simultâneo dorme. O semáforo conta as admissões; um mutex separado ainda protege a lista compartilhada que identifica qual conexão está livre. Duas threads admitidas não devem remover a mesma entrada da lista.

A limitação de concorrência aplica a mesma ideia a uma fase exigente. Suponha que 100 threads precisem de 500 MB cada uma durante o processamento. Juntas, poderiam precisar de 50.000 MB. Uma máquina com 8 GB tem 8.000 MB em unidades decimais do SI. Admitir no máximo 12 trabalhadoras limita essa fase a 6.000 MB e deixa 2.000 MB para outros usos.

sem_wait(&processing_slots);
run_memory_heavy_phase();
sem_post(&processing_slots);

Todo caminho após um wait bem-sucedido deve devolver sua permissão, incluindo um caminho de erro. Esquecer um post reduz gradualmente a capacidade. Executar post duas vezes cria uma permissão extra e permite mais usuários simultâneos que o previsto.

Encontre-se em um ponto de encontro

Um ponto de encontro exige que duas threads cheguem antes que qualquer uma continue. Inicialize a_arrived e b_arrived em 0.

/* A */
sem_post(&a_arrived);
sem_wait(&b_arrived);

/* B */
sem_post(&b_arrived);
sem_wait(&a_arrived);

Se A chegar primeiro, ela registra sua chegada e bloqueia esperando por B. B registra sua chegada e pode consumir a permissão guardada de A. A pode então consumir a permissão de B. Inverter ambas as sequências para executar wait antes de post deixa cada thread dormindo antes de anunciar qualquer coisa.

Esses dois semáforos implementam um encontro. Reutilizar a sincronização em rodadas repetidas exige impedir que as rodadas consumam as chegadas umas das outras; um acompanhamento bem-sucedido não constitui um projeto de barreira reutilizável.

Um semáforo é adequado para uma contagem ou um evento simples. Uma variável de condição é adequada para um predicado mais elaborado, como esperar até que uma fila conter vários itens de um tipo necessário. Nesse caso, o mutex protege o estado a partir do qual a condição é calculada.

Principais conclusões

  • Inicie um semáforo de ordenação em 0 e execute post somente após o evento que ele representa.
  • Um semáforo contador limita a admissão, mas não protege a lista de recursos.
  • A limitação de concorrência restringe a demanda simultânea por memória ou outro recurso limitado.
  • Para um ponto de encontro de duas threads, anuncie a chegada antes de esperar pela outra thread.
  • Um post extra cria capacidade; a falta de um post reduz a capacidade.

2.3 Reserve uma posição antes de adquirir a trava do buffer

Um buffer com semáforos separa duas perguntas: “Há um item ou uma posição disponível?” e “Quem pode atualizar o array agora?” Dois semáforos contadores respondem à primeira pergunta, e um mutex responde à segunda.

Configure três controles distintos

Para a capacidade MAX, inicialize empty em MAX, full em 0 e o mutex como não adquirido. Aqui, empty e full são semáforos, não variáveis de condição. Uma permissão de empty reserva uma posição; uma permissão de full reserva um item.

/* Producer */
sem_wait(&empty);
lock(&mutex);
put(v);
unlock(&mutex);
sem_post(&full);

/* Consumer */
sem_wait(&full);
lock(&mutex);
v = get();
unlock(&mutex);
sem_post(&empty);

O produtor adquire espaço antes de acessar o buffer circular e depois disponibiliza o item concluído com uma operação post em full. O consumidor adquire um item, remove-o sob a proteção do mutex e devolve espaço com uma operação post em empty. Uma operação wait bem-sucedida no semáforo já reserva o recurso, portanto essas chamadas não precisam de um laço para verificar o predicado, como no uso de variáveis de condição.

Acompanhe uma posição

Inicialmente, empty é 1 e full é 0. Um consumidor executa wait em full, alterando-o para −1 no modelo com sinal, e dorme. O produtor executa wait em empty, passando-o de 1 para 0. Ele adquire o mutex, insere o item, libera o mutex e executa post em full, passando-o de −1 para 0 e liberando o consumidor.

O próximo wait do produtor em empty altera 0 para −1 e bloqueia. O consumidor retira o item sob a proteção do mutex e executa post em empty, passando-o de −1 para 0 e liberando o produtor. As contagens dos semáforos realizam a passagem; não é necessário um count separado para sincronização.

Com um buffer maior, vários produtores podem passar pelo semáforo de espaços livres. Sem o mutex, ambos poderiam ler o mesmo fill_ptr e escrever na mesma posição. Uma reserva não é acesso exclusivo aos índices do array.

Por que adquirir o mutex primeiro pode causar impasse

Suponha que o consumidor adquira mutex antes de executar wait em full. Com o buffer vazio, ele dorme ainda mantendo o mutex adquirido. O produtor precisa desse mutex para inserir o item cujo post liberaria o consumidor. Nenhum dos dois consegue terminar.

Uma espera em semáforo não libera nenhum mutex. Uma espera em variável de condição libera deliberadamente seu mutex associado. Portanto, a ordem nesse buffer importa: primeiro reserve, depois adquira o mutex, altere o buffer, libere o mutex e então disponibilize a permissão correspondente ao papel oposto.

A regra mais ampla é evitar bloquear enquanto mantém uma trava necessária à operação que pode desbloquear você. Essa regra explica por que o algoritmo de leitores e escritores pode ter uma organização segura diferente: seu escritor não precisa da trava do contador de leitores para liberar seu recurso.

O próprio mutex pode ser um semáforo binário inicializado em 1. Sua função continua sendo a exclusão mútua; os outros dois semáforos contam recursos.

Principais conclusões

  • Inicialize empty com a capacidade, full em 0 e o mutex como não adquirido.
  • Reserve espaço ou um item antes de adquirir o mutex do buffer.
  • Proteja os índices circulares e as atualizações do array mesmo quando as permissões são contadas corretamente.
  • Esperar em um semáforo não libera nenhum mutex.
  • Execute post em full após inserir; execute post em empty após remover.
  • Uma thread bloqueada em espera não deve manter adquirida uma trava de que a thread que pode desbloqueá-la precisa.

2.4 Permita que leitores compartilhem e escritores trabalhem sozinhos

Estruturas com muitas leituras podem permitir vários leitores ao mesmo tempo porque a leitura não altera os dados. Uma trava de leitores e escritores admite um grupo de leitores ou um escritor. Toda alteração nos dados protegidos exige acesso de escrita.

Forme o grupo de leitores

Use readers = 0, um semáforo lock inicializado em 1 para proteger esse contador e writelock inicializado em 1 para proteger o recurso compartilhado.

/* Acquire read access */
sem_wait(&lock);
readers++;
if (readers == 1)
    sem_wait(&writelock);
sem_post(&lock);

/* Release read access */
sem_wait(&lock);
readers--;
if (readers == 0)
    sem_post(&writelock);
sem_post(&lock);

Um escritor executa wait em writelock, altera os dados e então executa post em writelock. Ele não adquire a trava do contador. O primeiro leitor adquire writelock para o grupo, e o último leitor o devolve. Um semáforo é adequado a essa passagem porque a thread que executa post não precisa ser a mesma que executou wait originalmente.

Acompanhe a entrada de R1: ela aumenta readers para 1 e adquire writelock. R2 aumenta a contagem para 2 sem adquirir outra permissão. O escritor W agora bloqueia em writelock. R1 sai e reduz a contagem para 1. R2 sai, reduz a contagem para 0 e executa post, permitindo que W escreva sozinho.

O primeiro leitor pode esperar por writelock enquanto mantém lock adquirido. Isso não cria o impasse do buffer limitado: o escritor atual não precisa da trava do contador para terminar e executar post em writelock.

Justiça é uma propriedade separada

Esse projeto dá preferência aos leitores. Enquanto a contagem de leitores permanecer positiva, outro leitor poderá entrar no grupo à frente de um escritor em espera. Um fluxo contínuo de leitores com períodos de leitura sobrepostos pode manter o escritor esperando indefinidamente: inanição de escritores.

PolíticaEscolha de admissãoRisco
Preferência por leitoresNovos leitores podem se juntar aos leitores ativos.Escritores podem sofrer inanição.
Preferência por escritoresEscritores em espera impedem a entrada de novos leitores.Leitores podem sofrer inanição.
Admissão justaThreads que chegam recebem acesso em uma ordem justa.Mais controle de admissão.

O projeto básico fornece segurança, mas não limita o tempo que um escritor espera. Maior vazão de leitores não é o mesmo que atendimento justo.

Use o modo correto

Uma trava de leitura não protege uma escrita de outros leitores. Atualizar até mesmo um contador de cache com acesso de leitura pode causar uma corrida com outro leitor. Para converter a trava nesse projeto básico, libere o acesso de leitura, adquira acesso de escrita e verifique novamente a condição. Esperar por acesso de escrita enquanto ainda é contado como leitor pode fazer a thread esperar por si mesma para sempre.

O POSIX fornece pthread_rwlock_t, pthread_rwlock_rdlock(), pthread_rwlock_wrlock() e pthread_rwlock_unlock(). O Java oferece ReentrantReadWriteLock; o C++17 oferece std::shared_mutex. Essas travas exigem mais controle do que um mutex comum. Meça a carga de trabalho: seções curtas ou escritas frequentes podem fazer um mutex simples ser tão rápido quanto ou mais rápido.

Principais conclusões

  • Uma trava de leitores e escritores permite muitos leitores ou um escritor exclusivo.
  • O primeiro leitor adquire a permissão do grupo; o último leitor a devolve.
  • O contador de leitores precisa de sua própria trava.
  • A preferência por leitores pode causar inanição de um escritor em espera.
  • Libere o acesso de leitura antes de solicitar acesso de escrita e então verifique novamente os dados.
3

Encontre a suposição de ordem por trás de um erro

Muitas falhas de concorrência permitem que o programa continue executando com dados incorretos. Duas causas recorrentes são tratar vários acessos como uma ação indivisível e supor que o trabalho de uma thread precede o de outra. Acompanhar alguns acessos escolhidos com cuidado costuma revelar a suposição oculta. A correção depende dessa suposição: proteja uma operação inteira quando ela precisar permanecer indivisível e estabeleça uma dependência explícita quando uma operação precisar acontecer primeiro.

3.1 Proteja toda a verificação e ação

Uma violação de atomicidade ocorre quando o código depende de um grupo de acessos ser indivisível, mas outra thread pode agir entre eles. Cada acesso individual pode parecer razoável, enquanto a operação combinada falha.

Encontre o intervalo após a verificação

Considere um buffer de registro compartilhado:

/* T1 */
if (log->buf != NULL)
    write_out(log->buf);

/* T2 */
log->buf = NULL;

T1 pode verificar um ponteiro não nulo, pausar e retomar após T2 defini-lo como nulo. T1 então passa um valor nulo a write_out(). A garantia pretendida não era apenas uma verificação válida; era que o valor verificado permanecesse utilizável até a chamada.

Proteja a verificação e o uso juntos e proteja a atribuição concorrente com o mesmo mutex:

/* T1 */
lock(&m);
if (log->buf != NULL)
    write_out(log->buf);
unlock(&m);

/* T2 */
lock(&m);
log->buf = NULL;
unlock(&m);

T2 agora executa antes ou depois de toda a operação de T1. Dar a cada thread sua própria trava não ajuda: ambas poderiam manter suas travas separadas adquiridas simultaneamente. Proteger apenas T1 deixa T2 livre para agir no intervalo.

Um teste válido ainda pode levar a um saldo inválido

Comece com saldo 50. Duas threads pretendem sacar 30 cada uma somente se houver saldo suficiente. Ambas podem ler 50 e passar por balance >= 30 antes que qualquer uma aja. Se os saques forem então aplicados, o saldo pode chegar a −10. Proteger o teste e a subtração juntos permite que o primeiro saque deixe 20; a próxima thread vê 20 e recusa o saque.

Mesmo leituras e subtrações atômicas não tornam esse par uma transação. Outra thread pode agir entre a verificação atômica e a alteração atômica. Uma trava em torno de toda a decisão e alteração garante o limite necessário.

O mesmo problema aparece em count++: leitura, soma e escrita são etapas separadas. Duas threads podem ler o mesmo valor antigo e armazenar o mesmo valor incrementado, perdendo uma atualização. Uma trava em torno do incremento ou uma instrução atômica de busca e soma realiza a atualização necessária com segurança.

Use evidências sem generalizar demais

Lu e colaboradores examinaram 105 erros de concorrência corrigidos em MySQL, Apache, Mozilla e OpenOffice em um estudo de 2008. Desses, 74 eram erros sem impasse e 31 eram impasses. Violações de atomicidade e de ordem correspondiam a 97% da amostra de erros sem impasse. Essas são conclusões sobre a amostra estudada, não uma porcentagem garantida para toda base de código.

Cerca de 92% dos erros estudados podiam ser provocados controlando a ordem de no máximo quatro acessos à memória. Uma pequena tabela de intercalação pode, portanto, ser mais reveladora do que milhares de execuções comuns bem-sucedidas. O ThreadSanitizer ajuda a encontrar corridas de dados executadas, mas um erro lógico de atomicidade pode permanecer mesmo quando cada acesso individual é sincronizado. Teste a propriedade invariante de toda a operação, como “um saque bem-sucedido nunca deixa o saldo negativo”.

Principais conclusões

  • Defina a seção crítica em torno de toda a operação que deve ser indivisível.
  • Todo acesso concorrente deve seguir o mesmo protocolo de travamento.
  • Acessos individuais atômicos ainda podem deixar um intervalo entre a verificação e a ação.
  • Acompanhe uma pequena intercalação prejudicial em vez de confiar apenas em execuções bem-sucedidas repetidas.

3.2 Faça uma operação acontecer antes de outra

Uma violação de ordem significa que a operação A deve acontecer antes da operação B, mas o programa não fornece sincronização que garanta isso. A exclusão mútua sozinha não determina qual operação ocorre primeiro.

Uma thread pode executar imediatamente

Suponha que uma thread criadora armazene um registro recém-criado de uma thread trabalhadora em um ponteiro compartilhado, enquanto a trabalhadora começa usando esse ponteiro:

/* Creator */
worker = thread_create(worker_main);

/* New worker */
use(worker->state);

A trabalhadora pode executar antes de thread_create() retornar e antes de a atribuição terminar. A ordem no código-fonte da criadora não diz nada sobre quando a nova thread receberá tempo de CPU. Uma execução bem-sucedida significa apenas que a criadora venceu essa corrida por acaso.

Sinalize a prontidão após a inicialização

Acrescente ready = 0, o mutex m e a variável de condição c. A criadora mantém o mutex adquirido enquanto atribui o ponteiro e sinaliza a prontidão:

/* Creator */
lock(&m);
worker = thread_create(worker_main);
ready = 1;
signal(&c);
unlock(&m);

/* Worker, before using worker */
lock(&m);
while (ready == 0)
    wait(&c, &m);
unlock(&m);
use(worker->state);

Se a trabalhadora chegar primeiro ao mutex, ela não poderá passar pelo teste de prontidão até que a criadora tenha disponibilizado o registro. Se chegar depois, ready registra que a inicialização terminou. Mantenha o registro válido enquanto a trabalhadora o usa e sincronize quaisquer alterações posteriores.

Um semáforo inicializado em 0 oferece uma passagem mais curta: a criadora executa post após atribuir o ponteiro, e a trabalhadora executa wait antes de usá-lo. Para um resultado que só existe depois que uma thread termina, aguardar seu término antes de ler o resultado estabelece a ordem necessária. A liberação de um objeto também deve esperar até que seus usuários terminem, por exemplo aguardando o término das threads ou usando uma contagem de referências corretamente sincronizada.

Por que mudanças no tempo de execução não corrigem o problema

Acrescentar sleep(1) ou sched_yield() apenas muda a probabilidade do escalonamento desejado. Um sistema mais lento ou sobrecarregado ainda pode permitir que o consumidor prossiga cedo demais. Um mutex em torno de cada operação impede a sobreposição, mas ainda permite que o uso adquira o mutex antes da inicialização.

Suposição ocultaTipo de erroGarantia necessária
“Nada muda entre esta verificação e a ação.”Violação de atomicidadeUma operação protegida.
“A inicialização termina antes deste uso.”Violação de ordemUma dependência explícita de conclusão ou prontidão.

No estudo de Lu e colaboradores, cerca de um terço dos erros sem impasse eram violações de ordem, e cerca de 73% dos erros sem impasse não foram corrigidos apenas acrescentando ou alterando travas. Antes de escolher uma primitiva, identifique a operação que deve terminar e a operação que deve esperar. Essa dependência determina onde colocar post, signal ou a espera pelo término.

Principais conclusões

  • Uma violação de ordem supõe que uma ação anterior terminou sem garantir essa ordem.
  • Sinalize a prontidão somente após a inicialização e espere antes de usar o resultado.
  • Uma trava impede a sobreposição, mas não determina quem executa primeiro.
  • Sleep e yield mudam os tempos de execução sem garantir uma dependência.
  • Mantenha objetos compartilhados válidos até que todos os usuários terminem.
4

Analise impasses e progresso

Um impasse transforma um conjunto de esperas comuns em uma parada permanente: cada thread precisa de algo que outra thread bloqueada mantém. Um grafo de dependências torna esse ciclo visível. As quatro condições necessárias sugerem formas de preveni-lo; declarações prévias de recursos permitem evitá-lo; e um detector pode encontrar um impasse depois que ele se forma. Compare essas estratégias pelo conhecimento de que precisam, pela concorrência que custam e pelo trabalho que pode ser perdido na recuperação.

4.1 Acompanhe o ciclo de threads em espera

Um conjunto de threads está em impasse quando cada uma espera por algo que somente outra thread do conjunto pode fornecer. Nenhuma delas consegue continuar e realizar a ação que libera as demais. O programa pode funcionar em muitos escalonamentos até que uma ordem desfavorável revele o ciclo.

Construa a falha com duas travas

T1 adquire L1 e em seguida solicita L2. T2 adquire L2 e em seguida solicita L1. Se T1 adquirir L1 e depois T2 adquirir L2, ambas as segundas aquisições bloqueiam. Cada thread mantém exatamente o recurso de que a outra precisa.

Isso pode ficar oculto em funções comuns. Se transfer(a, b) adquire a trava de a e depois a de b, chamadas simultâneas a transfer(x, y) e transfer(y, x) podem adquirir as travas das contas em ordens opostas. O encapsulamento torna uma chamada conveniente, mas pode ocultar as travas que ela adquire.

Identifique as quatro condições
Condição necessáriaSignificado no exemplo de travas
Exclusão mútuaApenas uma thread mantém uma determinada trava adquirida.
Posse e esperaCada uma mantém uma trava enquanto espera por outra.
Ausência de preempçãoUma trava adquirida não é retirada à força.
Espera circularCada uma espera pela trava mantida pela próxima thread.

As quatro condições são necessárias. Eliminar qualquer uma delas no projeto previne impasses por recursos. A presença de travas exclusivas e aquisições aninhadas, por si só, não estabelece que uma determinada execução chegou a um ciclo.

Interprete um grafo de espera

Desenhe um nó por thread. Desenhe T → U quando T esperar por um recurso mantido por U. Com recursos de instância única, como travas exclusivas, um ciclo identifica um impasse.

Para T1 mantendo A e esperando por B, T2 mantendo B e esperando por C e T3 mantendo C e esperando por A, o grafo é T1 → T2 → T3 → T1. As três estão presas. Se T3 solicitar uma trava livre D, poderá terminar e liberar C, permitindo que T2 e depois T1 prossigam. Uma cadeia não é um ciclo.

Quando tipos de recursos têm várias instâncias intercambiáveis, um ciclo sozinho é insuficiente: uma instância adicional pode ficar disponível. Use as contagens de recursos e as requisições pendentes para avaliar esse caso.

Jantar dos filósofos e falhas relacionadas

Cinco filósofos compartilham cinco garfos dispostos em círculo. Cada um precisa dos dois garfos vizinhos. Se todos pegarem primeiro o garfo à esquerda, todos poderão esperar para sempre pelo garfo à direita. Inverter a ordem de aquisição de um filósofo rompe o ciclo. Limitar a admissão a quatro filósofos também impede que os cinco mantenham um garfo cada; pelo menos um filósofo admitido consegue obter os dois garfos.

Inanição significa que uma thread espera indefinidamente enquanto outras progridem. Bloqueio ativo significa que threads executam e reagem, mas falham repetidamente em progredir. Um conjunto em impasse está bloqueado. Uma thread pode até causar um impasse consigo mesma ao adquirir um mutex normal, não recursivo, que já mantém adquirido. Semáforos e variáveis de condição também podem criar dependências: uma thread em espera que mantém uma trava necessária à thread que a sinaliza pode fechar um ciclo sem uma segunda aquisição explícita de trava.

Principais conclusões

  • Um impasse é um ciclo permanente de dependências entre threads bloqueadas.
  • As quatro condições devem ocorrer: exclusão, posse e espera, ausência de preempção e espera circular.
  • Para recursos de instância única, um ciclo no grafo de espera identifica um impasse.
  • Um ciclo com recursos de múltiplas instâncias exige análise adicional.
  • Inanição, bloqueio ativo e impasse descrevem falhas de progresso diferentes.
  • Travas ocultas dentro de funções podem inverter a ordem de aquisição.

4.2 Elimine uma condição que permite o impasse

A prevenção altera o projeto para que uma condição necessária ao impasse não possa surgir. A ordenação de aquisição de travas costuma ser a abordagem mais clara porque ataca diretamente a dependência circular.

Defina uma ordem para todas as travas

Atribua chaves únicas às travas e sempre adquira as de chaves menores antes das de chaves maiores. Para transferências entre contas, ordene os identificadores das contas antes de adquirir as travas. Assim, tanto transfer(x, y) quanto transfer(y, x) adquirem primeiro a trava da mesma conta. Se os identificadores indicarem a mesma conta, adquira sua trava uma vez.

Um ciclo exigiria que as chaves continuassem aumentando ao redor do círculo e retornassem à chave inicial, o que é impossível. Todo caminho de aquisição deve seguir a regra. Uma única função auxiliar que inverta a ordem traz o risco de volta. O lockdep do Linux acompanha as dependências de travamento observadas e informa ordens potencialmente inseguras.

Adquira tudo ou não adquira nada

Uma forma estrita de eliminar posse e espera é conceder a uma thread seu conjunto completo de recursos em uma etapa ou não conceder nada. Ela nunca mantém um conjunto parcial enquanto espera pelo restante. Isso exige conhecer os recursos necessários antecipadamente e pode reduzir a concorrência.

Um protocolo relacionado serializa sequências de aquisição com uma trava global de prevenção:

lock(&prevention);
lock(&L1);
lock(&L2);
unlock(&prevention);
/* use L1 and L2, then release them */

Todas as threads devem terminar de adquirir seu conjunto completo sob essa trava global, e a liberação de recursos não deve exigir sua aquisição. Uma thread ainda pode manter L1 enquanto espera por L2, portanto isso não elimina literalmente a posse e espera. O protocolo impede que outras threads iniciem uma aquisição parcial concorrente. Nenhuma thread que mantenha recursos pode estar no meio de uma segunda sequência de aquisição concorrente. Os custos são manter travas adquiridas mais cedo e serializar a aquisição.

Desista e tente novamente

pthread_mutex_trylock() retorna imediatamente com EBUSY quando uma trava está indisponível. Uma thread pode adquirir L1, tentar L2 e liberar L1 se a tentativa falhar. Ela também deve desfazer outras aquisições parciais antes de tentar novamente.

Duas threads podem colidir repetidamente, recuar e tentar novamente juntas. Isso é bloqueio ativo: ambas executam, mas nenhuma termina. A espera aleatória antes de nova tentativa reduz colisões repetidas; ela não transforma o laço de tentativas em uma garantia de justiça.

Substitua uma trava por uma operação atômica

A comparação e troca (CAS) altera uma posição apenas se ela ainda corresponder a um valor esperado. Conceitualmente, um laço de nova tentativa para soma atômica lê o valor antigo, tenta substituí-lo pelo valor antigo mais o incremento e tenta novamente se outra thread o tiver alterado primeiro. Implementações reais em C usam tipos e operações atômicas tanto para a leitura quanto para a atualização.

Eliminar a aquisição de travas elimina o impasse por travas dessa operação. Um algoritmo livre de travas garante progresso para o sistema como um todo, mas uma thread pode continuar falhando em suas tentativas. Estruturas gerais livres de travas também precisam de um projeto cuidadoso do tempo de vida da memória.

TécnicaPrincipal compensação
Aquisição ordenadaTodo caminho do código deve obedecer à ordem.
Aquisição simultâneaOs recursos devem ser conhecidos e podem ficar sem uso.
Trylock e nova tentativaO trabalho de repetição e o bloqueio ativo exigem atenção.
Atualização livre de travasA análise de correção é mais exigente.

Principais conclusões

  • Uma ordem comum de travas previne a espera circular somente quando todos os caminhos a seguem.
  • A aquisição integral ou nenhuma aquisição elimina a posse e espera.
  • Uma trava global de aquisição serializa sequências completas de aquisição.
  • Após uma tentativa de trylock malsucedida, libere os recursos adquiridos parcialmente antes de tentar novamente.
  • A espera aleatória antes de nova tentativa reduz colisões por bloqueio ativo.
  • O progresso livre de travas para o sistema não garante progresso para cada thread.

4.3 Teste a segurança de uma requisição de recursos

A evitação verifica se uma requisição proposta deixa uma forma de todas as threads terminarem. Ela permite os padrões usuais de uso de recursos, mas adia requisições que eliminem as ordens restantes de conclusão segura.

Um estado seguro garante uma ordem possível

Uma sequência segura ordena as threads de modo que cada uma possa obter sua demanda máxima restante com os recursos livres no momento mais os liberados pelas threads anteriores. Um estado inseguro não tem essa ordem garantida. Ele não precisa já estar em impasse: as threads podem solicitar menos que seu máximo ou liberar recursos antecipadamente.

O escalonamento também pode evitar uma sobreposição perigosa ao garantir que duas tarefas conflitantes não entrem juntas em suas fases de uso de recursos. Simplesmente colocar ambas em uma CPU é insuficiente, pois elas ainda podem sofrer preempção e se intercalar enquanto mantêm recursos. A garantia necessária é a ausência de sobreposição das fases conflitantes.

Prepare as tabelas do banqueiro

Para cada tipo de recurso, acompanhe Available. Para cada thread, acompanhe Max, Allocation e Need = Max - Allocation. Compare vetores componente por componente, nunca pela soma de seus totais.

Use os totais de recursos (7, 5, 6):

ThreadAllocationMaxNeed
T0(1, 1, 0)(5, 3, 2)(4, 2, 2)
T1(2, 0, 1)(3, 2, 2)(1, 2, 1)
T2(1, 1, 2)(2, 2, 4)(1, 1, 2)
T3(0, 1, 1)(4, 2, 3)(4, 1, 2)

As alocações somam (4, 3, 4), deixando Available = (3, 2, 2).

Acompanhe o teste de segurança

Defina Work como Available. Escolha uma thread não concluída cuja Need caiba em Work, simule sua conclusão e devolva sua Allocation a Work.

  1. T1 cabe em (3, 2, 2). Devolver (2, 0, 1) resulta em Work (5, 2, 3).
  2. T0 cabe. Devolver (1, 1, 0) resulta em (6, 3, 3).
  3. T2 cabe. Devolver (1, 1, 2) resulta em (7, 4, 5).
  4. T3 cabe. Devolver (0, 1, 1) resulta em (7, 5, 6).

Todas as threads terminam nessa simulação, portanto o estado é seguro. Work aumenta em Allocation, não em Max: os recursos extras temporariamente concedidos para a conclusão também são devolvidos, resultando em um ganho líquido igual ao que a thread mantinha originalmente.

Simule a concessão e teste novamente

Uma requisição deve caber tanto em Need da thread quanto em Available. Então subtraia-a de Available, some-a a Allocation, subtraia-a de Need e execute o teste de segurança nesse estado provisório.

Na tabela original, T2 solicitar (1, 0, 1) deixa Available em (2, 2, 1). T1 ainda pode ir primeiro, seguida por T0, T2 e T3, portanto é seguro.

Já T3 solicitar (2, 1, 1) deixa Available em (1, 1, 1). A Need restante de T3 é (2, 0, 1). Nenhuma thread cabe: T0 precisa de 4 de A, T1 precisa de 2 de B, T2 precisa de 2 de C e T3 precisa de 2 de A. Adie essa requisição apesar dos recursos livres no momento.

Toda requisição posterior precisa do mesmo teste. A evitação exige demandas máximas declaradas e um modelo de recursos conhecido, tornando-a mais adequada a sistemas especializados previsíveis do que a cargas de trabalho de aplicações gerais.

Principais conclusões

  • Need é igual à demanda máxima menos a alocação atual.
  • Uma sequência segura deve satisfazer cada tipo de recurso separadamente.
  • Após simular a conclusão, some Allocation a Work.
  • Uma requisição deve caber em Need, caber em Available e manter um estado seguro.
  • Inseguro significa que não há uma sequência de conclusão garantida; não significa que um impasse já se formou.

4.4 Detecte um impasse e escolha uma recuperação

Um sistema pode permitir que um impasse se forme, detectá-lo e se recuperar. Isso aceita a perda ocasional de trabalho em troca de menos restrições às requisições comuns de recursos. A detecção só é útil quando existe uma ação de recuperação viável.

Encontre ciclos de bloqueio

Para recursos de instância única, mantenha um grafo de espera e procure um ciclo, por exemplo com uma busca em profundidade. Suponha que as arestas sejam T1 → T2, T2 → T3, T3 → T1 e T4 → T2. T1, T2 e T3 formam o ciclo. T4 está bloqueada atrás dele; a própria execução de T4 não pode liberar um recurso que rompa esse ciclo.

Com múltiplas instâncias, um ciclo é insuficiente. Use Available, Allocation e a requisição atual pendente Request de cada thread. Inicie Work com Available. Encontre repetidamente uma requisição que caiba, simule a conclusão e some a Allocation dessa thread a Work. Diferentemente da evitação, esse teste usa aquilo pelo que as threads estão esperando agora, não sua possível demanda máxima futura.

O detector padrão de recursos inicialmente separa as threads que não mantêm recursos: elas não podem contribuir com recursos mantidos para um ciclo de recursos. Entre as threads que mantêm recursos, as que permanecem não concluídas pelo teste estão em impasse segundo o modelo. Outras threads ainda podem estar impossibilitadas de progredir porque dependem desse conjunto. Manter o ciclo separado das threads bloqueadas que dependem dele ajuda a escolher uma vítima eficaz para a recuperação.

Recupere sem corromper o estado compartilhado

Um sistema pode encerrar um ou mais processos, recuperar recursos com reversão a um ponto de verificação ou reiniciar. Escolha vítimas pelo custo de recuperação, prioridade, trabalho já realizado e recursos mantidos. Após remover uma vítima, verifique novamente: vários ciclos independentes podem exigir mais de uma ação de recuperação.

Encerrar uma thread arbitrária não basta se ela deixar um mutex adquirido ou uma estrutura compartilhada parcialmente alterada. A recuperação deve liberar ou reconstruir recursos e restaurar um estado válido. Sistemas de transações podem abortar uma transação em impasse porque a reversão faz parte de seu projeto. Uma aplicação comum muitas vezes precisa de uma reinicialização controlada do processo.

Interprete tempos limite com cuidado

pthread_mutex_timedlock() pode retornar ETIMEDOUT após um prazo. Um temporizador de supervisão pode reiniciar um dispositivo que deixa de informar progresso. Ambos detectam espera excessiva; uma espera longa também pode resultar de entrada e saída lenta ou de escalonamento. Um tempo limite é uma evidência para investigar ou recuperar o sistema, não um diagnóstico exclusivo de um ciclo.

EstratégiaO que exigePrincipal custo
PrevençãoUma regra de projeto que elimine uma condição necessária.Menor flexibilidade ou concorrência.
EvitaçãoDemandas máximas futuras e uma verificação de segurança.Requisições adiadas e controle adicional.
Detecção e recuperaçãoUm detector e um caminho de recuperação válido.Custo de verificação e possível perda de trabalho.
Ignorar impasses rarosUm custo de falha e reinicialização tolerável.Interrupções ocasionais.

Linux e Windows em geral não encontram nem reparam ciclos entre as próprias travas de uma aplicação. O programa deve implementar a estratégia escolhida. Se a recuperação selecionar repetidamente o mesmo trabalho como vítima, esse trabalho poderá sofrer inanição mesmo que cada impasse individual seja desfeito.

Principais conclusões

  • A detecção encontra um impasse existente; a prevenção impede sua formação.
  • A detecção com múltiplas instâncias usa requisições atuais, não a Need máxima.
  • Diferencie um ciclo de recursos das threads bloqueadas que dependem dele.
  • A recuperação deve restaurar recursos e estado compartilhado válidos, não apenas parar uma thread.
  • Um tempo limite também pode indicar uma operação lenta.
5

Mantenha um laço de eventos responsivo

A concorrência pode vir de uma thread atendendo muitas tarefas enquanto suas esperas por entrada e saída se sobrepõem. Um laço de eventos escolhe uma tarefa pronta e executa um tratador curto, retornando depois à espera pelo próximo evento. Isso elimina a necessidade de travas entre tratadores síncronos nesse laço, mas faz cada operação bloqueante atrasar todos. Verificações de prontidão, entrada e saída assíncrona e continuações salvas mantêm o laço em andamento; múltiplos núcleos e estado compartilhado ainda exigem cuidado.

5.1 Trate descritores prontos sem bloquear

Um laço de eventos espera por eventos e despacha tratadores curtos em uma thread. O programa escolhe explicitamente qual evento pronto tratar em seguida. Node.js e nginx usam projetos baseados em eventos para atender muitas conexões sem atribuir uma thread dedicada a cada conexão.

while (1) {
    events = get_events();
    /* handle each ready event, then return to the loop */
}

Tratadores síncronos no mesmo laço executam um por vez. As instruções comuns de um tratador não são interrompidas por outro tratador nesse laço. O laço ainda depende de os tratadores retornarem rapidamente.

Construa e inspecione conjuntos para select

select() aceita conjuntos de descritores a observar para leitura, escrita e condições excepcionais, além de um tempo limite. Seu argumento nfds é um a mais que o maior descritor observado. Observar os descritores 3, 5 e 9 exige, portanto, nfds = 10, não 3.

FD_ZERO(&readset);
FD_SET(3, &readset);
FD_SET(5, &readset);
FD_SET(9, &readset);
int n = select(10, &readset, NULL, NULL, NULL);
/* If n > 0, test each watched descriptor with FD_ISSET. */

Um tempo limite nulo espera pela prontidão; um tempo limite zero verifica e retorna imediatamente. select() sobrescreve os conjuntos com os descritores prontos, portanto reconstrua-os antes de cada chamada. Seu retorno positivo conta os bits definidos nos conjuntos retornados; um descritor pronto em dois conjuntos conta duas vezes. Zero significa tempo limite; −1 significa erro.

Prontidão não é transferência de dados. O programa ainda chama read() ou recv(). O fim da entrada pode aparecer como prontidão para leitura; uma leitura que retorna 0 significa que a outra ponta fechou seu lado de envio. Trate esse resultado em vez de despachar repetidamente a mesma entrada encerrada.

Mantenha os tratadores curtos e não bloqueantes

Configure os sockets com O_NONBLOCK. Quando uma operação não pode prosseguir, ela retorna −1 com errno definido como EAGAIN ou EWOULDBLOCK, permitindo que o tratador retorne ao laço. A prontidão pode mudar antes da operação, portanto o modo não bloqueante também protege contra uma espera inesperada após uma indicação de prontidão.

Uma leitura bloqueante dentro de um tratador atrasa todos os eventos na fila. Com 100 requisições enfileiradas e uma leitura bloqueante de disco de 10 ms por tratador, a última requisição espera cerca de 990 ms antes de sua própria leitura começar; o lote leva cerca de 1.000 ms. A thread passou a maior parte desse tempo esperando enquanto trabalho pronto permanecia na fila.

Escolha uma interface de prontidão
InterfaceComo o conjunto observado é representadoPropriedade importante
select()Conjuntos de bits de descritores reconstruídos a cada chamada.Os valores dos descritores devem caber em FD_SETSIZE, normalmente 1024.
poll()Um array de descritores e eventos solicitados/retornados.Não há limite fixo correspondente para valores de descritores.
epoll / kqueueUm conjunto registrado de interesses mantido pelo núcleo.Entrega eficiente de eventos para grandes conjuntos observados.

select() e poll() examinam o conjunto observado fornecido em cada chamada, portanto seu trabalho cresce com o tamanho dele. O Linux oferece epoll; FreeBSD e macOS oferecem kqueue. Observar apenas alguns descritores não torna seguro um valor alto de descritor para um fd_set de tamanho fixo.

Principais conclusões

  • Um laço despacha tratadores síncronos um por vez.
  • Para select, nfds é o maior descritor observado mais um.
  • Reconstrua os conjuntos de select após cada chamada.
  • A prontidão exige uma chamada posterior de entrada e saída; uma leitura que retorna 0 significa fim da entrada.
  • Sockets não bloqueantes permitem que tratadores retornem quando uma operação teria de esperar.
  • FD_SETSIZE restringe os valores dos descritores, não apenas quantos são observados.

5.2 Salve o trabalho que continua após a entrada e saída

A prontidão de rede informa a um laço quando ele pode tentar uma operação de entrada e saída. Arquivos regulares em disco são diferentes: select() e poll() os informam como prontos mesmo quando o acesso a seus dados ainda espera pelo armazenamento. Para continuar atendendo outros eventos, submeta o trabalho de forma assíncrona e trate seu resultado depois.

Submeta, faça outro trabalho e obtenha o resultado

A entrada e saída assíncrona POSIX armazena uma requisição em struct aiocb. Campos importantes são aio_fildes para o descritor de arquivo, aio_offset para a posição no arquivo, aio_buf para o destino e aio_nbytes para o comprimento solicitado.

/* cb describes a read request */
aio_read(&cb);
/* Return to other work. Later: */
int status = aio_error(&cb);
/* EINPROGRESS: still running; 0: succeeded; otherwise an error. */
/* After completion, aio_return(&cb) collects its result. */

aio_read() enfileira a requisição e retorna. A conclusão é um evento separado. Quando aio_error() indica conclusão, aio_return() fornece o resultado, como os bytes lidos após uma operação bem-sucedida. Uma requisição pode falhar, e uma leitura bem-sucedida pode retornar menos bytes que o solicitado. O registro da requisição e seu buffer devem permanecer válidos enquanto a operação estiver pendente.

Examinar repetidamente cada requisição em um laço contínuo desperdiça tempo de CPU. aio_sigevent pode configurar um sinal de conclusão ou uma chamada de retorno. O io_uring do Linux usa filas de submissão e conclusão; as portas de conclusão de entrada e saída do Windows fornecem despacho baseado em conclusão. Essas interfaces permitem que o programa aja sobre o trabalho concluído sem verificar constantemente cada requisição não concluída.

Preserve o estado durante o intervalo

Uma thread bloqueada mantém as variáveis locais de sua função na pilha. Um tratador de eventos que submete uma operação de entrada e saída e retorna deve salvar o que o tratador posterior precisa. Esse controle explícito é o gerenciamento manual da pilha; o estado salvo é uma continuação.

Suponha que um servidor leia o descritor de arquivo 12 para um cliente no socket 7. Na submissão, ele salva um registro que associa essa requisição de leitura ao socket 7 e ao estado de sua resposta. Quando a conclusão chega, o laço encontra o registro, verifica o resultado da leitura e continua enviando a resposta ao cliente correto. Registros separados impedem que várias requisições pendentes confundam seus destinos.

A continuação deve estar vinculada à operação específica, não apenas a um identificador que possa ser reutilizado depois. Mantenha a referência ao cliente válida até que a conclusão seja tratada e remova o registro quando a tarefa terminar. Esses detalhes de tempo de vida substituem suposições que uma função bloqueante de execução sequencial poderia manter em sua pilha.

Uma sintaxe legível pode ocultar uma suspensão

Chamadas de retorno, promessas e async/await automatizam grande parte desse salvamento de estado. Em JavaScript, const data = await readFile(path); suspende essa função enquanto sua operação de entrada e saída está pendente. Outro trabalho do laço pode executar, e a função é retomada mais tarde com seu estado local restaurado.

Assíncrono significa que quem chamou pode continuar antes de a operação terminar. Isso não significa que seus tratadores executem em vários núcleos de CPU ao mesmo tempo. Também não faz código que exige muita CPU após um await ceder automaticamente a execução: esse código ainda ocupa sua thread até retornar ou ser suspenso novamente.

Principais conclusões

  • A prontidão de um arquivo regular não garante que uma leitura de disco evite a espera.
  • A entrada e saída assíncrona separa a submissão da conclusão e da obtenção do resultado.
  • Uma continuação salva o estado necessário após o retorno do tratador original.
  • Mantenha os registros de requisição e os buffers válidos até a conclusão.
  • Async/await salva o estado durante a suspensão; não cria execução em múltiplos núcleos.

5.3 Escolha eventos, threads ou uma solução híbrida

Um laço de eventos é adequado para muitas conexões em geral ociosas com pequenas rajadas de trabalho. Um projeto baseado em threads mantém juntos o fluxo de controle e a pilha de uma tarefa. A melhor escolha depende do tempo de espera, do trabalho de CPU, da memória e de como o código evoluirá.

Compare o trabalho que cada projeto exige
AspectoProjeto baseado em eventosProjeto baseado em threads
Conexões em geral ociosasPequeno registro de estado por conexão.Uma thread tem sobrecarga de pilha e escalonamento.
Trabalho bloqueanteTransfira-o a outro trabalhador ou divida-o em torno da conclusão.A thread que faz a chamada pode esperar enquanto outras executam.
Vários núcleos de CPUExecute vários laços ou use trabalhadores.Escalone threads prontas para executar entre os núcleos.
Dados compartilhados mutáveisUm laço síncrono evita a sobreposição entre tratadores.Acessos compartilhados precisam de sincronização.
Uma função passa a bloquearQuem a chama pode precisar ser reestruturado com continuações.O fluxo de controle sequencial pode ser mantido.

Um servidor de chat com 50.000 conexões em geral ociosas pode se beneficiar de registros de eventos em vez de uma pilha de thread dedicada por conexão. Um processador de imagens que faz computação intensa por requisição precisa de trabalhadores suficientes para usar os núcleos disponíveis; um único laço ocupado não consegue paralelizar esses cálculos.

Esperas ocultas ainda importam

Um tratador pode parar sem uma chamada evidente a uma API bloqueante. Acessar uma página que foi transferida para o disco pode provocar uma leitura de disco. Durante essa falta de página, a thread do laço espera. mlock() pode manter as páginas necessárias residentes quando o sistema permite a quantidade solicitada de memória fixada. Apenas remover chamadas bloqueantes explícitas não elimina todas as fontes de atraso.

Mudanças no código podem introduzir outra espera oculta. Se uma função auxiliar que antes retornava imediatamente começar a buscar dados, quem a chama em um projeto baseado em eventos deverá dividir o trabalho em torno de uma continuação ou transferir a chamada a outro trabalhador. A pilha de uma thread bloqueante já armazena onde retomar e o estado local necessário.

Combine os pontos fortes

Um projeto híbrido mantém a prontidão de rede no laço de eventos e envia tarefas bloqueantes ou que exigem muita CPU a um conjunto de trabalhadores. As conclusões retornam ao laço para o tratamento da resposta. O Node.js usa o conjunto de threads do libuv para operações de sistema de arquivos, de modo que uma interface assíncrona pode ser implementada por threads em segundo plano.

Para usar múltiplos núcleos, um servidor pode executar um laço de eventos por núcleo em threads ou processos separados. Se esses laços compartilharem dados mutáveis, precisarão das travas usuais ou de outro protocolo de sincronização. A vantagem de dispensar travas se aplica a tratadores síncronos em um único laço, não a todas as partes de um servidor baseado em eventos.

Uma tarefa pode se intercalar durante um await

Suponha que reste uma unidade em estoque. Um tratador verifica a disponibilidade, aguarda o pagamento e então subtrai do estoque. Enquanto ele está suspenso, outro tratador pode verificar o mesmo estoque e também iniciar um pagamento. Ambas as verificações podem ter sucesso antes de qualquer subtração. Nenhum par de tratadores síncronos executa simultaneamente, mas a operação completa de verificação e atualização não é atômica.

Reserve o estoque em uma única alteração de estado sincronizada antes de ceder a execução, com um caminho de liberação caso o pagamento falhe, ou verifique novamente sob um protocolo que impeça uma segunda venda. Um mutex mantido adquirido durante uma longa espera pode criar outros problemas de progresso, portanto projete explicitamente a transição de estado e sua recuperação. Código baseado em eventos ainda exige analisar quais fatos permanecem verdadeiros após cada suspensão.

Principais conclusões

  • Eventos são adequados a muitas conexões ociosas; trabalho que exige muita CPU precisa de trabalhadores paralelos.
  • Um laço de eventos usa um núcleo; dados compartilhados entre laços precisam de sincronização.
  • Faltas de página e funções auxiliares que passam a bloquear podem parar um laço.
  • Servidores híbridos transferem trabalho bloqueante e retornam as conclusões ao laço.
  • Uma tarefa que atravessa um await pode sofrer uma violação de atomicidade entre verificação e ação.

Exercícios resolvidos de Variáveis de condição e semáforos

5 Lições Interativas

1

De um indicador de conclusão a uma fila funcional

12 passos
Acompanhe o sono, o despertar, a nova verificação e o direcionamento de notificações entre produtores e consumidores.
  1. 1
    IntroduçãoEspere sem desperdiçar uma CPU

    Uma thread pai precisa do resultado de sua filha, mas a filha pode terminar antes ou depois de a pai começar a esperar. Um protocolo funcional deve lidar com ambas as ordens.

    Use um indicador compartilhado done para registrar a conclusão, um mutex para protegê-lo e uma variável de condição para permitir que a pai durma. Acompanhe qual thread mantém o mutex conforme cada uma executa.

  2. 2
    ExemploA thread pai chega primeiro

    Inicialmente, done = 0. A thread pai adquire m, verifica o indicador e chama wait(&c, &m) dentro de um laço while.

    Wait libera o mutex e começa a esperar atomicamente. A filha agora pode adquirir m. Não há um intervalo sem a trava entre a verificação da condição e a entrada na espera no qual a filha poderia enviar uma notificação que a pai perderia.

  3. 3
    ExemploA filha registra a conclusão

    A filha adquire m, armazena done = 1, sinaliza c e libera o mutex. O sinal deixa a pai pronta; não lhe dá execução imediata.

    A pai adquire m novamente antes de wait retornar, verifica done outra vez e continua porque o estado de conclusão da filha agora está registrado. Ambas as threads usam o mesmo mutex para o indicador compartilhado.

  4. 4
    DesafioInverta a ordem de chegada

    A filha termina e sinaliza antes de a pai chegar à espera. O que impede a pai de dormir para sempre?

    ---reveal---

    A filha já definiu done = 1 sob o mutex. A pai verifica esse indicador mantendo o mutex adquirido e não espera. O sinal antecipado não é guardado; o indicador de estado preserva a informação apesar da ordem de chegada diferente.

  5. 5
    ExemploUm despertar não reserva um item

    C1 está esperando por um buffer de uma posição. Um produtor insere um item e sinaliza C1. Antes que C1 adquira o mutex novamente, C2 o adquire e remove esse item.

    Na semântica de Mesa, C1 está apenas pronta para competir. Ela deve testar o buffer novamente após adquirir o mutex. Retornar da espera não substitui a verificação de que o item ainda existe.

  6. 6
    DesafioEscolha a verificação correta

    Nesse acompanhamento, C1 usou if (count == 0) wait(...). Ela agora retorna depois que C2 esvaziou a posição. Que mudança no código impede uma remoção inválida?

    ---reveal---

    Use while (count == 0) wait(...). Após adquirir o mutex novamente, C1 vê o buffer vazio e volta a esperar. O mesmo laço também trata despertares espúrios, portanto é necessário mesmo sem um segundo consumidor.

  7. 7
    ExemploDesperte todas as requisições aptas

    Um alocador tem 0 bytes livres. Ta espera por 100 bytes e Tb por 10. Liberar 50 torna apenas Tb apta a prosseguir, mas um sinal pode despertar Ta.

    Um broadcast alcança ambas. Cada uma adquire o mutex novamente e verifica a quantidade que solicitou. Tb pode pegar 10; Ta espera novamente. O laço de verificação do predicado garante a correção, enquanto a notificação ampla evita deixar adormecida uma thread apta a prosseguir.

  8. 8
    ExemploMantenha a ordem da fila após o retorno circular

    A capacidade é 3. Insira 7, 8, 9; remova 7; insira 10. O array físico agora é [10, 8, 9].

    O índice de leitura aponta para a posição 1, portanto a ordem de remoção é 8, 9, 10. Avance ambos os índices módulo a capacidade e altere count sob o mutex. A exibição do array da esquerda para a direita não é necessariamente a ordem lógica da fila.

  9. 9
    Ideia-chaveDê a cada papel sua própria fila

    Um produtor espera em empty enquanto o buffer está cheio, insere e então sinaliza fill. Um consumidor espera em fill enquanto o buffer está vazio, remove e então sinaliza empty. Ambas as variáveis de condição usam o mutex do buffer.

    As filas separadas importam: uma notificação de item alcança um consumidor, e uma notificação de espaço alcança um produtor. Uma única fila de sinais compartilhada poderia escolher uma thread cuja condição ainda é falsa.

  10. 10
    DesafioEncontre o produtor adormecido

    Um buffer de uma posição usa uma fila de condição compartilhada e laços while. P dorme porque o buffer está cheio. C1 remove o item, sinaliza C2 e então espera porque o buffer está vazio. C2 também encontra o buffer vazio e espera. Por que o espaço continua sem uso?

    ---reveal---

    P nunca recebeu a notificação de espaço. Todo o trabalho pronto para execução parou, embora um produtor pudesse prosseguir. Filas separadas para produtores e consumidores direcionam o sinal de C1 a P.

  11. 11
    DesafioEncerre o trabalho sem perder itens

    Dois produtores ainda estão gerando requisições. Uma coordenadora planeja adicionar agora um marcador de parada para cada consumidor. O que ela deve mudar e por quê?

    ---reveal---

    Primeiro espere todos os produtores terminarem. Só então enfileire os marcadores de parada. Caso contrário, consumidores podem retirar os marcadores e sair enquanto os produtores ainda têm requisições reais para inserir. A sincronização normal da fila ainda é usada para inserir e remover marcadores.

  12. 12
    RecapitulaçãoExamine uma espera em condição

    Para cada espera, identifique o estado, o mutex e a fila. Mantenha a verificação do predicado sob o mutex, entre na espera sem um intervalo com o mutex liberado e verifique novamente após cada retorno.

    Depois inspecione o caminho de sinalização: ele alterou o estado, e a notificação consegue alcançar uma thread que agora pode prosseguir? Essas perguntas revelam tanto despertares perdidos quanto paradas do buffer causadas pelo despertar do papel errado.

2

Acompanhe permissões em protocolos reais

15 passos
Escolha contagens, acompanhe passagens e mantenha reservas de recursos separadas da exclusão mútua.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoDefina o significado de uma permissão
  2. 2
    ExemploAcompanhe um semáforo binário
  3. 3
    DesafioCorrija uma contagem inicial incorreta
  4. 4
    ExemploRegistre uma conclusão antecipada
  5. 5
    ExemploLimite uma fase que exige muita memória
  6. 6
    DesafioSepare a admissão do controle
  7. 7
    ConexãoEncontrem-se antes de continuar
  8. 8
    ExplicaçãoReserve, atualize, disponibilize
  9. 9
    ExemploAcompanhe um consumidor bloqueado
  10. 10
    DesafioIdentifique a dependência da trava
  11. 11
    ExplicaçãoPermita que um grupo de leitores mantenha uma permissão
  12. 12
    ExemploObserve o escritor em espera
  13. 13
    DesafioTrate uma atualização necessária
  14. 14
    DesafioMantenha os dois tipos de proteção
  15. 15
    RecapitulaçãoExamine cada permissão adquirida
3

Diagnostique a intercalação oculta

9 passos
Diferencie uma operação que deve permanecer indivisível de uma operação que deve acontecer primeiro.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoPergunte o que o código pressupõe
  2. 2
    ExemploColoque uma alteração no intervalo
  3. 3
    ExemploTorne a decisão numérica indivisível
  4. 4
    DesafioEncontre o limite ausente da seção crítica
  5. 5
    ExemploUma nova thread trabalhadora pode executar primeiro
  6. 6
    ExplicaçãoSinalize um estado concluído
  7. 7
    DesafioSubstitua uma estimativa de tempo
  8. 8
    DesafioEscolha entre as duas correções
  9. 9
    RecapitulaçãoTeste a garantia, não o atraso
4

De um ciclo de travas a uma requisição segura

17 passos
Acompanhe um impasse, elimine uma condição, teste a segurança dos recursos e planeje a recuperação.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoDiferencie três estratégias
  2. 2
    ExemploFeche um ciclo de duas threads
  3. 3
    ExplicaçãoNomeie as condições necessárias
  4. 4
    DesafioDiferencie uma cadeia de um ciclo
  5. 5
    ExemploOrdene os dois sentidos da mesma forma
  6. 6
    DicaLibere o que foi adquirido parcialmente antes de tentar novamente
  7. 7
    DesafioExamine a função auxiliar oculta
  8. 8
    ExemploCalcule as demandas restantes
  9. 9
    ExemploEncontre uma thread que possa terminar primeiro
  10. 10
    ExemploConclua a sequência de segurança
  11. 11
    Ideia-chaveUma concessão com recursos livres ainda pode ser insegura
  12. 12
    DesafioCompare recursos por tipo
  13. 13
    ConexãoDetecte a espera que existe agora
  14. 14
    ExplicaçãoRecupere o sistema até um estado válido
  15. 15
    DesafioSelecione um alvo útil para recuperação
  16. 16
    DesafioInterprete um resultado inseguro
  17. 17
    RecapitulaçãoEscolha a verificação adequada à pergunta
5

Atenda eventos enquanto a entrada e saída está pendente

13 passos
Construa um laço de prontidão, salve continuações e localize os riscos de concorrência restantes.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoLeve a espera para o ponto de espera do laço
  2. 2
    ExemploConstrua o conjunto observado por select
  3. 3
    ExplicaçãoExecute a leitura de fato
  4. 4
    DesafioVerifique um descritor de valor alto
  5. 5
    ExplicaçãoNão confunda prontidão de arquivo com conclusão
  6. 6
    ExemploSalve o destino da resposta
  7. 7
    DesafioInterprete uma operação pendente
  8. 8
    ConexãoDeixe await preservar o estado local
  9. 9
    ExemploUse trabalhadores para trabalho exigente
  10. 10
    DicaFique atento a uma espera implícita
  11. 11
    DesafioEncontre o intervalo durante await
  12. 12
    DesafioMantenha visível o limite entre núcleos
  13. 13
    RecapitulaçãoExamine cada ponto de suspensão

Questões de Variáveis de condição e semáforos

50 Questões de Prática em 5 conjuntos

Conjunto 1: Esperas, permissões e dependências10 Questões de Prática

Questão 1 de 10

Um serviço tem sete identificadores de dispositivo idênticos e livres. Que valor inicial de semáforo permite que todos os identificadores disponíveis sejam obtidos?
  1. 0
  2. 1
  3. 7
  4. 8
Mostrar respostaOcultar resposta
Resposta: C
Inicialize o semáforo em 7 porque sete unidades estão disponíveis imediatamente. Cada usuário consome uma permissão e a devolve após o uso. Começar em 1 serializaria todos os usuários, embora o conjunto possa atender sete; começar em 0 bloquearia o primeiro usuário.

Questão 2 de 10

Que laço deve envolver uma espera em variável de condição POSIX quando um consumidor precisa que uma fila não esteja vazia?
  1. while (count == 0)
  2. if (count == 0)
  3. while (count > 0)
  4. if (count > 0)
Mostrar respostaOcultar resposta
Resposta: A
Espere enquanto a fila estiver vazia e verifique novamente após cada retorno. A fila pode ter mudado antes de a thread despertada adquirir o mutex novamente, e despertares espúrios são permitidos. Um if não faz uma segunda verificação, portanto pode permitir que o consumidor remova de uma fila vazia.

Questão 3 de 10

Que contagem inicial de semáforo permite que exatamente uma thread entre em uma seção crítica antes de um post?
  1. 0
  2. 2
  3. −1
  4. 1
Mostrar respostaOcultar resposta
Resposta: D
Uma permissão inicial permite que a primeira thread passe e faz uma segunda esperar por uma permissão devolvida. Uma contagem de 0 bloqueia a primeira thread, de modo que ela não consegue chegar à seção crítica e ao seu post posterior. Um valor inicial negativo não é válido para inicializar um semáforo POSIX.

Questão 4 de 10

Uma máquina tem 12 GB de RAM. Outros trabalhos precisam de 3 GB, e cada trabalhador admitido precisa de 750 MB. Usando unidades decimais do SI, quantos trabalhadores cabem na memória restante?
  1. 16
  2. 12
  3. 9
  4. 15
Mostrar respostaOcultar resposta
Resposta: B
Os trabalhadores têm 9 GB, ou 9.000 MB, disponíveis após reservar 3 GB. Dividir 9.000 MB por 750 MB resulta em 12 trabalhadores simultâneos. Admitir 16 gastaria todos os 12 GB com trabalhadores e não deixaria memória para os outros trabalhos especificados.

Questão 5 de 10

Duas threads reservam quatro assentos cada uma. Restam seis assentos, e ambas as verificações de disponibilidade terminam antes de qualquer reserva. Que limite de seção crítica impede reservas acima da capacidade?
  1. Uma região protegida para verificar e outra para reservar.
  2. Uma região protegida para verificar e reservar.
  3. Uma trava de leitura para toda a reserva de cada thread.
  4. Um mutex privado separado para cada reserva.
Mostrar respostaOcultar resposta
Resposta: B
A verificação dos assentos deve permanecer válida até a reserva que depende dela, portanto proteja ambas dentro de uma seção crítica compartilhada por todos que reservam. Acessos protegidos separadamente ainda permitem que ambas as verificações observem seis assentos disponíveis antes que qualquer uma reserve quatro. Um mutex privado não consegue excluir a thread concorrente.

Questão 6 de 10

Uma thread de relatório precisa de um total final escrito por uma trabalhadora pouco antes de ela terminar. Que operação garante a conclusão antes que o relatório leia o total?
  1. Dormir brevemente antes de ler.
  2. Ceder a CPU antes de ler.
  3. Adquirir uma trava apenas para ler.
  4. Aguardar o término da trabalhadora antes de ler.
Mostrar respostaOcultar resposta
Resposta: D
Aguardar o término da trabalhadora garante que ela tenha terminado, de modo que seu total concluído esteja disponível antes de o relatório lê-lo. Um sono apenas estima quanto tempo o trabalho leva. Proteger apenas a leitura não exige que a trabalhadora tenha terminado ou escrito o resultado primeiro.

Questão 7 de 10

A thread H mantém M e espera por N. A thread J mantém N e espera por M. Ambas as travas são exclusivas e não podem ser retiradas à força. O que descreve essas duas threads?
  1. Um conjunto em impasse.
  2. Um grupo em inanição.
  3. Um par em bloqueio ativo.
  4. Um par pronto para executar.
Mostrar respostaOcultar resposta
Resposta: A
Cada thread espera por uma trava que somente sua parceira bloqueada pode liberar, formando um ciclo permanente. Isso é um impasse, com ambas as threads bloqueadas. O bloqueio ativo exigiria que elas continuassem executando e reagindo, enquanto a inanição deixaria outros trabalhos progredirem enquanto uma thread espera.

Questão 8 de 10

Uma transferência adquire travas de objetos de contas com chaves únicas. Que regra de aquisição impede que transferências em sentidos opostos criem espera circular?
  1. Adquirir a trava da conta de origem antes da de destino.
  2. Adquirir a trava da conta de destino antes da de origem.
  3. Adquirir as travas das duas contas em ordem crescente de chave.
  4. Adquirir primeiro a trava da conta com menor saldo.
Mostrar respostaOcultar resposta
Resposta: C
Uma ordem crescente comum de chaves faz ambos os sentidos de transferência solicitarem a mesma primeira trava, impedindo o padrão de aquisição oposta. Adquirir primeiro a trava da origem inverte a ordem quando o sentido da transferência muda. A ordem baseada no saldo pode mudar conforme os saldos mudam, portanto não é a regra de chave fixa compartilhada.

Questão 9 de 10

Um laço observa os descritores 2, 8 e 14 com select. Que valor deve passar para nfds?
  1. 3
  2. 14
  3. 15
  4. 16
Mostrar respostaOcultar resposta
Resposta: C
Select examina os números de descritores de 0 até nfds - 1, portanto o maior descritor observado, 14, exige 15. O argumento se baseia no maior valor de descritor, não nas três entradas do conjunto observado. Passar 14 excluiria o descritor 14 do intervalo examinado.

Questão 10 de 10

O tratador A submete uma leitura para o socket 40 de um cliente. O tratador B então submete outra leitura para o socket 41. Ambos os tratadores de conclusão usam um destino global, agora 41. A leitura de A termina primeiro. Para onde a resposta de A é direcionada e que correção preserva os dois clientes?
  1. Para 40; continuar usando o destino global.
  2. Para 41; salvar o destino por requisição.
  3. Para 40; reconstruir o conjunto de select antes de responder.
  4. Para 41; manter apenas uma contagem de requisições pendentes.
Mostrar respostaOcultar resposta
Resposta: B
A conclusão de A lê o valor global sobrescrito 41, portanto direciona a resposta ao cliente de B em vez do socket 40. Salve cada destino com sua própria requisição e consulte-o na conclusão. Um tratamento mais rápido da conclusão não permite que um único destino global preserve dois clientes pendentes.
Conjunto 2: Acompanhe o protocolo em um escalonamento10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Um buffer circular tem capacidade 4 e começa vazio, com ambos os índices em 0. Ele recebe 21, 22, 23, remove dois itens e depois recebe 24 e 25. Qual é agora sua ordem lógica de remoção?
  1. 25, 23, 24
  2. 23, 24, 25
  3. 24, 25, 23
  4. 21, 24, 25
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Uma rotina de carregamento define ready sob um mutex e sinaliza antes que um usuário comece a esperar. Mais tarde, o usuário adquire esse mutex e testa ready antes de decidir se vai esperar. Por que ele pode prosseguir?
  1. A variável de condição guardou o sinal.
  2. A aquisição do mutex repete a notificação.
  3. O sinal passou o mutex ao usuário posterior.
  4. O estado ready permanece após o sinal.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Um buffer com semáforos tem capacidade 6 e inicialmente contém dois itens. Não há operações em andamento. Quais são suas contagens de permissões empty e full?
  1. Empty 4; full 2.
  2. Empty 6; full 2.
  3. Empty 2; full 4.
  4. Empty 4; full 0.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Três leitores mantêm adquirida a trava básica que dá preferência a leitores, e um escritor está esperando. Dois leitores saem. O que ainda impede o escritor de entrar?
  1. O escritor precisa obter três permissões de escrita separadas.
  2. O último leitor a sair já liberou writelock.
  3. O leitor restante ainda mantém o acesso do grupo.
  4. A trava do contador continua com o escritor em espera.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Um saldo compartilhado usa leituras e subtrações atômicas. Duas threads verificam, cada uma, se há pelo menos 45 unidades e depois subtraem 45; o saldo começa em 70. Que falha permanece se ambas as verificações terminarem primeiro?
  1. A segunda subtração recusa o débito.
  2. As leituras atômicas reservam 45 unidades cada uma.
  3. Ambos os débitos podem ocorrer e deixar −20.
  4. Somente a primeira thread pode ler 70.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Available é (1, 1). A tem Allocation (1, 0) e Max (2, 2); B tem Allocation (0, 1) e Max (1, 2). O que conclui o teste de segurança do banqueiro?
  1. Seguro; B pode terminar antes de A.
  2. Seguro; A pode terminar antes de B.
  3. Inseguro; nenhuma demanda restante cabe inicialmente.
  4. Inseguro; o máximo total de A excede Available.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Uma espera por mutex com prazo retorna ETIMEDOUT. O que isso estabelece?
  1. Quem mantém o mutex está em um ciclo de travas.
  2. O mutex foi liberado.
  3. O estado protegido está corrompido.
  4. A espera excedeu seu prazo.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Um servidor mantém 30.000 sockets em geral ociosos e faz pouca computação por mensagem. Por que um laço de eventos pode usar menos memória por conexão do que uma thread dedicada por socket?
  1. Conexões não precisam de estado enquanto estão ociosas.
  2. Conexões podem usar pequenos registros de estado salvo.
  3. Cada tratador de eventos recebe um núcleo de CPU separado.
  4. Todos os sockets compartilham um descritor de rede reutilizável.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma thread em espera lê done como falso sem o mutex. A trabalhadora então adquire o mutex, define done como verdadeiro, sinaliza sem haver threads em espera e libera o mutex. Em seguida, a thread em espera adquire o mutex e chama wait sem verificar novamente. Que falha de dependência ocorreu?
  1. Um despertar espúrio antes do sinal da trabalhadora.
  2. Uma nova verificação segura de Mesa após adquirir o mutex.
  3. Um sinal guardado à espera da próxima chamada.
  4. Um despertar perdido após uma verificação desprotegida.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Em um grafo de espera com recursos de instância única, U → V, V → W, W → V, e X não tem aresta de saída. Quais threads formam o ciclo de dependências?
  1. V e W.
  2. U, V e W.
  3. U e X.
  4. V, W e X.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 3: Aplique as regras quando as condições mudam10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Um alocador compartilhado não tem memória livre. Uma thread em espera precisa de 180 bytes e outra de 40. Uma liberação adiciona 90 bytes e sinaliza apenas a thread que espera por 180 bytes; nenhuma outra alteração ocorre. Como a política de notificação deve mudar para permitir que uma thread apta prossiga?
  1. Usar broadcast após a liberação.
  2. Sinalizar assim que 180 bytes se acumularem.
  3. Dispensar o mutex na atualização.
  4. Trocar ambos os laços while por if.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Um buffer de capacidade dois com variáveis de condição contém um item. Cada um de dois produtores verifica se há espaço sob o mutex, libera-o e depois o adquire novamente apenas para put. Ambas as verificações terminam antes de qualquer put. Que estado os dois puts podem produzir?
  1. Contagem 2; a segunda verificação reserva a posição.
  2. Contagem 1; um dos puts serializados é descartado.
  3. Contagem 3; o intervalo permite que ambas as verificações admitam um item.
  4. Contagem 0; o retorno circular do índice esvazia o buffer.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Um semáforo de conclusão não tem nenhuma thread em espera quando sua trabalhadora executa post. O que esse post deixa para um wait posterior bem-sucedido?
  1. Um dono de mutex guardado.
  2. Uma notificação perdida.
  3. Uma thread bloqueada em espera.
  4. Uma permissão armazenada.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Uma trava de leitura protege um cache. Um leitor percebe que uma entrada está ausente, libera o acesso de leitura e espera por acesso de escrita. Outro escritor preenche a entrada antes que ele entre. O que o leitor original deve fazer após adquirir acesso de escrita?
  1. Inserir imediatamente com base na observação anterior.
  2. Verificar novamente se a entrada ainda está ausente.
  3. Adquirir novamente acesso de leitura mantendo o de escrita.
  4. Liberar o acesso de escrita sem examinar o cache.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

A inicialização e o primeiro uso adquirem o mesmo mutex, mas o primeiro uso às vezes encontra um registro não inicializado. Que garantia está faltando?
  1. Acesso exclusivo durante cada operação.
  2. Conclusão da inicialização antes do uso.
  3. Uma trava privada diferente para cada operação.
  4. Um atraso fixo após cada liberação do mutex.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Tarefas adquirem seus conjuntos completos de recursos enquanto mantêm um mutex global de aquisição e depois liberam esse mutex antes de trabalhar. Todos os caminhos seguem essa regra, e a liberação de recursos nunca precisa do mutex de aquisição. Por que aquisições parciais concorrentes não podem formar um ciclo?
  1. Só pode existir uma sequência de aquisição incompleta.
  2. Nenhuma thread pode manter um recurso enquanto espera.
  3. Todos os recursos se tornam preemptíveis quando o trabalho começa.
  4. A CPU não pode preemptar uma thread durante a aquisição.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Há 9 recursos idênticos. A mantém 3 com Max 7; B mantém 2 com Max 4. A solicita mais 3. Segundo o algoritmo do banqueiro, essa requisição deve ser concedida?
  1. Não; o estado resultante não tem sequência segura.
  2. Sim; B pode terminar antes de A.
  3. Não; a requisição excede a demanda restante de A.
  4. Sim; A pode terminar antes de B.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Um socket de fluxo pronto para leitura retorna 0 em uma leitura bem-sucedida. Que condição de entrada o tratador deve processar?
  1. Um resultado temporário de operação que bloquearia.
  2. Um byte recebido com valor zero.
  3. Fim da entrada enviada pela outra ponta.
  4. Uma conclusão pendente de leitura de disco.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Um tratador enfileira uma leitura assíncrona de disco, libera seu buffer de destino e retorna. A requisição ainda está pendente. O que deve mudar para que o caminho de conclusão possa usar o resultado com segurança?
  1. Manter apenas o valor do descritor de arquivo até o retorno.
  2. Ler imediatamente porque a submissão já teve sucesso.
  3. Mover a liberação do buffer para antes da submissão.
  4. Manter o buffer válido até a conclusão da entrada e saída.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Um laço verifica uma cota compartilhada, aguarda aprovação externa e então decrementa a cota. Duas tarefas podem passar pela verificação antes que qualquer uma seja retomada. Que mudança impede que ambas reivindiquem a última unidade?
  1. Tornar atômicas as leituras da cota, mas manter a decisão dividida.
  2. Proteger apenas o decremento final após a aprovação.
  3. Reservar atomicamente uma unidade da cota antes de esperar.
  4. Reservar uma unidade após a aprovação usando a verificação anterior.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 4: Analise correções incompletas10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Uma thread que espera por uma condição mantém o mutex M adquirido para seu predicado e também mantém a trava L. Wait libera M. A única thread que pode tornar o predicado verdadeiro deve primeiro adquirir L. O que impede o progresso e que dependência deve mudar?
  1. Quem sinaliza deve adquirir M duas vezes antes de alterar o estado.
  2. Quem espera deve manter M adquirido durante todo o sono.
  3. Quem espera deve descartar o predicado e esperar uma vez.
  4. Quem espera deve deixar de manter L durante essa espera.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Quatro consumidores processam uma fila FIFO. Já se aguardou o término de todos os produtores, ainda pode haver itens reais na fila, e um marcador de parada reservado faz um consumidor sair. Quantos marcadores a coordenação deve enfileirar?
  1. 1
  2. 4
  3. 3
  4. 5
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Um conjunto começa com três permissões. Um usuário executa wait com sucesso, depois devolve seu recurso e acidentalmente executa post duas vezes. Quando todo o trabalho está ocioso, quantos usuários podem passar por waits antes que alguém execute post novamente?
  1. 4
  2. 2
  3. 3
  4. 5
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Um buffer com semáforos de capacidade dois está vazio. P1 e P2 executam wait em empty com sucesso; então P1 insere sob o mutex e o libera, mas ainda não executou post em full. P2 não inseriu. Um consumidor agora executa wait em full. O que acontece?
  1. Ele remove o item de P1 antes que P2 insira.
  2. Ele bloqueia até que P2 conclua sua inserção.
  3. Ele bloqueia até que uma permissão de item seja disponibilizada.
  4. Ele remove o item de P1 quando o mutex fica livre.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Uma pessoa programadora quer que o incremento de um contador compartilhado seja uma única atualização indivisível sem usar um mutex. Que operação atômica implementa diretamente essa atualização?
  1. Apenas uma leitura atômica.
  2. Apenas uma escrita atômica.
  3. Um sinal de condição.
  4. Busca e soma.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Cinco filósofos à mesa usam um semáforo inicialmente definido em 4. Cada um executa wait nele, pega um garfo à esquerda, executa post no semáforo e então tenta pegar o garfo à direita. Os cinco podem ficar bloqueados mantendo um garfo cada?
  1. Não; a contagem inicial limita permanentemente a quatro quem mantém garfos.
  2. Sim; devolver permissões cedo permite que os cinco cheguem ao ciclo de garfos.
  3. Não; cada post devolve atomicamente o garfo à esquerda do filósofo.
  4. Sim; mas apenas se a contagem inicial do semáforo subir acima de quatro.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Available é (1, 1). P mantém (1, 0), Max (3, 1); Q mantém (1, 1), Max (2, 2). P solicita (0, 1). O algoritmo do banqueiro deve conceder essa requisição?
  1. Sim; P pode terminar com os recursos restantes.
  2. Não; o estado provisório não tem sequência segura.
  3. Sim; Q pode terminar com os recursos restantes.
  4. Não; P pede mais que sua demanda restante.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Um detector com múltiplas instâncias encontra Available (0, 0). P mantém (1, 0) e solicita (0, 1); Q mantém (0, 1) e solicita (1, 0). R não mantém nada e solicita (1, 0). Quais threads que mantêm recursos sustentam o impasse?
  1. P, Q e R.
  2. P e R.
  3. P e Q.
  4. Q e R.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Um laço select observa inicialmente os sockets de fluxo 6 e 10. Apenas 6 é retornado como pronto. O laço reutiliza esse conjunto retornado sem reconstruí-lo, enquanto 10 recebe dados e leituras em 6 agora retornam 0. Que duas falhas podem ocorrer?
  1. O socket 10 continua observado; o socket 6 fornece um byte de dados zero.
  2. O socket 10 fica pronto para escrita; o socket 6 espera pela conclusão do disco.
  3. O socket 10 é omitido; o socket 6 pode ser despachado novamente no fim da entrada.
  4. O socket 10 substitui 6 no conjunto; o socket 6 é fechado por select.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Quarenta requisições estão enfileiradas em um laço de eventos. Cada tratador realiza uma leitura bloqueante de disco de 6 ms antes de retornar, com trabalho adicional desprezível. Quanto tempo a última requisição espera antes de sua leitura começar?
  1. 234 ms
  2. 240 ms
  3. 6 ms
  4. 39 ms
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 5: Transfira o raciocínio para novos projetos10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Uma thread Java chama notifyAll em um objeto fora de qualquer região synchronized e sem possuir seu monitor de outra forma. O que acontece?
  1. Ela guarda uma notificação para threads que esperarem depois.
  2. Ela lança IllegalMonitorStateException.
  3. Ela entrega as travas da thread que chamou para quem espera.
  4. Ela espera até que o monitor fique livre.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Um semáforo é implementado com um mutex, uma variável de condição e um valor não negativo. Post incrementa e sinaliza sob o mutex. Wait verifica value com if, espera se for zero e então decrementa. Como ele ainda pode falhar?
  1. O mutex permite que duas threads alterem value simultaneamente.
  2. O semáforo não consegue lembrar um post sem threads em espera.
  3. Uma thread que retorna da espera pode decrementar um valor zero.
  4. Um post sempre decrementa value quando não há thread em espera.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Um consumidor mantém o mutex do buffer adquirido enquanto espera no semáforo de itens. O buffer está vazio. Uma correção proposta faz o produtor executar post no semáforo de itens antes de inserir, para que possa despertar o consumidor sem o mutex. Por que essa correção é insegura?
  1. Ela permite a remoção antes da inserção pelo produtor.
  2. Ela permite a inserção antes de reservar espaço.
  3. Ela devolve uma permissão de posição vazia antes do fim da remoção.
  4. Ela disponibiliza duas vezes a mesma permissão de item concluído.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

No projeto básico com contagem de leitores, o primeiro leitor pode manter adquirida a trava do contador enquanto espera por writelock. Por que o escritor ativo ainda pode terminar, diferentemente de um produtor bloqueado pelo mutex do buffer mantido por um consumidor?
  1. O leitor em espera libera a trava do contador em sem_wait.
  2. O próprio escritor já mantém a trava do contador de leitores.
  3. O post do escritor espera a contagem de leitores chegar a zero.
  4. O escritor pode executar post em writelock sem a trava do contador.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Uma thread trabalhadora lê um ponteiro compartilhado antes que a criadora termine de atribuí-lo. Que tipo de erro de dependência é esse?
  1. Uma violação de ordem.
  2. Um impasse por recursos.
  3. Uma inanição de escritores.
  4. Um bloqueio ativo por repetição.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Dois laços de atualização adquirem cada um uma trava, falham ao tentar adquirir a outra com trylock, liberam a primeira e repetem juntos indefinidamente. Uma correção proposta mantém a primeira trava adquirida enquanto espera normalmente pela segunda. Que risco essa mudança traz?
  1. Ela mantém o bloqueio ativo porque ambas as threads continuam tentando.
  2. Ela garante que uma termine, enquanto a outra pode sofrer inanição.
  3. Ela substitui o bloqueio ativo por um possível impasse.
  4. Ela serializa ambas as aquisições sob uma primeira trava comum.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Available é (1, 1). P mantém (1, 0), Max (2, 1); Q mantém (0, 2), Max (1, 3); R mantém (1, 0), Max (3, 2). Que sequência segura permite que as três terminem?
  1. R, P, Q.
  2. P, R, Q.
  3. Q, R, P.
  4. P, Q, R.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Available é (0, 0). A mantém (1, 0), solicita atualmente (0, 0) e tem Need máxima restante (0, 2). B mantém (0, 2) e solicita atualmente (1, 0). A detecção por requisições atuais encontra um impasse?
  1. Sim; nenhuma Need máxima cabe no Available inicial.
  2. Não; A libera (1, 0), permitindo que a requisição de B prossiga.
  3. Sim; a requisição atual de B não pode ser atendida imediatamente.
  4. Não; ambas as requisições já cabem no Available inicial.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma pessoa desenvolvedora substitui varreduras repetidas de requisições de entrada e saída pendentes por uma fila de conclusão. Que trabalho o laço agora pode despachar diretamente?
  1. Respostas finais para toda requisição recém-submetida.
  2. Trabalho posterior para operações informadas como concluídas.
  3. Leituras em todo arquivo regular informado como pronto por select.
  4. Execução paralela de todos os tratadores em uma thread de laço.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Um buffer circular de capacidade dois começa vazio. Insira 31, insira 32, remova uma vez e insira 33. Ambos os índices agora são 1. Quais são a contagem de ocupação e o valor retornado pela próxima remoção?
  1. Contagem 1; próximo valor 32.
  2. Contagem 2; próximo valor 33.
  3. Contagem 0; a remoção deve esperar.
  4. Contagem 2; próximo valor 32.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Travou em uma questão sua? Ajuda com Tarefas

Termos-Chave

90 Termos-Chave, cada um com sua definição

A

Admissão justa
A admissão justa concede acesso segundo uma política que impede que uma classe contínua de chegadas ultrapasse indefinidamente uma thread em espera.
Uma política justa de leitores e escritores impede que leitores posteriores ultrapassem indefinidamente um escritor que chegou antes.
Algoritmo do banqueiro
O algoritmo do banqueiro concede uma requisição de recursos somente quando ela cabe nas demandas restantes declaradas e preserva uma sequência segura de conclusão.
Um alocador concede provisoriamente uma requisição, testa o estado resultante e adia uma concessão insegura.
Atualização perdida
Uma atualização perdida ocorre quando operações sobrepostas de leitura, modificação e escrita sobrescrevem umas às outras, fazendo uma alteração pretendida desaparecer.
Duas threads leem a contagem 8 e cada uma armazena 9, embora dois incrementos devessem resultar em 10.
Ausência de preempção
A ausência de preempção significa que um recurso mantido não pode ser retirado à força de quem o possui e deve ser liberado por meio de seu protocolo.
Uma thread que espera por um mutex comum não pode simplesmente retirar a trava de sua dona atual.

B

Bloco de controle de entrada e saída assíncrona
Um bloco de controle de entrada e saída assíncrona descreve uma requisição AIO do POSIX, incluindo seu descritor, deslocamento no arquivo, buffer e contagem de bytes solicitada.
O aiocb e seu buffer de destino permanecem válidos enquanto a leitura enfileirada ainda está pendente.
Bloqueio ativo
Um bloqueio ativo mantém threads executando e reagindo umas às outras enquanto suas ações repetidas não conseguem concluir trabalho útil.
Dois laços trylock liberam e tentam novamente juntos, repetidamente, sem que nenhum obtenha ambas as travas.
Bloqueio implícito
Um bloqueio implícito atrasa uma thread por meio de uma operação que parece imediata, como acessar uma página que precisa ser lida do disco.
Um tratador para em uma falta de página mesmo sem fazer uma chamada explícita de leitura bloqueante.
Buffer circular
Um buffer circular armazena uma fila em um array cujos índices de leitura e escrita retornam da última posição à primeira.
Após escrever na posição 2 de um array de três posições, a próxima posição de escrita retorna à posição 0.
Buffer limitado
Um buffer limitado é uma fila compartilhada de capacidade fixa que faz produtores esperarem quando está cheia e consumidores esperarem quando está vazia.
Um buffer de capacidade três não pode aceitar um quarto item até que um consumidor remova um.
Busca e soma
Busca e soma é uma operação atômica de leitura, modificação e escrita que adiciona uma quantidade e retorna o valor antigo sem uma atualização intermediária.
Uma busca e soma atômica impede que incrementos concorrentes de contador sobrescrevam uns aos outros.

C

Comparação e troca
A comparação e troca substitui atomicamente o valor de uma posição somente quando seu valor atual corresponde ao valor esperado fornecido por quem chamou.
Uma rotina de atualização de contador tenta novamente quando outra thread altera o valor antigo antes que seu CAS tenha sucesso.
Concorrência híbrida
A concorrência híbrida combina um laço de eventos com trabalhadores que realizam operações bloqueantes ou que exigem muita CPU fora da thread do laço.
Um laço de rede envia processamento de imagens a trabalhadores e depois trata suas conclusões.
Condição abrangente
Uma condição abrangente usa uma notificação ampla para alcançar todas as threads em espera cuja condição mais específica possa agora ser satisfeita.
Quando um alocador com solicitantes de 10 bytes e de 100 bytes em espera libera 50 bytes, ele desperta ambos para que cada um verifique novamente.
Conjunto de descritores
Um conjunto de descritores representa números de descritores de arquivo selecionados para monitoramento, com bits que indicam a inclusão no conjunto ou a prontidão retornada.
O servidor reconstrói seu conjunto de leitura porque select() o substitui pelo subconjunto pronto.
Conjunto de trabalhadores
Um conjunto de trabalhadores é um grupo limitado de threads ou processos reutilizáveis que realizam tarefas submetidas e retornam seus resultados.
O libuv despacha operações de sistema de arquivos para seu conjunto de threads enquanto o laço atende eventos.
Continuação
Uma continuação armazena o estado necessário para retomar uma tarefa após o retorno de um tratador que espera por uma operação assíncrona.
A requisição de leitura guarda qual socket de cliente deve receber seus dados.
Controle de admissão
O controle de admissão decide se mais trabalho pode entrar em uma região com recursos limitados, adiando-o quando a capacidade permitida está em uso.
Uma trabalhadora espera por uma permissão de processamento antes de alocar seu grande buffer temporário.
Conversão de trava de leitura em trava de escrita
Uma conversão de trava de leitura em trava de escrita muda o acesso de leitura para acesso exclusivo de escrita; no projeto básico, o acesso de leitura deve ser liberado primeiro.
Após adquirir acesso de escrita, uma rotina de atualização de cache verifica novamente se a entrada ainda precisa ser alterada.

D

Despertar espúrio
Um despertar espúrio é um retorno de uma espera em condição sem uma notificação correspondente, exigindo a mesma verificação do predicado que qualquer despertar.
Um consumidor volta a esperar no laço se despertar e ainda encontrar o buffer vazio.
Despertar perdido
Um despertar perdido ocorre quando uma notificação necessária passa antes de uma thread começar a esperar, deixando-a adormecida sem uma notificação futura.
Quando uma thread pai verifica um indicador de conclusão fora do mutex, a thread filha pode terminar entre a verificação e a espera.
Detecção de impasses
A detecção de impasses procura nas dependências de espera ou requisições de recursos atuais um conjunto existente que não consegue progredir.
Uma busca por ciclos encontra três threads que mantêm mutexes e estão bloqueadas umas pelas outras.

E

Entrada e saída assíncrona
A entrada e saída assíncrona separa a submissão de uma operação da obtenção de seu resultado concluído, permitindo que quem chamou atenda outro trabalho nesse intervalo.
O tratador submete uma leitura de arquivo e envia a resposta após a chegada de sua conclusão.
Entrada e saída não bloqueante
A entrada e saída não bloqueante devolve o controle quando uma operação não pode prosseguir imediatamente, permitindo que a thread que chamou trate outro trabalho.
Uma leitura de socket retorna EAGAIN e o tratador retorna ao laço de eventos.
epoll
epoll é uma interface de eventos do Linux que mantém um conjunto de interesse no núcleo e informa eventos prontos para descritores registrados.
Um servidor pode monitorar muitas conexões sem reenviar a lista completa de observação a cada vez.
Espera aleatória antes de nova tentativa
A espera aleatória antes de nova tentativa acrescenta um atraso escolhido aleatoriamente antes de tentar novamente, reduzindo colisões repetidas entre concorrentes que, de outro modo, estariam sincronizados.
Transferências que tentam novamente esperam intervalos diferentes para reduzir a chance de outra colisão.
Espera circular
Uma espera circular é uma cadeia fechada de threads na qual cada uma espera por um recurso mantido pela próxima thread da cadeia.
Três threads esperando de A para B, de B para C e de C para A fecham o ciclo de dependências.
Espera pelo término de uma thread
A espera pelo término de uma thread aguarda o encerramento de uma thread específica antes que a thread que espera continue um trabalho que depende dessa conclusão.
A thread de relatório aguarda o término da trabalhadora que soma antes de ler seu total final.
Estado inseguro
Um estado inseguro não tem uma sequência garantida de conclusão das demandas máximas, embora o comportamento futuro real ainda possa evitar um impasse.
Uma thread pode liberar recursos antecipadamente em vez de solicitar toda a sua demanda restante.
Estado seguro
Um estado seguro tem pelo menos uma ordem de conclusão capaz de satisfazer a demanda máxima restante de cada thread usando recursos disponíveis e liberados.
Uma thread pode terminar primeiro e liberar recursos suficientes para que uma segunda thread conclua em seguida.
Evitação de impasses
A evitação de impasses verifica concessões propostas de recursos com base em informações prévias de demanda e adia concessões que deixariam o sistema inseguro.
Um recurso livre pode deixar de ser concedido quando sua concessão eliminaria todas as sequências seguras de conclusão.
Exclusão mútua
A exclusão mútua permite que apenas uma thread por vez mantenha um recurso ou execute uma operação protegida sobre o estado compartilhado.
Um mutex exclusivo não pode ser concedido a uma segunda thread enquanto sua dona ainda o mantém adquirido.

F

Fila de conclusão
Uma fila de conclusão entrega registros de operações assíncronas terminadas, permitindo que um programa obtenha resultados e despache trabalho posterior.
O laço lê um registro de conclusão em vez de verificar repetidamente cada leitura pendente.

G

Gerenciamento manual da pilha
O gerenciamento manual da pilha preserva explicitamente o estado local da tarefa entre retornos de tratadores em vez de depender de uma única pilha de chamadas suspensa.
Um registro de resposta mantém a posição da requisição e o destino até que a leitura termine.
Grafo de espera
Um grafo de espera representa threads como nós e dependências de espera como arestas dirigidas da thread em espera para quem mantém um recurso.
Uma seta de T1 para T2 significa que T1 precisa de um recurso que T2 mantém no momento.

I

Impasses
Um impasse prende um conjunto de threads bloqueadas porque cada uma precisa de uma ação ou recurso que somente outra integrante presa pode fornecer.
T1 mantém A e espera por B enquanto T2 mantém B e espera por A.
Inanição
A inanição adia uma thread indefinidamente enquanto outras continuam realizando trabalho útil, mesmo sem um ciclo de dependências bloqueadas que prenda todas elas.
Um escritor continua esperando enquanto um fluxo ininterrupto de leitores com períodos de leitura sobrepostos usa o recurso.
Inanição de escritores
A inanição de escritores é o adiamento indefinido de um escritor em espera enquanto leitores continuam entrando e progredindo.
Uma trava que apenas conta leitores não dá vez a um escritor se novos leitores continuarem se sobrepondo.
Indicador de prontidão
Um indicador de prontidão registra a conclusão da inicialização sob sincronização, permitindo que usuários esperem até que o estado necessário exista.
A criadora define ready somente após atribuir o registro que a trabalhadora usará.
Intercalação
Uma intercalação é uma ordenação de etapas de threads diferentes que preserva a ordem interna de cada thread enquanto mistura seus acessos.
Definir um ponteiro como nulo entre a verificação e o uso por outra thread revela o escalonamento inseguro.
io_uring
io_uring é uma interface do Linux com filas de submissão e conclusão para operações assíncronas, que permite o tratamento de entrada e saída orientado por conclusão.
Um servidor submete requisições e depois despacha continuações a partir de seus registros de conclusão.

K

kqueue
kqueue é uma interface de notificação de eventos no FreeBSD e no macOS que mantém interesses registrados e retorna os eventos informados.
Um servidor registra interesses de sockets com kqueue em vez de reconstruir conjuntos completos de descritores de select.

L

Laço de eventos
Um laço de eventos espera por eventos e despacha tratadores curtos, permitindo que uma thread coordene muitas tarefas cujos períodos de espera se sobrepõem.
Um servidor trata dados de um socket pronto e depois retorna para atender outra conexão.
Liberação e espera atômicas
A liberação e espera atômicas tornam a liberação do mutex e a entrada na espera em condição uma única passagem indivisível em relação às threads concorrentes.
A filha não consegue sinalizar em um intervalo após a pai liberar o mutex, mas antes de ela começar a esperar.
Limitação de concorrência
A limitação de concorrência restringe a entrada simultânea em uma fase exigente para que a demanda combinada permaneça dentro de um limite escolhido de recursos.
Doze trabalhadores simultâneos que usam 500 MB cada consomem 6.000 MB durante sua fase de uso intenso de memória.

M

Marcador de fim dos dados
Um marcador de fim dos dados é um item reservado da fila que informa a um consumidor que deve parar após todos os itens reais anteriores terem sido processados.
Uma coordenação aguarda o término de cada produtor antes de inserir um marcador de parada por consumidor.
Modelo de contador com sinal
O modelo de contador com sinal representa por valores negativos as threads bloqueadas em espera em um semáforo, preservando o mesmo comportamento de wait e post observado pelas threads.
Um valor de acompanhamento −2 representa duas threads bloqueadas em espera, não uma contagem negativa de permissões no Linux.
Monitor
Um monitor reúne estado compartilhado e operações que permitem a entrada de uma thread por vez, juntamente com espera baseada em condições.
Um bloco synchronized do Java mantém adquirido o monitor do objeto exigido para chamadas a wait().
Mutex do buffer
Um mutex do buffer serializa alterações no array e nos índices do buffer, independentemente dos semáforos que contam os recursos disponíveis.
Dois produtores com permissões de espaço válidas ainda atualizam o índice de escrita do buffer, fill_ptr, sob o mesmo mutex.

N

Notificação a todas as threads em espera
Uma notificação a todas as threads em espera desperta todas as threads que esperam em uma variável de condição; cada uma deve readquirir o mutex e testar seu próprio predicado.
Um alocador usa broadcast após uma liberação porque as threads em espera solicitam quantidades diferentes de memória.

O

Operação de espera em semáforo
Uma operação de espera em semáforo adquire atomicamente uma permissão, bloqueando quando nenhuma está disponível até que outra operação forneça uma.
Um usuário executa wait antes de obter um identificador para não exceder o limite de admissão do conjunto.
Operação de sinalização de semáforo
Uma operação de sinalização de semáforo fornece atomicamente uma permissão, permitindo que uma thread bloqueada em espera continue ou guardando a disponibilidade para um wait posterior.
Uma trabalhadora executa post em seu semáforo de conclusão mesmo que sua pai ainda não tenha começado a esperar.
Ordenação de aquisição de travas
A ordenação de aquisição de travas atribui uma ordem compartilhada de aquisição às travas e exige que todo caminho de código relevante siga essa ordem.
Ambos os sentidos de uma transferência adquirem a trava da conta de número menor antes da conta de número maior.

P

Paralelismo em múltiplos núcleos de processamento
O paralelismo em múltiplos núcleos de processamento executa computação em vários núcleos de CPU ao mesmo tempo, exigindo mais trabalhadores de execução do que uma única thread de laço.
Vários trabalhadores de processamento de imagens podem calcular ao mesmo tempo enquanto um laço de eventos atende sockets.
Permissão
Uma permissão representa uma unidade disponível de admissão ou recurso que um wait bem-sucedido em semáforo consome e que um post fornece.
Um semáforo com três permissões não usadas pode admitir três threads sem bloqueá-las.
poll
poll() monitora um array de entradas de descritores para eventos de entrada e saída solicitados, sem a restrição fixa de valor de descritor de fd_set.
Um socket de número alto pode ser observado com poll mesmo quando seu valor não cabe em um conjunto de select.
Ponto de encontro
Um ponto de encontro é um ponto que nenhuma de duas threads pode ultrapassar até que ambas tenham anunciado sua chegada.
Cada thread executa post para sua chegada antes de esperar pela permissão de chegada da outra.
Posse de semáforo
Um semáforo não tem dono, portanto qualquer thread pode executar post para fornecer uma permissão, mesmo quando outra thread executou o wait correspondente.
Uma filha executa post em um semáforo de conclusão no qual sua pai espera.
Posse e espera
A posse e espera ocorre quando uma thread mantém recursos que já possui enquanto bloqueia à espera de recursos adicionais de que precisa.
Uma transferência mantém a trava de uma conta enquanto espera para adquirir a trava da outra.
Predicado
Um predicado é uma condição verdadeira ou falsa sobre o estado compartilhado que uma thread verifica antes de decidir se pode prosseguir.
O predicado done != 0 permite que uma thread pai verifique se sua filha terminou.
Preferência por escritores
A preferência por escritores bloqueia novos leitores quando há um escritor esperando, reduzindo o adiamento de escritores, mas podendo causar inanição de leitores.
Uma consulta que chega espera atrás do escritor do cache em vez de se juntar ao grupo atual de leitores.
Preferência por leitores
A preferência por leitores permite que leitores que chegam se juntem a um grupo ativo de leitores mesmo quando um escritor já está esperando por acesso.
Leitores com períodos de leitura sobrepostos podem manter a contagem acima de zero enquanto um escritor espera.
Prevenção de impasses
A prevenção de impasses projeta a aquisição de recursos de modo que pelo menos uma condição necessária ao impasse nunca possa surgir.
Uma ordem comum de travas de contas impede que transferências em sentidos opostos criem espera circular.
Problema do produtor e consumidor
O problema do produtor e consumidor coordena threads que adicionam e removem itens de um buffer compartilhado de capacidade limitada.
Um receptor de rede produz requisições que threads trabalhadoras consomem de uma fila limitada.
Prontidão
A prontidão informa que uma operação de entrada e saída pode ser tentada para um descritor; ela não transfere os dados para o programa.
Um socket pronto para leitura pode retornar dados ou informar fim da entrada quando lido.

R

Reserva de recursos
Uma reserva de recursos atribui uma unidade de recurso a uma operação admitida, mesmo antes que essa operação atualize os dados de controle compartilhados.
Um wait bem-sucedido por espaço reserva uma posição para o produtor enquanto ele espera pelo mutex do buffer.
Reversão
Uma reversão restaura um ponto de verificação válido anterior para que o trabalho possa liberar ou recuperar recursos sem preservar alterações parciais.
Um sistema de transações aborta e repete uma transação em impasse por meio de seu mecanismo de reversão.

S

select
select() monitora conjuntos de descritores quanto à prontidão para entrada e saída, substituindo-os por subconjuntos prontos e limitando a verificação a números de descritores menores que nfds.
Observar os descritores 5 e 12 exige nfds 13 e a reconstrução dos conjuntos observados após cada chamada.
Semáforo
Um semáforo é um objeto de sincronização baseado em um inteiro, com operações atômicas wait e post que adquirem e fornecem permissões.
Um post pode ocorrer antes de um wait porque o semáforo lembra a permissão não usada.
Semáforo binário
Um semáforo binário usado como trava começa com uma permissão e combina a aquisição antes de uma seção crítica com a liberação depois dela.
Uma segunda thread bloqueia até que a primeira devolva a permissão da seção crítica.
Semáforo contador
Um semáforo contador começa com o número de unidades idênticas disponíveis e limita quantos usuários podem adquiri-las ao mesmo tempo.
Um conjunto de conexões com quatro permissões faz um quinto usuário esperar pela devolução de uma conexão.
Semáforo de espaços livres
Um semáforo de espaços livres conta permissões para reservar posições disponíveis no buffer antes que produtores insiram seus itens concluídos.
Um buffer vazio de capacidade cinco inicializa seu semáforo de espaços livres em cinco.
Semáforo de itens disponíveis
Um semáforo de itens disponíveis conta os itens do buffer disponibilizados que consumidores podem reservar antes de removê-los do array compartilhado.
Um buffer inicialmente vazio atribui zero permissões ao seu semáforo de itens disponíveis.
Semântica de Hoare
A semântica de Hoare transfere a trava e a execução diretamente à thread notificada, preservando a condição sinalizada nessa passagem.
A thread notificada retoma a execução antes que outra thread entre e possa consumir o item recém-disponibilizado.
Semântica de Mesa
A semântica de Mesa deixa uma thread notificada pronta para competir por sua trava, de modo que ela deve verificar a condição novamente após readquiri-la.
Um segundo consumidor pode retirar o único item antes que o primeiro consumidor notificado readquira o mutex.
Sequência segura
Uma sequência segura ordena todas as threads de modo que cada demanda máxima restante caiba nos recursos disponíveis após o término das threads anteriores.
Cada etapa de uma sequência proposta deve caber em cada tipo de recurso, não apenas na contagem total.
Sinal
Um sinal notifica pelo menos uma thread que espera em uma variável de condição, quando há threads em espera, sem armazenar uma notificação para quem chegar depois.
Uma filha sinaliza após definir seu indicador de conclusão protegido, para que uma pai em espera possa verificá-lo novamente.
Suspensão
Uma suspensão pausa uma função até que uma operação aguardada termine, permitindo que o laço de eventos execute outro trabalho pronto.
Uma função assíncrona é retomada após a conclusão de sua leitura, com suas variáveis locais preservadas.

T

Tempo limite
Um tempo limite encerra uma espera após um prazo, revelando um atraso excessivo sem distinguir um impasse permanente de progresso lento.
Uma espera por mutex com prazo expirado pode resultar tanto de um sistema sobrecarregado quanto de um ciclo de travas.
Temporizador de supervisão
Um temporizador de supervisão aciona a recuperação quando o software deixa de informar progresso dentro de um prazo, sem diagnosticar exclusivamente a causa.
Um controlador embarcado reinicia após seu software parar de enviar os informes de progresso esperados.
Tentativa não bloqueante de aquisição de trava
Uma tentativa não bloqueante de aquisição de trava tenta adquirir um mutex e retorna imediatamente quando ele está indisponível, permitindo que quem chamou libere recursos e tente novamente.
Uma segunda tentativa malsucedida com trylock faz a transferência liberar a trava de sua primeira conta.
Trava de leitores e escritores
Uma trava de leitores e escritores permite que vários leitores compartilhem o acesso ou que um escritor tenha acesso exclusivo aos dados protegidos.
Várias consultas ao cache podem se sobrepor, mas atualizar o cache exige acesso de escrita.

V

Variável de condição
Uma variável de condição fornece uma fila na qual threads dormem até que uma mudança no estado compartilhado permita que verifiquem sua condição novamente.
Um consumidor espera em uma variável de condição quando a fila de requisições não contém trabalho.
Variável de estado
Uma variável de estado registra um fato sobre o trabalho compartilhado, de modo que uma thread possa testá-lo mesmo após uma notificação anterior ter passado.
Um indicador de conclusão permanece definido quando a filha termina antes de a pai começar a esperar.
Verificação seguida de ação
Uma operação de verificação seguida de ação testa o estado compartilhado e depois o altera ou usa de uma forma que depende de o teste permanecer válido.
Testar o estoque e reservá-lo devem permanecer juntos quando outras threads podem vender o mesmo item.
Vetor de necessidades
Um vetor de necessidades registra a demanda máxima restante de uma thread para cada tipo de recurso, calculada como Max menos a Allocation atual.
Uma thread com Max (4, 3) e Allocation (1, 2) tem Need (3, 1).
Vetor de recursos disponíveis
Um vetor de recursos disponíveis conta as unidades atualmente não alocadas de cada tipo de recurso, formando o valor inicial de Work para um teste de segurança.
Subtraia todas as unidades mantidas de cada total de recursos antes de começar a simulação de conclusão.
Vetor de trabalho
Um vetor de trabalho é a disponibilidade simulada do teste de segurança, aumentada pela Allocation de uma thread sempre que ela é marcada como concluída.
A conclusão de uma thread que mantém (2, 1) altera Work de (1, 3) para (3, 4).
Violação de atomicidade
Uma violação de atomicidade permite que outra thread interrompa acessos que devem se comportar como uma única operação indivisível.
Duas regiões protegidas separadas em torno de uma verificação de saldo e de um débito ainda deixam um intervalo inseguro.
Violação de ordem
Uma violação de ordem ocorre quando uma operação executa antes de uma operação anterior necessária porque a dependência entre elas não é garantida.
Uma trabalhadora lê a configuração antes que a rotina de carregamento termine de disponibilizar os valores.
Vítima selecionada para recuperação
Uma vítima selecionada para recuperação é um trabalho que o sistema escolhe abortar ou reverter para liberar recursos e desfazer um impasse detectado.
Um processo com pouco trabalho concluído pode custar menos para reiniciar do que um quase terminado.

Sobre esta aula

Por StudeliRelatar um erro

Crie seus próprios Materiais de Estudo

Transforme suas anotações, slides e documentos em Guias de Estudo, Questões de Prática, Flashcards e mais 6 ferramentas.

Começar