Como dormir certo e errado

Há pouco tempo, um bom artigo passou por nós sobre o terrível estado de desempenho do software moderno (o original em inglês , tradução em Habré ). Este artigo me lembrou um antipadrão do código, que é muito comum e geralmente funciona de alguma forma, mas leva a pequenas perdas de desempenho aqui e ali. Bem, você sabe, um pouco, que mãos não alcançarão de forma alguma. O único problema é que uma dúzia dessas "ninharias" espalhadas em diferentes lugares do código começa a levar a problemas como "Eu tenho o mais recente Intel Core i7 e a rolagem se contrai".

Estou falando do uso incorreto da função Sleep (o caso pode variar dependendo da linguagem e plataforma de programação). Então, o que é sono? A documentação responde a essa pergunta de maneira muito simples: essa é uma pausa na execução do encadeamento atual para o número especificado de milissegundos. Note-se a beleza estética do protótipo desta função:

void Sleep(DWORD dwMilliseconds); 

Apenas um parâmetro (extremamente claro), sem códigos de erro ou exceções - ele sempre funciona. Existem muito poucas funções agradáveis ​​e compreensíveis!

Você ganha ainda mais respeito por essa função quando lê como ela funciona.
A função vai para o agendador de encadeamentos do SO e diz: “Meu encadeamento e eu gostaríamos de recusar o tempo de CPU alocado para nós, agora e por milissegundos no futuro. Dê aos pobres! O agendador, levemente surpreso com essa generosidade, executa as funções de gratidão em nome do processador, dedica o tempo restante à próxima pessoa (e sempre existem) e não inclui o encadeamento que fez com que o Sleep fosse fingido transmitir o contexto de execução para o número especificado de milissegundos. Beleza!

O que poderia ter dado errado? O fato de os programadores usarem esse recurso maravilhoso não é para o que ele se destina.

E destina-se à simulação de software de algum externo, definido por algo real, processo de pausa.

Exemplo de número correto 1


Estamos escrevendo um aplicativo de "relógio" no qual, uma vez por segundo, você precisa alterar um dígito na tela (ou a posição da seta). A função Sleep aqui é perfeitamente adequada: realmente não temos nada a fazer por um período de tempo claramente definido (exatamente um segundo). Por que não dormir?

Exemplo de número correto 2


Estamos escrevendo um controlador de luar para uma máquina de pão. O algoritmo de operação é definido por um dos programas e se parece com isso:

  1. Vá para o modo 1.
  2. Trabalhe nele por 20 minutos
  3. Vá para o modo 2.
  4. Trabalhe nele por 10 minutos
  5. Desligue.

Tudo aqui também é claro: trabalhamos com o tempo, é definido pelo processo tecnológico. Usar o sono é aceitável.

Agora vamos ver exemplos do uso indevido do sono.

Quando preciso de algum exemplo de código C ++ incorreto, vou para o repositório de códigos do editor de texto do Notepad ++. Seu código é tão terrível que qualquer antipadrão definitivamente existe, até escrevi um artigo sobre isso uma vez. O Notepad ++ também não me decepcionou desta vez! Vamos ver como ele usa o sono.

Exemplo ruim número 1


Na inicialização, o Notepad ++ verifica se outra instância do processo já está em execução e, nesse caso, procura sua janela, envia uma mensagem e fecha-se. Para detectar outro processo, é usado um método padrão - o mutex denominado global. Mas o código a seguir foi escrito para procurar janelas:

 if ((!isMultiInst) && (!TheFirstOne)) { HWND hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL); for (int i = 0 ;!hNotepad_plus && i < 5 ; ++i) { Sleep(100); hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL); } if (hNotepad_plus) { ... } ... } 


O programador que escreveu esse código tentou encontrar a janela do Notepad ++ que já havia sido lançada e até imaginou uma situação em que dois processos foram iniciados literalmente ao mesmo tempo; portanto, o primeiro deles já criou um mutex global, mas ainda não criou uma janela do editor. Nesse caso, o segundo processo aguardará a criação da janela "5 vezes em 100 ms". Como resultado, não esperamos nem perdemos até 100 ms entre o momento da criação da janela e a saída do modo de suspensão.

Este é o primeiro (e um dos principais) antipadrões do uso do sono. Não estamos aguardando a ocorrência do evento, mas "por alguns milissegundos, de repente haverá sorte". Estamos esperando tanto que, por um lado, realmente não incomodamos o usuário e, por outro lado, temos a chance de esperar pelo evento de que precisamos. Sim, o usuário pode não perceber uma pausa de 100 ms ao iniciar o aplicativo. Mas se tal prática de “esperar um pouco do trator” for aceita e aceitável no projeto, isso pode terminar com o fato de que esperaremos a cada passo pelas razões mais mesquinhas. Aqui 100 ms, existem outros 50 ms e aqui 200 ms - e aqui nosso programa já está "desacelerando por alguns segundos".

Além disso, é simplesmente esteticamente desagradável ver o código que é executado por um longo tempo enquanto pode funcionar rapidamente. Nesse caso específico, pode-se usar a função SetWindowsHookEx inscrevendo-se no evento HSHELL_WINDOWCREATED - e receber uma notificação da criação da janela instantaneamente. Sim, o código se torna um pouco mais complicado, mas literalmente 3-4 linhas. E ganhamos até 100 ms! E o mais importante - não usamos mais as funções de expectativa incondicional, onde a expectativa não é incondicional.

Exemplo ruim número 2


 HANDLE hThread = ::CreateThread(NULL, 0, threadTextTroller, &trollerParams, 0, NULL); int sleepTime = 1000 / x * y; ::Sleep(sleepTime); 

Eu realmente não entendi o que exatamente e por quanto tempo esse código estava aguardando no Notepad ++, mas muitas vezes eu via o antipadrão geral "iniciar o fluxo e esperar". As pessoas esperam coisas diferentes: o início de outro fluxo, o recebimento de alguns dados dele, o fim de seu trabalho. Duas coisas estão ruins aqui imediatamente:

  1. A programação multithread é necessária para fazer algo multithread. I.e. o lançamento do segundo thread pressupõe que continuaremos a fazer algo no primeiro; nesse momento, o segundo thread fará outro trabalho e o primeiro, após concluir seu trabalho (e, talvez, ter esperado um pouco mais), obterá o resultado e, de alguma forma, utilizá-lo. Se começarmos a "dormir" imediatamente após iniciar o segundo segmento - por que é necessário?
  2. É necessário esperar corretamente. Para uma expectativa adequada, existem práticas comprovadas: o uso de eventos, funções de espera, chamadas de retorno de chamada. Se estamos aguardando o código começar a funcionar no segundo segmento, configure um evento para isso e sinalize-o no segundo segmento. Se estamos aguardando a conclusão do segundo thread - em C ++, há uma maravilhosa classe de threads e seu método de junção (bem, ou, novamente, métodos específicos da plataforma, como WaitForSingleObject e HANDLE no Windows). Esperar o trabalho ser concluído em outro encadeamento "por alguns milissegundos" é simplesmente estúpido, porque se não tivermos um sistema operacional em tempo real, ninguém lhe dará nenhuma garantia por quanto tempo esse segundo encadeamento iniciará ou atingirá algum estágio de seu trabalho.

Exemplo ruim número 3


Aqui, vemos um encadeamento em segundo plano que está dormindo, aguardando alguns eventos.

 class CReadChangesServer { ... void Run() { while (m_nOutstandingRequests || !m_bTerminate) { ::SleepEx(INFINITE, true); } } ... void RequestTermination() { m_bTerminate = true; ... } ... bool m_bTerminate; }; 

Devo admitir que não é o Sleep que é usado aqui, mas o SleepEx , que é mais inteligente e pode interromper a espera de alguns eventos (como a conclusão de operações assíncronas). Mas isso não ajuda em nada! O fato é que o loop while (! M_bTerminate) tem todo o direito de funcionar indefinidamente, ignorando o método RequestTermination () chamado de outro encadeamento, redefinindo a variável m_bTerminate para true. Eu escrevi sobre as causas e consequências disso em um artigo anterior . Para evitar isso, você deve usar algo garantido para funcionar corretamente entre os threads: atômico, evento ou algo semelhante.

Sim, formalmente o SleepEx não é o culpado pelo problema de usar a variável booleana usual para sincronizar threads. Esse é um erro separado de outra classe. Mas por que isso se tornou possível nesse código? Porque, a princípio, o programador pensou "você precisa dormir aqui" e depois pensou por quanto tempo e sob que condições parar de fazer isso. E no cenário certo, ele nem deveria ter um primeiro pensamento. O pensamento "teria que esperar por um evento" deveria ter surgido na minha cabeça - e, a partir de agora, o pensamento teria trabalhado para escolher o mecanismo certo para sincronizar dados entre threads, o que excluiria a variável booleana e o uso do SleepEx.

Exemplo ruim número 4


Neste exemplo, veremos a função backupDocument, que atua como um "salvamento automático", útil no caso de uma falha inesperada do editor. Por padrão, ela dorme por 7 segundos e, em seguida, fornece o comando para salvar as alterações (se elas foram).

 DWORD WINAPI Notepad_plus::backupDocument(void * /*param*/) { ... while (isSnapshotMode) { ... ::Sleep(DWORD(timer)); ... ::PostMessage(Notepad_plus_Window::gNppHWND, NPPM_INTERNAL_SAVEBACKUP, 0, 0); } return TRUE; } 

O intervalo pode ser alterado, mas este não é o problema. Qualquer intervalo será muito longo e muito curto ao mesmo tempo. Se digitarmos uma letra por minuto, não faz sentido dormir por apenas 7 segundos. Se copiarmos e colarmos 10 megabytes de texto de algum lugar, não precisaremos esperar mais sete segundos depois disso, será um volume grande o suficiente para iniciar o backup imediatamente (de repente, o cortamos de algum lugar e ele foi para lá, e o editor falha após um segundo).

I.e. com uma simples expectativa, substituímos o algoritmo mais inteligente que falta aqui.

Exemplo ruim número 5


O Notepad ++ pode "digitar texto" - ou seja, emule a entrada de texto humano fazendo uma pausa entre a inserção de letra. Parece estar escrito como um "ovo de Páscoa", mas você pode criar algum tipo de aplicativo funcional desse recurso ( enganando o Upwork, sim ).

 int pauseTimeArray[nbPauseTime] = {200,400,600}; const int maxRange = 200; ... int ranNum = getRandomNumber(maxRange); ::Sleep(ranNum + pauseTimeArray[ranNum%nbPauseTime]); ::SendMessage(pCurrentView->getHSelf(), SCI_DELETEBACK, 0, 0); 

O problema aqui é que o código tem uma idéia de algum tipo de "pessoa comum" que pausa de 400 a 800 ms entre cada tecla pressionada. Ok, talvez seja "médio" e normal. Mas você sabe, se o programa que eu uso faz algumas pausas no meu trabalho, simplesmente porque elas lhe parecem bonitas e adequadas - isso não significa que eu compartilhe sua opinião. Gostaria de poder ajustar a duração dos dados de pausa. E, se no caso do Notepad ++ isso não for muito crítico, em outros programas algumas vezes encontrei configurações como “atualizar dados: frequentemente, normalmente, raramente”, onde “frequentemente” não era suficiente para mim com frequência e “raramente” não era suficiente. raramente. Sim, e "normal" não era normal. Essa funcionalidade deve permitir que o usuário indique com precisão o número de milissegundos que ele gostaria de esperar até que a ação desejada seja concluída. Com a opção obrigatória para inserir "0". Além disso, nesse caso, 0 nem deve ser passado como argumento para a função Sleep, mas simplesmente excluir sua chamada (Sleep (0) na verdade não retorna instantaneamente, mas fornece o restante do intervalo de tempo fornecido pelo planejador para outro encadeamento).

Conclusões


Com a ajuda do Sleep, é possível e necessário atender a uma expectativa quando é precisamente uma expectativa atribuída incondicionalmente por um período específico de tempo e há alguma explicação lógica para que seja assim: “de acordo com a tecnologia do processo”, “o tempo é calculado de acordo com esta fórmula”, “aguarde tanto disse o cliente. " A espera de alguns eventos ou a sincronização de threads não deve ser implementada usando a função Sleep.

Source: https://habr.com/ru/post/pt427843/


All Articles