Garantias de segurança de thread

Esta página documenta as garantias de segurança de thread para tipos embutido na construção com threads livres do Python. As garantias aqui descritas aplicam-se ao utilizar o Python com a GIL desativada (modo threads livres). Quando a GIL está ativada, a maioria das operações é serializada implicitamente.

Para orientações gerais sobre como escrever código seguro para thread no Python com threads livres, consulte Suporte do Python para threads livres.

Níveis de segurança de threads

A documentação da API C usa os seguintes níveis para descrever as garantias de segurança de threads de cada função. Os níveis estão listados do menos para o mais seguro.

Incompatível

Uma função ou operação que não pode se tornar segura para uso concorrente mesmo com sincronização externa. Código incompatível normalmente acessa o estado global de maneira não sincronizada e deve ser chamado apenas a partir de uma única thread durante todo o tempo de vida do programa.

Exemplo: uma função que modifica o estado em todo o processo, como manipuladores de sinais ou variáveis de ambiente, onde chamadas concorrentes de quaisquer threads, mesmo com trava externo, podem conflitar com o ambiente de execução (runtime) ou com outras bibliotecas.

Compatível

Uma função ou operação que é segura para ser chamada a partir de múltiplas threads desde que o chamador forneça a sincronização externa apropriada, por exemplo mantendo uma trava durante a duração de cada chamada. Sem tal sincronização, chamadas concorrentes podem produzir condições de corrida ou corridas de dados.

Exemplo: uma função que lê ou escreve em um objeto cujo estado interno não é protegido por uma trava. Os chamadores devem garantir que duas threads não acessem o mesmo objeto ao mesmo tempo.

Seguro em objetos distintos

Uma função ou operação que é segura para ser chamada a partir de múltiplas threads sem sincronização externa, desde que cada thread opere em um objeto diferente. Duas threads podem chamar a função ao mesmo tempo, mas não devem passar o mesmo objeto (ou objetos que compartilham estado subjacente) como argumentos.

Exemplo: uma função que modifica campos de uma struct usando escritas não atômicas. Duas threads podem chamar a função cada uma em sua própria instância de struct com segurança, mas chamadas concorrentes na mesma instância exigem sincronização externa.

Seguro em objetos compartilhados

Uma função ou operação que é segura para uso concorrente no mesmo objeto. A implementação usa sincronização interna (como travas por objeto ou seções críticas) para proteger o estado mutável compartilhado, de modo que os chamadores não precisam fornecer sua própria trava.

Exemplo: PyList_GetItemRef() pode ser chamada a partir de múltiplas threads no mesmo PyListObject - ela usa sincronização interna para serializar o acesso.

Atômica

Uma função ou operação que parece atômica em relação a outras threads - ela executa instantaneamente da perspectiva de outras threads. Esta é a forma mais forte de segurança de threads.

Exemplo: PyMutex_IsLocked() realiza uma leitura atômica do estado do mutex e pode ser chamada a partir de qualquer thread a qualquer momento.

Segurança de threads para objetos lista

Ler um único elemento de uma list é atômico:

lst[i]   # list.__getitem__

Os seguintes métodos percorrem a lista e usam leituras atômicas de cada item para realizar sua função. Isso significa que eles podem retornar resultados afetados por modificações concorrentes:

item in lst
lst.index(item)
lst.count(item)

Todas as operações acima evitam adquirir travas por objeto. Elas não travam modificações concorrentes. Outras operações que mantêm uma trava não impedirão que estas observem estados intermediários.

Todas as outras operações a partir daqui bloqueiam usando o trava por objeto.

Escrever um único item via lst[i] = x é seguro para chamar a partir de múltiplas threads e não corromperá a lista.

As seguintes operações retornam novos objetos e parecem atômicas para outras threads:

lst1 + lst2    # concatena duas listas em uma nova lista
x * lst        # repete lst x vezes em uma nova lista
lst.copy()     # retorna uma cópia rasa da lista

Os seguintes métodos que operam apenas em um único elemento sem necessidade de deslocamento são atômicos:

lst.append(x)  # acrescenta ao final da lista; não é necessário deslocar elementos
lst.pop()      # remove o elemento do final da lista; não é necessário deslocar os demais elementos

O método clear() também é atômico. Outras threads não podem observar elementos sendo removidos.

O método sort() não é atômico. Outras threads não podem observar estados intermediários durante a ordenação, mas a lista parece vazia durante a execução da ordenação.

As seguintes operações podem permitir que operações livres de travas observem estados intermediários, pois modificam múltiplos elementos no local (in-place):

lst.insert(idx, item)  # desloca elementos
lst.pop(idx)           # idx não no fim da lista, desloca elementos
lst *= x               # cópia elementos no local

O método remove() pode permitir modificações concorrentes, pois a comparação de elementos pode executar código Python arbitrário (via __eq__()).

extend() é seguro para chamar a partir de múltiplas threads. No entanto, suas garantias dependem do iterável passado para ele. Se for uma list, uma tuple, um set, um frozenset, um dict ou um objeto de visão de dicionário (mas não suas subclasses), a operação extend estará segura de modificações concorrentes no iterável. Caso contrário, um iterador é criado, o qual pode ser modificado concorrentemente por outra thread. O mesmo se aplica à concatenação no local de uma lista com outros iteráveis ao usar lst += iterable.

De forma semelhante, a atribuição a uma fatia de lista com lst[i:j] = iterable é segura para ser chamada a partir de múltiplas threads, mas iterable só é travado quando também for uma list (mas não suas subclasses).

Operações que envolvem múltiplos acessos, assim como iteração, nunca são atômicas. Por exemplo:

# NÃO é atômico: lê-modifica-escreve
lst[i] = lst[i] + 1

# NÃO é atômico: verifica-e-então-age
if lst:
    item = lst.pop()

# NÃO é seguro para thread: iteração enquanto modifica
for item in lst:
    process(item)  # outra thread pode modificar lst

Considere a sincronização externa ao compartilhar instâncias de list entre threads.

Segurança de threads para objetos dict

Criar um dicionário com o construtor dict é atômico quando o argumento passado for um dict ou uma tuple. Ao usar o método dict.fromkeys(), a criação do dicionário é atômica quando o argumento for um dict, tuple, set ou frozenset.

As seguintes operações e funções são livres de travas e atômicas.

d[key]       # dict.__getitem__
d.get(key)   # dict.get
key in d     # dict.__contains__
len(d)       # dict.__len__

Todas as outras operações a partir daqui mantêm a trava por objeto.

Escrever ou remover um único item é seguro para chamar a partir de múltiplas threads e não corromperá o dicionário:

d[key] = value        # escreve
del d[key]            # exclui
d.pop(key)            # remove e retorna
d.popitem()           # remove e retorna último item
d.setdefault(key, v)  # insere se não existir

Essas operações podem comparar chaves usando __eq__(), o que pode executar código Python arbitrário. Durante tais comparações, o dicionário pode ser modificado por outra thread. Para tipos embutidos como str, int e float, que implementam __eq__() em C, o trava subjacente não é liberado durante as comparações e isso não é uma preocupação.

As seguintes operações retornam novos objetos e mantêm a trava por objeto durante a duração da operação:

d.copy()      # retorna uma cópia rasa do dicionário
d | other     # combina dois dicionários em um novo dicionário
d.keys()      # retorna um novo objeto de visão dict_keys
d.values()    # retorna um novo objeto de visão dict_values
d.items()     # retorna um novo objeto de visão dict_items

O método clear() mantém a trava durante sua execução. Outras threads não podem observar elementos sendo removidos.

As seguintes operações travariam ambos os dicionários. Para update() e |=, isso se aplica apenas quando o outro operando for um dict que usa o iterador de dicionário padrão (mas não subclasses que substituem a iteração). Para comparação de igualdade, isso se aplica a dict e suas subclasses:

d.update(outro_dict)  # ambos travados quando outro_dict é um dict
d |= outro_dict       # ambos travados quando outro_dict é um dict
d == outro_dict       # ambos travados para dict e subclasses

Todas as operações de comparação também comparam valores usando __eq__(), portanto, para tipos não embutidos, a trava pode ser liberada durante a comparação.

fromkeys() trava tanto o novo dicionário quanto o iterável quando o iterável for exatamente um dict, set ou frozenset (não subclasses):

dict.fromkeys(a_dict)      # trava ambas
dict.fromkeys(a_set)       # trava ambas
dict.fromkeys(a_frozenset) # trava ambas

Ao atualizar a partir de um iterável que não é dicionário, apenas o dicionário de destino é travada. O iterável pode ser modificado concorrentemente por outra thread:

d.update(iterável)        # iterável não é um dict: somente d travado
d |= iterável             # iterável não é um dict: somente d travado
dict.fromkeys(iterável)   # iterável não é um dict/conjunto/frozenset: somente o resultado travado

Operações que envolvem múltiplos acessos, assim como iteração, nunca são atômicas:

# NÃO é atômico: lê-modifica-escreve
d[key] = d[key] + 1

# NÃO é atômico: verifica-e-então-age (TOCTOU)
if key in d:
    del d[key]

# NÃO é seguro para thread: iteração enquanto modifica
for key, value in d.items():
    process(key)  # outa thread pode modificar d

Para evitar problemas de tempo de verificação para tempo de uso (TOCTOU, do inglês time-of-check to time-of-us), use operações atômicas ou trata exceções:

# Usa pop() com um valor padrão em vez de verificar-e-então-excluir
d.pop(key, None)

# ou trata a exceção
try:
    del d[key]
except KeyError:
    pass

Para iterar com segurança sobre um dicionário que pode ser modificado por outra thread, itere sobre uma cópia:

# Faz uma cópia para iterar com segurança
for key, value in d.copy().items():
    process(key)

Considere a sincronização externa ao compartilhar instâncias de dict entre threads.

Segurança de thread para objetos conjunto

A função len() é livre de trava e atômica.

A seguinte operação de leitura é livre de trava. Ela não bloqueia modificações concorrentes e pode observar estados intermediários de operações que mantêm a trava por objeto:

elem in s    # set.__contains__

Esta operação pode comparar elementos usando __eq__(), o que pode executar código Python arbitrário. Durante tais comparações, o conjunto pode ser modificado por outra thread. Para tipos embutidos como str, int e float, __eq__() não libera a trava subjacente durante as comparações e isso não é uma preocupação.

Todas as outras operações a partir daqui mantêm a trava por objeto.

Adicionar ou remover um único elemento é seguro para chamar a partir de múltiplas threads e não corromperá o conjunto:

s.add(elem)      # adiciona elemento
s.remove(elem)   # remove elemento, levanta se estiver faltando
s.discard(elem)  # remove elemento se estiver presente
s.pop()          # remove e retorna elemento arbitrário

Essas operações também comparam elementos; portanto, aplicam-se as mesmas considerações sobre __eq__() mencionadas acima.

O método copy() retorna um novo objeto e mantém a trava por objeto durante toda a execução para que seja sempre atômico.

O método clear() mantém a trava durante sua execução. Outras threads não podem observar elementos sendo removidos.

As seguintes operações aceitam apenas set ou frozenset como operandos e sempre travam ambos os objetos:

s |= outro                   # outro deve ser conjunto/frozenset
s &= outro                   # outro deve ser conjunto/frozenset
s -= outro                   # outro deve ser conjunto/frozenset
s ^= outro                   # outro deve ser conjunto/frozenset
s & outro                    # outro deve ser conjunto/frozenset
s | outro                    # outro deve ser conjunto/frozenset
s - outro                    # outro deve ser conjunto/frozenset
s ^ outro                    # outro deve ser conjunto/frozenset

set.update(), set.union(), set.intersection() and set.difference() can take multiple iterables as arguments. They all iterate through all the passed iterables and do the following:

set.symmetric_difference() tries to lock both objects.

The update variants of the above methods also have some differences between them:

The following methods always try to lock both objects:

s.isdisjoint(other)          # both locked
s.issubset(other)            # both locked
s.issuperset(other)          # both locked

Operações que envolvem múltiplos acessos, assim como iteração, nunca são atômicas:

# NOT atomic: check-then-act
if elem in s:
      s.remove(elem)

# NOT thread-safe: iteration while modifying
for elem in s:
      process(elem)  # another thread may modify s

Consider external synchronization when sharing set instances across threads. See Suporte do Python para threads livres for more information.

Thread safety for bytearray objects

A função len() é livre de trava e atômica.

Concatenation and comparisons use the buffer protocol, which prevents resizing but does not hold the per-object lock. These operations may observe intermediate states from concurrent modifications:

ba + other    # may observe concurrent writes
ba == other   # may observe concurrent writes
ba < other    # may observe concurrent writes

Todas as outras operações a partir daqui mantêm a trava por objeto.

Reading a single element or slice is safe to call from multiple threads:

ba[i]        # bytearray.__getitem__
ba[i:j]      # slice

The following operations are safe to call from multiple threads and will not corrupt the bytearray:

ba[i] = x         # write single byte
ba[i:j] = values  # write slice
ba.append(x)      # append single byte
ba.extend(other)  # extend with iterable
ba.insert(i, x)   # insert single byte
ba.pop()          # remove and return last byte
ba.pop(i)         # remove and return byte at index
ba.remove(x)      # remove first occurrence
ba.reverse()      # reverse in place
ba.clear()        # remove all bytes

Slice assignment locks both objects when values is a bytearray:

ba[i:j] = other_bytearray  # both locked

The following operations return new objects and hold the per-object lock for the duration:

ba.copy()     # returns a shallow copy
ba * n        # repeat into new bytearray

The membership test holds the lock for its duration:

x in ba       # bytearray.__contains__

All other bytearray methods (such as find(), replace(), split(), decode(), etc.) hold the per-object lock for their duration.

Operações que envolvem múltiplos acessos, assim como iteração, nunca são atômicas:

# NOT atomic: check-then-act
if x in ba:
    ba.remove(x)

# NOT thread-safe: iteration while modifying
for byte in ba:
    process(byte)  # another thread may modify ba

To safely iterate over a bytearray that may be modified by another thread, iterate over a copy:

# Make a copy to iterate safely
for byte in ba.copy():
    process(byte)

Consider external synchronization when sharing bytearray instances across threads. See Suporte do Python para threads livres for more information.

Thread safety for memoryview objects

memoryview objects provide access to the internal data of an underlying object without copying. Thread safety depends on both the memoryview itself and the underlying buffer exporter.

The memoryview implementation uses atomic operations to track its own exports in the free-threaded build. Creating and releasing a memoryview are thread-safe. Attribute access (e.g., shape, format) reads fields that are immutable for the lifetime of the memoryview, so concurrent reads are safe as long as the memoryview has not been released.

However, the actual data accessed through the memoryview is owned by the underlying object. Concurrent access to this data is only safe if the underlying object supports it:

  • For immutable objects like bytes, concurrent reads through multiple memoryviews are safe.

  • For mutable objects like bytearray, reading and writing the same memory region from multiple threads without external synchronization is not safe and may result in data corruption. Note that even read-only memoryviews of mutable objects do not prevent data races if the underlying object is modified from another thread.

# NOT safe: concurrent writes to the same buffer
data = bytearray(1000)
view = memoryview(data)
# Thread 1: view[0:500] = b'x' * 500
# Thread 2: view[0:500] = b'y' * 500
# Safe: use a lock for concurrent access
import threading
lock = threading.Lock()
data = bytearray(1000)
view = memoryview(data)

with lock:
    view[0:500] = b'x' * 500

Resizing or reallocating the underlying object (such as calling bytearray.resize()) while a memoryview is exported raises BufferError. This is enforced regardless of threading.