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.
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.update()andset.union()lock both objects only when
set.intersection()andset.difference()always try to lockall objects.
set.symmetric_difference() tries to lock both objects.
The update variants of the above methods also have some differences between them:
set.difference_update()andset.intersection_update()tryto lock all objects one-by-one.
set.symmetric_difference_update()only locks the arguments if it is
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 writesTodas 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] # sliceThe 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 bytesSlice assignment locks both objects when values is a
bytearray:ba[i:j] = other_bytearray # both lockedThe 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 bytearrayThe 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 baTo 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
bytearrayinstances 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.