Voltar18 de fevereiro de 2024

O QUE SÃO THREADS EM PYTHON E COMO USÁ-LAS

Implementando threads para execução simultânea de ações em scripts Python

PYTHON

Capa da postagem Uma das primeiras coisas que se aprende em programação é que os algoritmos que escrevemos são sequências finitas de instruções, executadas sequencialmente. Porém, frequentemente necessitamos escrever um programa que precisa executar duas ou mais ações simultaneamente, como atualizar para o usuário, o progresso da função principal que o programa está executando. Para estes tipos de situações, utilizar “threads” em seu programa pode ser a solução, e veremos neste artigo como elas funcionam em Python.

O que são threads?

Para entender o que são threads, primeiramente precisamos falar de processos. Um processo no sistema operacional é um agrupamento de recursos, e estes são utilizados para executar seu respectivo programa no computador. O processo agrupa recursos como variáveis, arquivos abertos, memória alocada, entre outros. Todo o processo tem por padrão uma thread, e uma thread nada mais é que uma linha de execução dentro do processo, que utilizará os recursos alocados por ele, para executar seus respectivos códigos. Um mesmo processo pode ter várias threads associadas, e cada uma dessas threads executam suas tarefas independentes uma das outras, porém, compartilhando os recursos do processo. Complicou um pouco né? Pense da seguinte forma: para um programa funcionar no computador, ele precisa de recursos. O processo, então, é responsável por alocar os recursos necessários para o programa ser executado. O processo, por sua vez, terá uma ou mais threads que utilizarão de seus recursos para poderem executar as instruções do programa e fazê-lo funcionar. As threads são as linhas de execução que executam as instruções do programa, portanto, o processo fornece os recursos necessários para cada uma de suas threads funcionarem. Por isso as threads compartilham os recursos do processo, porém, funcionam de forma independente, por serem justamente linhas de execução diferentes.

Como utilizar threads em Python

Com a teoria de threads já explicada, podemos começar a entender como aplicá-las na linguagem Python. Comecemos abstraindo a seguinte situação: imagine que você fará uma caminhada, e durante ela, você quer ouvir música. É possível fazer as duas coisas simultaneamente sem problemas, certo? Com esta situação mente, temos o seguinte código em Python:

python
1import time
2
3def mostrar_tempo_decorrido(inicio, fim):
4    tempo_decorrido = round(fim - inicio)
5    print(f'Tempo decorrido: {tempo_decorrido} segundos.\n')
6
7def andar():
8    inicio = time.time()
9
10    print('Começou a andar...')
11    time.sleep(2)
12    print('Terminou de andar!')
13
14    fim = time.time()
15    mostrar_tempo_decorrido(inicio, fim)
16
17def tocar_musica():
18    inicio = time.time()
19
20    print('Música começou a tocar')
21    time.sleep(2)
22    print('Música parou de tocar!')
23
24    fim = time.time()
25    mostrar_tempo_decorrido(inicio, fim)
26
27inicio = time.time()
28andar()
29tocar_musica()
30fim = time.time()
31
32print('As funções foram executadas!')
33mostrar_tempo_decorrido(inicio, fim)

Antes mesmo de executar este código, já é possível perceber que a função tocar_musica() será executada somente após o final da função andar(). Isso seria o mesmo que, ao invés de andar ouvindo música, a pessoa andasse todo o seu trajeto, e depois ouvisse a música inteira, parada no mesmo lugar. Não tem muito sentido, não é mesmo? Esse é o resultado no console: Podemos perceber que o script levou o total de 4 segundos para poder finalizar a execução. Isso por que estamos executando as funções sequencialmente em uma única thread (a thread main), e, por isso, primeiro a função andar() é executada, levando 2 segundos, e logo após a função tocar_musica() é executada, levando mais 2 segundos. Não há paralelismo nesta situação. Agora, vamos executar as duas funções em threads separadas. O primeiro passo, é importar o módulo threading.

python
1import threading

Com isto feito, podemos criar as threads. Para fazer isso, basta criar objetos do tipo threading e acessar a classe .Thread deles, definindo no atributo target as funções criadas anteriormente. Para criar a thread da função andar(), por exemplo, a sentença de código ficaria da seguinte forma:

python
1thread_andar = threading.Thread(target=andar)

Simples, não é? Porém, tem um detalhe: até agora nós apenas criamos as threads. Esta sentença de código por si só não vai executá-la, e para isso, precisamos usar o método start() para que ela inicie de fato.

python
1thread_andar.start()

Com estes detalhes em mente, o código final ficou desta forma:

python
1import time
2import threading
3
4def mostrar_tempo_decorrido(inicio, fim):
5    tempo_decorrido = round(fim - inicio)
6    print(f'Tempo decorrido: {tempo_decorrido} segundos.\n')
7
8def andar():
9    inicio = time.time()
10
11    print('Começou a andar...')
12    time.sleep(2)
13    print('Terminou de andar!')
14
15    fim = time.time()
16    mostrar_tempo_decorrido(inicio, fim)
17
18def tocar_musica():
19    inicio = time.time()
20
21    print('Música começou a tocar')
22    time.sleep(2)
23    print('Música parou de tocar!')
24
25    fim = time.time()
26    mostrar_tempo_decorrido(inicio, fim)
27
28inicio = time.time()
29thread_andar = threading.Thread(target=andar)
30thread_tocar_musica = threading.Thread(target=tocar_musica)
31thread_andar.start()
32thread_tocar_musica.start()
33fim = time.time()
34
35print('As funções foram executadas!')
36mostrar_tempo_decorrido(inicio, fim)

Agora, cada função está sendo executada em sua própria thread, paralelamente. Ambas começam ao mesmo tempo, e ambas levam 2 segundos para terminarem sua execução, desta forma, obtemos o paralelismo no qual havíamos falado anteriormente. Note que o tempo de execução do script foi de 2 segundos: Agora as funções do script estão sendo executadas paralelamente, porém, você deve ter notado um comportamento estranho. Os prints da função principal que, antes de implementarmos as threads, eram executados apenas ao final da execução das duas funções, agora é executado junto delas. O que nos leva a seguinte questão: Problemas de sincronismo Na verdade, o comportamento apresentado é correto para o código que escrevemos. Atente-se para o seguinte detalhe: um programa, por padrão, sempre terá uma thread, a qual é chamada de main thread: a thread principal. Como nós “jogamos” as duas funções em threads separadas, elas serão executadas juntamente à thread principal. Ou seja, enquanto as nossas duas funções estão sendo executadas, a main thread executará o único papel que fornecemos a ela: o print da string 'Todas as funções foram executadas', e o print do cálculo do tempo decorrido. Aqui temos um problema de sincronismo. Este é um dos principais aspectos a considerar quando se trabalha com threads. É preciso considerar em que momento queremos que as threads sejam executadas durante a execução do programa, no nosso caso, queremos que a thread principal execute sua função apenas ao final das demais threads. O problema é o seguinte: delegamos para threads separadas, as funções andar() e tocar_musica(). Estas threads são iniciadas na thread principal, e queremos que esta continue sua execução somente após o final das demais duas. Para fazer isso, podemos utilizar o método .join(). Este método permite que duas ou mais threads comecem sua execução ao mesmo tempo, e faz com que a thread principal aguarde até que as outras terminem suas execuções. Para fazer isto, basta utilizar o método `.join() após as threads terem sido iniciadas.

python
1thread_andar = threading.Thread(target=andar)
2thread_tocar_musica = threading.Thread(target=tocar_musica)
3
4thread_andar.start()
5thread_tocar_musica.start()
6
7thread_andar.join()
8thread_tocar_musica.join()

Para ilustrar melhor ainda, alterei o código para que a função `tocar_musica() dure 3 segundos ao invés de 2. O código final ficou da seguinte forma:

python
1import time
2import threading
3
4def mostrar_tempo_decorrido(inicio, fim):
5    tempo_decorrido = round(fim - inicio)
6    print(f'Tempo decorrido: {tempo_decorrido} segundos.\n')
7
8def andar():
9    inicio = time.time()
10
11    print('Começou a andar...')
12    time.sleep(2)
13    print('\nTerminou de andar!')
14
15    fim = time.time()
16    mostrar_tempo_decorrido(inicio, fim)
17
18def tocar_musica():
19    inicio = time.time()
20
21    print('Música começou a tocar')
22    time.sleep(3)
23    print('Música parou de tocar!')
24
25    fim = time.time()
26    mostrar_tempo_decorrido(inicio, fim)
27
28inicio = time.time()
29
30thread_andar = threading.Thread(target=andar)
31thread_tocar_musica = threading.Thread(target=tocar_musica)
32
33thread_andar.start()
34thread_tocar_musica.start()
35
36thread_andar.join()
37thread_tocar_musica.join()
38
39fim = time.time()
40
41print('As funções foram executadas!')
42mostrar_tempo_decorrido(inicio, fim) 

Repare que, agora, o programa tem o comportamento que esperávamos. Passando argumentos para threads O que vimos até agora é bastante útil, porém, quando trabalhamos com funções, na maioria das vezes elas recebem argumentos. Bom, isso também é possível de se fazer com threads, e sem muita dificuldade. Vamos, então, alterar nosso código de exemplo para fazer isso. Faremos com que as funções recebam como parâmetro um número, que será utilizado para determinar o tempo que a thread levará para executar.

python
1import time
2import threading
3
4def mostrar_tempo_decorrido(inicio, fim):
5    tempo_decorrido = round(fim - inicio)
6    print(f'Tempo decorrido: {tempo_decorrido} segundos.\n')
7
8def andar(tempo):
9    inicio = time.time()
10
11    print('\nComeçou a andar...')
12    time.sleep(tempo)
13    print('\nTerminou de andar!')
14
15    fim = time.time()
16    mostrar_tempo_decorrido(inicio, fim)
17
18def tocar_musica(tempo):
19    inicio = time.time()
20
21    print('Música começou a tocar')
22    time.sleep(tempo)
23    print('Música parou de tocar!')
24
25    fim = time.time()
26    mostrar_tempo_decorrido(inicio, fim)
27
28print('Este é um exemplo de threads em Python! Digite dois números inteiros maiores que zero.\n')
29
30while True:
31    try:
32        tempo_andar = int(input('Quanto tempo levará para andar? '))
33        if tempo_andar <= 0:
34            raise ValueError
35        break
36    except ValueError:
37        print('Por favor, digite um número inteiro maior que zero.\n')
38
39while True:
40    try:
41        tempo_musica = int(input('Quanto tempo a música tem? '))
42        if tempo_musica <= 0:
43            raise ValueError
44        break
45    except ValueError:
46        print('Por favor, digite um número inteiro maior que zero.\n')
47
48inicio = time.time()
49
50thread_andar = threading.Thread(target=andar, args=(tempo_andar,))
51thread_tocar_musica = threading.Thread(target=tocar_musica, args=(tempo_musica,))
52
53thread_andar.start()
54thread_tocar_musica.start()
55
56thread_andar.join()
57thread_tocar_musica.join()
58
59fim = time.time()
60
61print('As funções foram executadas!')
62mostrar_tempo_decorrido(inicio, fim)

No código acima, primeiramente, passamos a receber um input do usuário, que será o valor das variáveis. Um tratamento de erros foi feito para que o programa não quebre, caso o usuário digite um valor inválido.

python
1while True:
2    try:
3        tempo_andar = int(input('Quanto tempo levará para andar? '))
4        if tempo_andar <= 0:
5            raise ValueError
6        break
7    except ValueError:
8        print('Por favor, digite um número inteiro maior que zero.\n') 

Com os valores coletados, basta utilizar o parâmetro args= para passar os argumentos para as funções. Note que não passamos apenas a variável, mas sim uma tupla. Isto é somente por que o parâmetro requer que uma tupla seja passada, mas caso você tenha apenas um argumento, basta passá-lo conforme feito abaixo, que o código será executado corretamente.

python
1thread_andar = threading.Thread(target=andar, args=(tempo_andar,))
2thread_tocar_musica = threading.Thread(target=tocar_musica, args=(tempo_musica,))

Por fim, nosso programa ficou da seguinte forma:

Finalizando

E estes são os essenciais para se trabalhar com threads em Python! Tenha em mente todas as atenções necessárias para se trabalhar com threads, pois você pode ter problemas inesperados na sua aplicação se não souber utilizá-las da maneira correta. Caso queira, você pode baixar os códigos-fonte neste repositório do GitHub ↗.