Deadlocks no Two-Phase Locking
Objetivos de Aprendizagem
- Explicar por que a fase de crescimento do 2PL pode entrar em deadlock, e por que esse deadlock tem exatamente a mesma estrutura de um deadlock de recursos no nível do SO.
- Construir um grafo de espera para um agendamento real de várias transações e usá-lo para detectar um ciclo de deadlock.
- Descrever como um sistema resolve um deadlock detectado escolhendo e abortando uma transação vítima.
- Explicar os esquemas de prevenção de deadlock wait-die e wound-wait e como os carimbos de tempo das transações decidem quem espera e quem aborta.
Contexto e Motivação
two-phase-locking-and-conflict-serializability construiu a fase de crescimento do 2PL (adquirir todo lock de que uma transação precisa antes de liberar qualquer um) como o mecanismo que garante a serializabilidade por conflito. Essa mesma fase de crescimento introduz um novo modo de falha que deadlock-conditions-and-detection, de computer/operating-systems-i, já definiu com precisão, só que numa granularidade diferente: um deadlock é um ciclo de transações, cada uma esperando para adquirir um lock atualmente detido pela próxima transação do ciclo, sem nenhuma transação conseguindo prosseguir. A estrutura é idêntica à de um deadlock de recursos no nível do SO (exclusão mútua, hold-and-wait, sem preempção e espera circular), exceto que os "recursos" são tuplas/páginas de banco de dados em vez de dispositivos ou memória gerenciados pelo SO, e os "processos" são transações em vez de threads do SO.
Teoria Central
O deadlock como um ciclo no grafo de espera
Exatamente como deadlock-conditions-and-detection construiu para recursos do SO, um grafo de espera (waits-for graph) para um conjunto de transações ativas tem um nó por transação e uma aresta dirigida Tᵢ → Tⱼ sempre que Tᵢ está atualmente bloqueada esperando por um lock detido por Tⱼ. Um deadlock existe se e somente se este grafo contém um ciclo: um conjunto de transações cada uma esperando pela próxima, sem forma de nenhuma delas jamais progredir, já que cada uma segura algo de que a próxima do ciclo precisa e nenhuma vai liberar nada até adquirir aquilo pelo qual está esperando.
Detectando e resolvendo um deadlock
Um SGBD checa periodicamente o seu grafo de espera atrás de ciclos (usando a mesma técnica de detecção de ciclos que deadlock-conditions-and-detection já construiu para grafos de recursos do SO). Quando um ciclo é encontrado, o sistema precisa quebrá-lo escolhendo uma transação vítima para abortar, liberando todos os seus locks e revertendo o seu trabalho, o que libera o recurso pelo qual a próxima transação do ciclo estava esperando e deixa as transações restantes do ciclo prosseguirem. Sistemas reais tipicamente escolhem uma vítima usando critérios como a quantidade de trabalho já feito (abortar uma transação que fez o mínimo de trabalho desperdiça o mínimo de esforço) ou quantas outras transações precisariam ser revertidas como consequência, em vez de escolher arbitrariamente.
Prevenção de deadlock: wait-die e wound-wait
Detectar ciclos depois que eles já se formaram significa que algumas transações genuinamente ficam bloqueadas, incapazes de progredir, até que uma checagem periódica pegue o ciclo. Sistemas reais podem, em vez disso, prevenir que os deadlocks sequer se formem, usando o carimbo de início de cada transação (atribuído uma vez, quando a transação começa, e nunca alterado, mesmo que essa transação depois aborte e reinicie) para decidir, no momento de um conflito de lock, se quem pede deve esperar ou deve ela mesma abortar:
- Wait-die: quando a transação
Tᵢpede um lock detido porTⱼ, seTᵢfor mais velha queTⱼ(começou antes),Tᵢtem permissão para esperar. SeTᵢfor mais nova queTⱼ,Tᵢmorre: aborta imediatamente em vez de esperar, e reinicia depois, mantendo o seu carimbo original, de modo que ela eventualmente se torne a transação mais velha do sistema e tenha garantia de vencer qualquer conflito futuro, evitando que fique em inanição para sempre. - Wound-wait: quando
Tᵢpede um lock detido porTⱼ, seTᵢfor mais velha queTⱼ,TᵢfereTⱼ: forçaTⱼa abortar imediatamente, fazendo a sua preempção, e toma o lock. SeTᵢfor mais nova,Tᵢespera.
Os dois esquemas só deixam uma transação mais velha esperar por uma mais nova ou só o contrário, nunca as duas coisas, e é exatamente isso que torna um ciclo no grafo de espera estruturalmente impossível: um ciclo exigiria que alguma transação estivesse esperando por uma que fosse ao mesmo tempo mais velha e mais nova que ela, o que a regra de ordenação acima nunca permite acontecer.
Exemplos Resolvidos
Exemplo 1: construindo o grafo de espera e encontrando um ciclo real
Três transações rodam concorrentemente: T1 segura XLock(A) e pede XLock(B); T2 segura XLock(B) e pede XLock(C); T3 segura XLock(C) e pede XLock(A). O grafo de espera tem exatamente as arestas T1 → T2 (T1 espera pelo lock que T2 segura), T2 → T3 e T3 → T1: um ciclo de 3 nós, confirmando um deadlock. Nenhuma das três jamais consegue adquirir o lock pelo qual está esperando, porque cada um é detido pela próxima transação do mesmo ciclo, e nenhuma vai liberar o próprio lock detido até que o seu próprio pedido tenha sucesso.
Exemplo 2: resolvendo o deadlock escolhendo uma vítima
Detectando o ciclo do Exemplo 1, o sistema precisa abortar uma de T1, T2 ou T3 para quebrá-lo. Suponha que T3 fez o mínimo de trabalho até agora (ela acabou de adquirir XLock(C) e fazer o seu primeiro pedido); o sistema escolhe T3 como vítima, a aborta e libera XLock(C). T2, que estava esperando exatamente por esse lock, agora consegue adquiri-lo e prosseguir; T1 continua esperando por T2 até que T2 eventualmente termine e libere XLock(B). O ciclo é quebrado com a menor quantidade de trabalho desperdiçado, embora a própria T3 agora precise reiniciar do zero.
Exemplo 3: wait-die e wound-wait impedindo que o mesmo deadlock sequer se forme
Atribua os carimbos de início T1 = 100 (a mais velha), T2 = 105, T3 = 110 (a mais nova), e repita os mesmos pedidos de lock do Exemplo 1 usando wait-die: T1 (100) pede um lock detido por T2 (105); T1 é mais velha, então T1 espera. T2 (105) pede um lock detido por T3 (110); T2 é mais velha, então T2 espera. Agora T3 (110) pede um lock detido por T1 (100); T3 é mais nova que T1, então, sob wait-die, T3 morre imediatamente em vez de esperar, abortando e liberando XLock(C) antes que a terceira aresta do ciclo possa sequer se formar. O deadlock do Exemplo 1 nunca chega de fato a se completar. Repetir o mesmo cenário com wound-wait o resolve ainda mais cedo: T1 (100), pedindo um lock detido por T2 (105), é agora a transação mais velha, então, em vez de esperar, T1 fere T2, forçando T2 a abortar imediatamente no primeiríssimo conflito, antes que T2 chegue sequer a fazer o seu próprio pedido contra T3.
Equívocos Comuns e Armadilhas
- "Deadlocks de banco de dados e deadlocks de recursos do SO só são superficialmente parecidos." A estrutura é idêntica, e não meramente análoga: os dois são precisamente um ciclo num grafo de espera entre entidades (transações ou processos) cada uma segurando algo de que a próxima do ciclo precisa. A técnica de detecção de ciclos de
deadlock-conditions-and-detectionse transfere diretamente, com a única diferença real sendo aquilo pelo qual se espera (um lock de tupla/página de banco de dados versus um recurso gerenciado pelo SO). - "Wait-die e wound-wait garantem que nenhuma transação jamais seja abortada." Os dois esquemas são de prevenção de deadlock, e não de evitação de deadlock sem custo algum: o Exemplo 3 mostra transações reais genuinamente abortando (
T3sob wait-die,T2sob wound-wait) especificamente para impedir que um ciclo jamais se complete. O ganho é que o aborto acontece de forma proativa, num único ponto de conflito, em vez de depois que várias transações ficam totalmente bloqueadas num ciclo inquebrável. - "Wait-die e wound-wait produzem o mesmo resultado, só com nomes diferentes." O Exemplo 3 mostra os dois esquemas resolvendo o mesmo cenário de forma diferente: o wait-die deixa
T1eT2esperarem inicialmente e só abortaT3no ponto em que o ciclo de outro modo se completaria, enquanto o wound-wait aborta preventivamenteT2no primeiríssimo conflito, antes queT3sequer esteja envolvida. As duas regras (a mais nova morre vs. a mais nova espera, a mais velha espera vs. a mais velha fere) são inversas uma da outra, e não formulações equivalentes de uma mesma regra.
Resumo
A fase de crescimento do 2PL pode entrar em deadlock exatamente do jeito que deadlock-conditions-and-detection já definiu um deadlock para recursos do SO (um ciclo de transações, cada uma segurando um lock de que a próxima do ciclo precisa), só que na granularidade de tuplas/páginas de banco de dados. Um SGBD pode detectar isso depois do fato construindo um grafo de espera e checando ciclos (o ciclo real de 3 transações do Exemplo 1), resolvendo-o ao abortar uma vítima escolhida (Exemplo 2), ou impedir que ele sequer se forme usando os carimbos de início das transações: wait-die (quem pede mais velha espera, mais nova morre) ou wound-wait (quem pede mais velha fere quem detém, mais nova espera), ambos mostrados no Exemplo 3 quebrando o ciclo idêntico antes que ele possa jamais se completar, pela construção da regra de ordenação, e não por detectar o ciclo depois do fato.