Cursus
Als je net begint met Python en meer wilt leren, volg dan DataCamps cursus Introduction to Data Science in Python.
Een Global Interpreter Lock (GIL) is een mechanisme dat een globale lock toepast op een interpreter. Het wordt gebruikt in taalinterpreterers om de uitvoering van threads te synchroniseren en te beheren, zodat er telkens maar één native thread (gepland door het besturingssysteem) kan uitvoeren.
In een situatie met meerdere threads kan het gebeuren dat beide threads tegelijk proberen geheugen te gebruiken, waardoor ze de gegevens in het geheugen zouden overschrijven. Daarom is er behoefte aan een mechanisme dat dit kan voorkomen.
Enkele populaire interpreters met een GIL zijn CPython en Ruby MRI. Zoals de meesten weten is Python een geïnterpreteerde taal en heeft het verschillende distributies zoals CPython, Jython, IronPython. Van deze ondersteunt alleen CPython de GIL, en het is ook de meest gebruikte implementatie van Python. CPython is ontwikkeld in zowel C als Python, voornamelijk om applicaties te ondersteunen die veel C onder de motorkap hebben.
Zelfs als je processor meerdere cores heeft, staat een global interpreter maar één thread tegelijk toe. Dit komt doordat een thread, zodra die start met draaien, de Global Interpreter Lock verwerft. Wanneer hij wacht op een I/O-bewerking (data lezen/schrijven van/naar schijf) of een CPU-gebonden bewerking (vector-/matrixvermenigvuldiging), geeft hij de lock vrij zodat andere threads van dat proces kunnen draaien. Zo voorkomt het dat je andere threads tegelijkertijd laat lopen.

Laten we even stilstaan bij het bovenstaande diagram. Je ziet een functie factorial en twee threads 1 en 2; thread 1 is vergrendeld terwijl thread 2 in een wachtstatus staat. Dit betekent dat slechts één van de threads toegang heeft tot de functie. Stel nu dat de functie factorial 2 seconden nodig heeft om te voltooien. In een ideale situatie zouden beide threads de uitvoering in 2 seconden moeten kunnen afronden. In Python is dat echter niet het geval: beide threads draaien serieel en niet parallel aan elkaar.
De threads 1 en 2 die de functie factorial aanroepen, kunnen twee keer zoveel tijd kosten als één enkele thread die de functie twee keer aanroept. Dit laat ook zien dat de geheugenmanager, aangestuurd door de interpreter, niet thread-safe is, wat betekent dat meerdere threads niet gelijktijdig toegang kunnen krijgen tot dezelfde gedeelde data.
Daarom doet de GIL (Bron: Understanding the Python GIL):
-
Beperkt de threading-werking.
-
Parallelle uitvoering is beperkt.
-
De GIL zorgt ervoor dat er maar één thread tegelijk in de interpreter draait.
-
Het helpt om allerlei low-level details, zoals geheugenbeheer, te vereenvoudigen.
Met GIL wordt coöperatief rekenen of gecoördineerde multitasking bereikt in plaats van parallel rekenen.
Zoals in het bovenstaande diagram te zien is, zijn er drie threads; aanvankelijk draait thread 1 en heeft die de GIL verkregen, en wanneer een I/O-bewerking zoals lezen, schrijven, enz. plaatsvindt, geeft thread 1 de GIL vrij en wordt die overgenomen door thread 2. Deze cyclus blijft doorgaan en de GIL wisselt steeds van thread totdat de threads de uitvoering van het programma hebben voltooid. Onthoud dat threads die de lock niet hebben en hun uitvoering nog niet hebben afgerond, in een wachtstatus blijven.
GIL in Python2 vs. Python3

De Global Interpreter Lock in Python 2.7 werkt anders dan in Python 3. Bij een puur CPU-gebonden bewerking blijft de thread doorgaan, omdat er geen I/O-bewerking is; in dat geval zijn de andere threads inactief of in een wachtstatus, wat niet wenselijk is. Om dit op te lossen, heeft Python 2 het begrip ticks. De Global Interpreter Lock voert een controle uit om de status van de thread te monitoren, zoals wachtstatus, I/O-bewerking of of hij draait. De GIL controleert de threads echter niet continu, maar gebruikt het concept van ticks en controleert de threads elke 100 ticks.
Een tick is een bytecode-instructie, en wanneer 100 bytecode-instructies zijn voltooid, controleert de GIL of de threads draaien of in een wachtstatus staan. Het is belangrijk te onthouden dat een tick niet met tijd samenhangt, aangezien het een bytecode-instructie is. Elke tick kan langer of korter duren dan een andere.
Deze periodieke controle van de threads na elke 100 ticks is essentieel, vooral bij CPU-gebonden bewerkingen, omdat die geen I/O-bewerkingen hebben. Let op: je kunt de tick-teller aanpassen met de ingebouwde Python-module sys.
Laten we enkele nadelen van bovenstaande aanpak bespreken:
Als je naar dit diagram kijkt, zie je dat de threads niet parallel maar in serie draaien, ook wel coöperatief rekenen genoemd, aangestuurd door de Global Interpreter Lock. Ook moeten de threads langer wachten. Zo moest thread T3 wachten tot zowel threads T1 als T2 de GIL vrijgaven. Daardoor lijden de threads honger naar de lock en rekentijd.
Een ander nadeel is dat er een strijd is tussen threads over welke de GIL zal verkrijgen. Aanvankelijk kan het zijn dat op basis van prioriteit T1 de GIL krijgt, dan T2, enzovoort. Maar wanneer T3 de GIL vrijgeeft, stuurt die een melding naar alle andere threads, waarna alle andere threads blijven strijden om de GIL te bemachtigen. Dat gezegd hebbende, kun je dit mogelijk ondervangen door de prioriteiten van elke thread in te stellen.
In Python 3 krijg je een vaste tijd toegewezen per thread, d.w.z. 5 ms uitvoeringstijd. Dit voorkomt dat threads honger lijden naar resources, omdat de wachttijd voor alle threads gelijk is. De threads wachten om te zien of de GIL bewust wordt vrijgegeven door Thread 1 vanwege een I/O- of sleep-bewerking. Zo niet, dan wordt het na 5 ms afgedwongen om vrij te geven. Daardoor is er geen honger naar CPU-tijd, omdat elke thread een gelijke hoeveelheid tijd krijgt, en dit elimineert de uitdaging om de GIL te bemachtigen.
Het is echter belangrijk om te noteren dat de nieuwe implementatie van de GIL meer prioriteit geeft aan CPU-gebonden taken dan aan I/O-bewerkingen. In een situatie waarin een thread de GIL heeft vrijgegeven en twee threads wachten, wordt de GIL toegewezen aan de thread die een CPU-gebonden taak moet uitvoeren, in plaats van aan de andere thread.
Conclusie
Gefeliciteerd met het afronden van de tutorial.
Deze tutorial behandelde een gevorderd onderwerp en had als doel je een theoretisch overzicht te geven van hoe de GIL precies werkt in Python. Omdat deze tutorial geen code-aspecten van GIL en threads behandelde, is een goede oefening om zelf te experimenteren door meerdere threads te maken en een methode die een I/O-intensieve taak uitvoert, en vervolgens te analyseren hoe de GIL tussen elke thread wordt gewisseld.
Stel gerust vragen over deze tutorial in de reacties hieronder.
Referenties:
Als je net begint met Python en meer wilt leren, volg dan DataCamps cursus Introduction to Data Science in Python.
