domingo, 8 de maio de 2011

Sobre polling, futuros e execução em paralelo

Uma das maiores preocupações na computação atual é economizar energia; algo bastante necessário em aparelhos portáteis (laptops, tablets, handhelds). Sua CPU é capaz de assumir diferentes tipos de estados de baixa-potência quando inativa. Quanto mais longo o período de inatividade, mais profundo o estado de baixa-potência, e maior a economia de energia, levando a uma carga de bateria mais durável.

Estados de baixa-potência têm um inimigo: "polling" (literalmente, votação). Quando uma tarefa ativa a CPU, mesmo que para algo trivial como leitura de endereços de memória para checar por mudanças que possam ter acontecido, a CPU deixa o estado de baixa-potência, ativa-se e ativa as estruturas internas, e só retornará a um estado de baixa-potência muito depois da simples leitura de memória ter finalizado o trabalho. Este tipo de atividade diminui muito a duração da carga da bateria. Até mesmo a Intel parece preocupada..

Python 3.2 apresenta um novo módulo padrão que inicia tarefas concomitantes e as espera terminar: o módulo concurrent.futures. Revendo o código presente neste módulo, percebemos que utiliza "polling" em alguns dos processos e "threads". Dizemos alguns, pois a implementação difere entre ThreadPoolExecutor e ProcessPoolExecutor. O primeiro produz "polling" entre todas as "threads" em funcionamento, enquanto o último só produz em uma "thread" apenas, conhecida como a "thread" de gerenciamento da fila de processo., a qual é utilizada para comunicar-se com a processos em funcionamento.

Aqui, "polling" foi usado para uma coisa só: detectar quando um processo de desligamento deve ser iniciado. Outras tarefas, tais como organizar procedimentos "callables" ou busca de resultados de tarefas prévias na fila, usam objetos sincronizados na fila. Estes objetos na fila veem tanto de "threading" ou de módulos de multiprocessamento, o que depende do que o implementação do executador estiver usando.

Desta maneira, apresentamos uma solução bem simples: substituímos o "polling" por uma sentinela (sentinel em inglês), chamado de "None". Quando uma fila recebe um "None", uma tarefa em processo é chamada, é checa se deve ser desligada ou não. No ProcessPoolExecutor, há uma pequena complicação, já que devemos reiniciar N tarefas, além da "thread" que gerencia a fila.

No "patch" inicial, ainda tínhamos um contador de "polling"; um contador bem longo (10 minutos), esperando que as tarefas se reiniciassem nesse período. Este contador era longo para casos de código com erros que não recebessem a tempo uma notificação de desligamento do sentinela mencionado acima. Por curiosidade, checamos o código fonte do módulo de multiprocessamento e tivemos uma outra observação bem interessante: em Windows, multiprocessing.Queue.get() com um contador diferente de zero e não infinito usa ... "polling" (por o qual, abrimos o problema 11668). Ele usa um "polling" de alta-frequência muito interessante, já que se inicia com um contador de um milissegundo, o qual é incrementado a cada iteração loop.

Desnecessário comentar que se utilizar um contador, não importando o quão longo, faria com que nosso patch fosse inútil no Windows, visto que a maneira que o contador foi implementado envolveria chamadas a casa milissegundo. Fizemos um sacrifício e removemos o enorme contador do "pooling". Nosso último patch não usa um contador, o que deve limitar o número de chamadas periódicas em qualquer plataforma.

Historicamente, antes do Python 3.2, cada contador no módulo de threading usa "polling", e por consequência em grande parte do multiprocessamento, o qual usa tarefas de processamento, usa "polling". Isto foi corrigido no número 7316.

Nenhum comentário:

Postar um comentário