O que é o bloqueio global de intérprete (GIL) no CPython?

244

O que é um bloqueio de intérprete global e por que é um problema?

Muito barulho foi feito para remover o GIL do Python, e eu gostaria de entender por que isso é tão importante. Eu nunca escrevi um compilador nem um intérprete, então não seja frugal com detalhes, provavelmente precisarei que eles entendam.

e-satis
fonte
3
Assista David Beazley contar tudo o que você sempre quis saber sobre o GIL.
hughdbrown
1
Aqui está um longo artigo falando sobre o GIL e o threading em Python que escrevi há algum tempo. Ele entra em uma boa quantidade de detalhes sobre ele: jessenoller.com/2009/02/01/...
jnoller
Aqui está um código que demonstra os efeitos do GIL: github.com/cankav/python_gil_demonstration
Can Kavaklıoğlu
3
Acho que essa é a melhor explicação para o GIL. Por favor leia. dabeaz.com/python/UnderstandingGIL.pdf
suhao399
realpython.com/python-gil Achei isso útil
qwr 4/02/19

Respostas:

220

O GIL do Python destina-se a serializar o acesso a intérpretes internos de diferentes threads. Em sistemas com vários núcleos, isso significa que vários threads não podem efetivamente usar vários núcleos. (Se o GIL não levou a esse problema, a maioria das pessoas não se importaria com o GIL - ele só está sendo levantado como um problema devido à crescente prevalência de sistemas com vários núcleos.) Se você quiser entendê-lo em detalhes, você pode ver este vídeo ou ver este conjunto de slides . Pode ser muita informação, mas você pediu detalhes :-)

Observe que o GIL do Python é realmente apenas um problema para o CPython, a implementação de referência. Jython e IronPython não têm um GIL. Como desenvolvedor Python, você geralmente não encontra o GIL, a menos que esteja escrevendo uma extensão C. Os gravadores de extensão C precisam liberar o GIL quando suas extensões bloqueiam a E / S, para que outros threads no processo Python tenham a chance de executar.

Vinay Sajip
fonte
46
Boa resposta - basicamente significa que os threads no Python são bons apenas para bloquear E / S; seu aplicativo nunca vai acima core de 1 CPU de uso do processador
Ana Betts
8
"Como desenvolvedor de Python, você geralmente não encontra o GIL a menos que esteja escrevendo uma extensão C" - talvez você não saiba que a causa do código multiencadeado em execução no ritmo dos caracóis é o GIL, mas você ' certamente sentirei seus efeitos. Ainda me surpreende que, para tirar proveito de um servidor de 32 núcleos com Python, seja necessário 32 processos com toda a sobrecarga associada.
Basic
6
@ PaulBetts: não é verdade. É provável que o código crítico desempenho já usa extensões C que podem e liberam GIL por exemplo, regex, lxml, numpymódulos. Cython permite liberar GIL no código personalizado, por exemplo,b2a_bin(data)
JFS
5
@ Paul Betts: Você pode obter acima de 1 código de CPU do uso do processador usando o módulo de multiprocessamento . Criar múltiplos processos é "mais pesado" do que criar vários threads, mas se você realmente precisar realizar um trabalho paralelo, em python, é uma opção.
AJNeufeld
1
@david_adler Sim, ainda é o caso e provavelmente permanecerá por um tempo ainda. Isso realmente não impediu o Python de ser realmente útil para muitas cargas de trabalho diferentes.
Vinay Sajip 18/09/19
59

Suponha que você tenha vários threads que realmente não tocam nos dados um do outro. Aqueles devem executar o mais independentemente possível. Se você tem um "bloqueio global" que precisa adquirir para (por exemplo) chamar uma função, isso pode acabar como um gargalo. Você pode acabar não obtendo muitos benefícios por ter vários threads em primeiro lugar.

Para colocá-lo em uma analogia do mundo real: imagine 100 desenvolvedores trabalhando em uma empresa com apenas uma caneca de café. A maioria dos desenvolvedores gastava seu tempo esperando café em vez de codificar.

Nada disso é específico do Python - não sei os detalhes sobre o que o Python precisava de um GIL para começar. No entanto, espero que tenha lhe dado uma idéia melhor do conceito geral.

Jon Skeet
fonte
Exceto aguardar a caneca de café parece um processo vinculado de E / S, pois eles certamente podem fazer outras coisas enquanto aguardam a caneca. O GIL tem muito pouco efeito nos encadeamentos pesados ​​de E / S que passam a maior parte do tempo aguardando de qualquer maneira.
Cruncher
36

Vamos primeiro entender o que o python GIL fornece:

Qualquer operação / instrução é executada no intérprete. O GIL garante que o intérprete seja mantido por um único thread em um determinado instante de tempo . E seu programa python com vários threads funciona em um único intérprete. A qualquer momento específico, esse intérprete é mantido por um único thread. Isso significa que apenas o segmento que está segurando o intérprete está sendo executado a qualquer instante .

Agora, por que isso é um problema:

Sua máquina pode ter vários núcleos / processadores. E múltiplos núcleos permitem que vários threads sejam executados simultaneamente, ou seja, vários threads podem ser executados a qualquer instante específico. . Porém, como o intérprete é mantido por um único encadeamento, outros encadeamentos não estão fazendo nada, apesar de terem acesso a um núcleo. Portanto, você não está obtendo nenhuma vantagem fornecida por vários núcleos porque, a qualquer instante, apenas um único núcleo, que é o núcleo que está sendo usado pelo encadeamento que está mantendo o intérprete, está sendo usado. Portanto, seu programa levará tanto tempo para ser executado como se fosse um único programa encadeado.

No entanto, operações potencialmente bloqueadoras ou de longa execução, como E / S, processamento de imagens e processamento de números NumPy, ocorrem fora do GIL. Tomado daqui . Portanto, para essas operações, uma operação multithread ainda será mais rápida que uma única operação encadeada, apesar da presença do GIL. Portanto, o GIL nem sempre é um gargalo.

Edit: GIL é um detalhe de implementação do CPython. IronPython e Jython não têm GIL, portanto, um programa verdadeiramente multithread deve ser possível neles, embora eu nunca tenha usado PyPy e Jython e não tenha certeza disso.

Akshar Raaj
fonte
4
Nota : PyPy tem o GIL . Referência : http://doc.pypy.org/en/latest/faq.html#does-pypy-have-a-gil-why . Enquanto o Ironpython e o Jython não possuem o GIL.
Tasdik Rahman
De fato, o PyPy tem um GIL, mas o IronPython não.
Emmanuel
@Emmanuel Editou a resposta para remover o PyPy e incluir o IronPython.
Akshar Raaj
17

Python não permite multi-threading no sentido mais verdadeiro da palavra. Ele possui um pacote multiencadeado, mas se você deseja multiencadear para acelerar seu código, geralmente não é uma boa ideia usá-lo. Python tem uma construção chamada Global Interpreter Lock (GIL).

https://www.youtube.com/watch?v=ph374fJqFPE

O GIL garante que apenas um dos seus 'threads' possa ser executado a qualquer momento. Um segmento adquire o GIL, faz um pouco de trabalho e passa o GIL para o próximo segmento. Isso acontece muito rapidamente e, para o olho humano, pode parecer que seus threads estão executando paralelamente, mas na verdade eles estão apenas se revezando usando o mesmo núcleo da CPU. Toda essa passagem do GIL adiciona sobrecarga à execução. Isso significa que, se você deseja que seu código seja executado mais rapidamente, o uso do pacote threading geralmente não é uma boa ideia.

Existem razões para usar o pacote de threading do Python. Se você deseja executar algumas coisas simultaneamente, e a eficiência não é uma preocupação, é totalmente adequado e conveniente. Ou, se você estiver executando um código que precisa esperar por algo (como algumas IO), pode fazer muito sentido. Mas a biblioteca de encadeamentos não permitirá que você use núcleos extras da CPU.

A multithreading pode ser terceirizada para o sistema operacional (executando o multiprocessamento), algum aplicativo externo que chama seu código Python (por exemplo, Spark ou Hadoop) ou algum código que seu código Python chama (por exemplo: você pode ter seu Python código chama uma função C que faz o material multiencadeado caro).

Ijaz Ahmad Khan
fonte
15

Sempre que dois threads têm acesso à mesma variável, você tem um problema. No C ++, por exemplo, a maneira de evitar o problema é definir algum bloqueio mutex para impedir que dois threads entrem, digamos, no setter de um objeto ao mesmo tempo.

Multithreading é possível em python, mas dois threads não podem ser executados ao mesmo tempo em uma granularidade mais fina que uma instrução python. O encadeamento em execução está recebendo um bloqueio global chamado GIL.

Isso significa que, se você começar a escrever um código multithread para tirar proveito do seu processador multicore, seu desempenho não melhorará. A solução alternativa usual consiste em executar vários processos.

Observe que é possível liberar o GIL se você estiver dentro de um método que você escreveu em C, por exemplo.

O uso de um GIL não é inerente ao Python, mas a alguns de seus intérpretes, incluindo o CPython mais comum. (#editado, ver comentário)

O problema GIL ainda é válido no Python 3000.

fulmicoton
fonte
Stackless ainda tem um GIL. O Stackless não melhora o encadeamento (como no módulo) - ele oferece um método diferente de programação (corotinas) que tentam contornar o problema, mas exigem funções não-bloqueadoras.
jnoller
E o novo GIL no 3.2?
precisa saber é o seguinte
Apenas para adicionar que você não tem um problema / precisa de mutexes / semáforos se apenas um thread atualizar a memória. @ new123456 reduz a contenção e agende melhor os threads sem prejudicar o desempenho de thread único (o que é impressionante por si só), mas ainda é um bloqueio global.
básico
14

Documentação do Python 3.7

Gostaria também de destacar a seguinte citação da documentação do Pythonthreading :

Detalhes da implementação do CPython: No CPython, devido ao Bloqueio Global de Intérpretes, apenas um encadeamento pode executar o código Python de uma só vez (mesmo que certas bibliotecas orientadas ao desempenho possam superar essa limitação). Se você deseja que seu aplicativo faça melhor uso dos recursos computacionais de máquinas com vários núcleos, é recomendável usar multiprocessingou concurrent.futures.ProcessPoolExecutor. No entanto, o encadeamento ainda é um modelo apropriado se você deseja executar várias tarefas associadas a E / S simultaneamente.

Isso vincula à entradaglobal interpreter lock do Glossário, na qual explica que o GIL implica que o paralelismo encadeado em Python não é adequado para tarefas vinculadas à CPU :

O mecanismo usado pelo interpretador CPython para garantir que apenas um encadeamento execute o bytecode do Python por vez. Isso simplifica a implementação do CPython, tornando o modelo de objeto (incluindo tipos internos críticos, como dict) implicitamente seguro contra acesso simultâneo. O bloqueio de todo o intérprete facilita que o intérprete seja multiencadeado, às custas de grande parte do paralelismo proporcionado pelas máquinas com vários processadores.

No entanto, alguns módulos de extensão, padrão ou de terceiros, são projetados para liberar o GIL ao executar tarefas intensivas em computação, como compactação ou hash. Além disso, o GIL é sempre liberado ao fazer E / S.

Os esforços anteriores para criar um intérprete de "encadeamento livre" (que bloqueia os dados compartilhados com uma granularidade muito mais fina) não foram bem-sucedidos porque o desempenho sofreu no caso comum de processador único. Acredita-se que superar esse problema de desempenho tornaria a implementação muito mais complicada e, portanto, mais cara de manter.

Esta citação também implica que os dictos e, portanto, a atribuição de variáveis ​​também são seguros para threads como um detalhe de implementação do CPython:

A seguir, os documentos do multiprocessingpacote explicam como ele supera o GIL ao gerar processo, enquanto expõe uma interface semelhante à de threading:

multiprocessamento é um pacote que suporta processos de geração usando uma API semelhante ao módulo de segmentação. O pacote de multiprocessamento oferece simultaneidade local e remota, efetivamente ultrapassando o bloqueio global de intérpretes usando subprocessos em vez de threads. Devido a isso, o módulo de multiprocessamento permite que o programador aproveite totalmente vários processadores em uma determinada máquina. Ele roda em Unix e Windows.

E os documentos paraconcurrent.futures.ProcessPoolExecutor explicar que ele usa multiprocessingcomo back-end:

A classe ProcessPoolExecutor é uma subclasse Executor que usa um conjunto de processos para executar chamadas de forma assíncrona. ProcessPoolExecutor usa o módulo de multiprocessamento, o que lhe permite desviar o bloqueio global de intérpretes, mas também significa que apenas objetos selecionáveis ​​podem ser executados e retornados.

que deve ser contrastado com a outra classe base ThreadPoolExecutorque usa threads em vez de processos

ThreadPoolExecutor é uma subclasse de Executor que usa um pool de threads para executar chamadas de forma assíncrona.

da qual concluímos que ThreadPoolExecutoré adequado apenas para tarefas vinculadas de E / S, enquanto ProcessPoolExecutortambém pode lidar com tarefas vinculadas à CPU.

A pergunta a seguir pergunta por que o GIL existe em primeiro lugar: Por que o bloqueio global de intérpretes?

Experiências de processo versus encadeamento

No Multiprocessing vs Threading Python , fiz uma análise experimental do processo vs threads no Python.

Visualização rápida dos resultados:

insira a descrição da imagem aqui

Ciro Santilli adicionou uma nova foto
fonte
0

Por que Python (CPython e outros) usa o GIL

Em http://wiki.python.org/moin/GlobalInterpreterLock

No CPython, o bloqueio global de intérpretes, ou GIL, é um mutex que impede que vários threads nativos executem bytecodes do Python de uma só vez. Esse bloqueio é necessário principalmente porque o gerenciamento de memória do CPython não é seguro para threads.

Como removê-lo de Python?

Como Lua, talvez o Python possa iniciar várias VMs, mas o python não faz isso, acho que deve haver outros motivos.

Em Numpy ou em alguma outra biblioteca estendida python, às vezes, liberar o GIL para outros threads poderia aumentar a eficiência de todo o programa.

maoyang
fonte
0

Quero compartilhar um exemplo do livro multithreading for Visual Effects. Então, aqui está uma situação clássica de cadeado

static void MyCallback(const Context &context){
Auto<Lock> lock(GetMyMutexFromContext(context));
...
EvalMyPythonString(str); //A function that takes the GIL
...    
}

Agora considere os eventos na sequência que resultam em um bloqueio.

╔═══╦════════════════════════════════════════╦══════════════════════════════════════╗
    Main Thread                             Other Thread                         
╠═══╬════════════════════════════════════════╬══════════════════════════════════════╣
 1  Python Command acquires GIL             Work started                         
 2  Computation requested                   MyCallback runs and acquires MyMutex 
 3                                          MyCallback now waits for GIL         
 4  MyCallback runs and waits for MyMutex   waiting for GIL                      
╚═══╩════════════════════════════════════════╩══════════════════════════════════════╝
user1767754
fonte