Stöd för fri trådning i C API-tillägg

Från och med version 3.13 har CPython stöd för att köra med global interpreter lock (GIL) inaktiverat i en konfiguration som kallas free threading. Detta dokument beskriver hur man anpassar C API-tillägg för att stödja fri trådning.

Identifiera den fritt trådade byggnaden i C

CPython C API exponerar makrot Py_GIL_DISABLED: i free-threaded build är det definierat till 1, och i regular build är det inte definierat. Du kan använda det för att aktivera kod som bara körs under free-threaded build:

#ifdef Py_GIL_DISABLED
/* kod som bara körs i den fritt trådade versionen */
#endif

Anteckning

I Windows definieras inte detta makro automatiskt, utan måste anges till kompilatorn när den bygger programmet. Funktionen sysconfig.get_config_var() kan användas för att avgöra om den tolk som körs för tillfället har definierat makrot.

Initialisering av modul

Tilläggsmoduler måste uttryckligen ange att de stöder körning med GIL inaktiverad; annars kommer import av tillägget att ge upphov till en varning och aktivera GIL vid körning.

Det finns två sätt att ange att en tilläggsmodul stöder körning med GIL inaktiverad beroende på om tillägget använder flerfas- eller enfasinitialisering.

Initialisering av flera faser

Tillägg som använder flerfasinitialisering (dvs. PyModuleDef_Init()) bör lägga till en Py_mod_gil slot i moduldefinitionen. Om ditt tillägg stöder äldre versioner av CPython bör du bevaka platsen med en PY_VERSION_HEX-kontroll.

static struct PyModuleDef_Slot module_slots[] = {
    ...
#if PY_VERSION_HEX >= 0x030D0000
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
    {0, NULL}
};

static struct PyModuleDef moduledef = {
    PyModuleDef_HEAD_INIT,
    .m_slots = modul_slots,
    ...
};

Initialisering med en fas

Tillägg som använder enfasinitialisering (dvs. PyModule_Create()) bör anropa PyUnstable_Module_SetGIL() för att ange att de stöder körning med GIL inaktiverad. Funktionen är endast definierad i den fritt trådade versionen, så du bör skydda anropet med #ifdef Py_GIL_DISABLED för att undvika kompileringsfel i den vanliga versionen.

static struct PyModuleDef moduledef = {
    PyModuleDef_HEAD_INIT,
    ...
};

PyMODINIT_FUNC
PyInit_mymodule(void)
{
    PyObject *m = PyModule_Create(&moduledef);
    if (m == NULL) {
        returnera NULL;
    }
#ifdef Py_GIL_DISABLED
    PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
    returnera m;
}

Allmänna API-riktlinjer

Det mesta av C API:et är trådsäkert, men det finns några undantag.

  • Strukturfält: Direkt åtkomst till fält i Python C API-objekt eller strukturer är inte trådsäkert om fältet kan ändras samtidigt.

  • Makroer: Accessormakron som PyList_GET_ITEM, PyList_SET_ITEM och makron som PySequence_Fast_GET_SIZE som använder det objekt som returneras av PySequence_Fast() utför ingen felkontroll eller låsning. Dessa makron är inte trådsäkra om containerobjektet kan ändras samtidigt.

  • Lånade referenser: C API-funktioner som returnerar lånade referenser kanske inte är trådsäkra om det innehållande objektet ändras samtidigt. Se avsnittet om lånade referenser för mer information.

Säkerhet för containergänga

Behållare som PyListObject, PyDictObject och PySetObject utför intern låsning i den frittrådade versionen. Till exempel kommer PyList_Append() att låsa listan innan ett objekt läggs till.

PyDict_Next

Ett anmärkningsvärt undantag är PyDict_Next(), som inte låser ordlistan. Du bör använda Py_BEGIN_CRITICAL_SECTION för att skydda ordlistan medan du itererar över den om ordlistan kan ändras samtidigt:

Py_BEGIN_CRITICAL_SECTION(dict);
PyObject *nyckel, *värde;
Py_ssize_t pos = 0;
while (PyDict_Next(dict, &pos, &key, &value)) {
    ...
}
Py_END_CRITICAL_SECTION();

Lånade referenser

Vissa C API-funktioner returnerar lånade referenser. Dessa API:er är inte trådsäkra om det objekt som innehåller dem ändras samtidigt. Det är t.ex. inte säkert att använda PyList_GetItem() om listan kan ändras samtidigt.

I följande tabell listas några API:er för lånade referenser och deras ersättare som returnerar strong references.

Lånad referens API

API för stark referens

PyList_GetItem()

PyList_GetItemRef()

PyList_GET_ITEM()

PyList_GetItemRef()

PyDict_GetItem()

PyDict_GetItemRef()

PyDict_GetItemWithError()

PyDict_GetItemRef()

PyDict_GetItemString()

PyDict_GetItemStringRef()

PyDict_SetDefault()

PyDict_SetDefaultRef()

PyDict_Next()

ingen (se PyDict_Next)

PyWeakref_GetObject()

PyWeakref_GetRef()

PyWeakref_GET_OBJECT()

PyWeakref_GetRef()

PyImport_AddModule()

PyImport_AddModuleRef()

PyCell_GET()

PyCell_Get()

Det är inte alla API:er som returnerar lånade referenser som är problematiska. Till exempel är PyTuple_GetItem() säkert eftersom tuples är oföränderliga. På samma sätt är inte alla användningar av ovanstående API:er problematiska. Till exempel används PyDict_GetItem() ofta för att analysera ordlistor med nyckelordsargument i funktionsanrop; dessa ordlistor med nyckelordsargument är i praktiken privata (inte tillgängliga för andra trådar), så det är säkert att använda lånade referenser i det sammanhanget.

Några av dessa funktioner lades till i Python 3.13. Du kan använda paketet pythoncapi-compat för att tillhandahålla implementeringar av dessa funktioner för äldre Python-versioner.

API:er för minnesallokering

Pythons minneshantering C API tillhandahåller funktioner i tre olika allokeringsdomäner: ”raw”, ”mem” och ”object”. För trådsäkerhet kräver den frittrådade versionen att endast Python-objekt allokeras med hjälp av objektdomänen och att alla Python-objekt allokeras med hjälp av den domänen. Detta skiljer sig från de tidigare Python-versionerna, där detta endast var en bästa praxis och inte ett hårt krav.

Anteckning

Sök efter användningar av PyObject_Malloc() i ditt tillägg och kontrollera att det allokerade minnet används för Python-objekt. Använd PyMem_Malloc() för att allokera buffertar istället för PyObject_Malloc().

Trådstatus och GIL API:er

Python tillhandahåller en uppsättning funktioner och makron för att hantera trådstatus och GIL, t.ex:

Dessa funktioner bör fortfarande användas i den fritt trådade versionen för att hantera trådstatus även när GIL är inaktiverad. Om du t.ex. skapar en tråd utanför Python måste du anropa PyGILState_Ensure() innan du anropar Python API för att säkerställa att tråden har ett giltigt Python-trådtillstånd.

Du bör fortsätta att anropa PyEval_SaveThread() eller Py_BEGIN_ALLOW_THREADS runt blockerande operationer, t.ex. I/O eller låsförvärv, för att tillåta andra trådar att köra cyclic garbage collector.

Skydd av intern utökningstillstånd

Ditt tillägg kan ha interna tillstånd som tidigare skyddades av GIL. Du kan behöva lägga till låsning för att skydda detta tillstånd. Tillvägagångssättet beror på ditt tillägg, men några vanliga mönster inkluderar:

  • Cacher: globala cacher är en vanlig källa till delat tillstånd. Överväg att använda ett lås för att skydda cacheminnet eller inaktivera det i den frittrådade versionen om cacheminnet inte är avgörande för prestandan.

  • Global State: globalt tillstånd kan behöva skyddas av ett lås eller flyttas till trådlokal lagring. C11 och C++11 tillhandahåller thread_local eller _Thread_local för trådlokal lagring.

Kritiska avsnitt

I den fritt trådade versionen tillhandahåller CPython en mekanism som kallas ”kritiska sektioner” för att skydda data som annars skulle skyddas av GIL. Även om författare av tillägg kanske inte interagerar direkt med den interna implementeringen av kritiska avsnitt är det viktigt att förstå deras beteende när man använder vissa C API-funktioner eller hanterar delat tillstånd i den fritt trådade versionen.

Vad är kritiska avsnitt?

I princip fungerar kritiska sektioner som ett lager för att undvika dödlägen ovanpå enkla mutexar. Varje tråd upprätthåller en stack med aktiva kritiska sektioner. När en tråd behöver förvärva ett lås som är associerat med en kritisk sektion (t.ex. implicit när den anropar en trådsäker C API-funktion som PyDict_SetItem(), eller explicit med hjälp av makron), försöker den förvärva den underliggande mutexen.

Använda kritiska avsnitt

De primära API:erna för att använda kritiska sektioner är:

Dessa makron måste användas i matchande par och måste förekomma i samma C-scope, eftersom de skapar ett nytt lokalt scope. Dessa makron är no-ops i icke-frittrådade builds, så de kan utan problem läggas till i kod som behöver stödja båda build-typerna.

En vanlig användning av en kritisk sektion är att låsa ett objekt när man använder ett internt attribut i det. Om en tilläggstyp till exempel har ett internt räknefält kan du använda en kritisk sektion när du läser eller skriver fältet:

// läser räkningen, returnerar ny referens till internt räkningsvärde
PyObject *resultat;
Py_BEGIN_CRITICAL_SECTION(obj);
resultat = Py_NewRef(obj->räkning);
Py_END_CRITICAL_SECTION();
returnera resultat;

// skriv räkningen, förbrukar referens från new_count
Py_BEGIN_CRITICAL_SECTION(obj);
obj->räkning = ny_räkning;
Py_END_CRITICAL_SECTION();

Hur kritiska avsnitt fungerar

Till skillnad från traditionella lås garanterar kritiska sektioner inte exklusiv åtkomst under hela sin varaktighet. Om en tråd skulle blockeras medan den håller en kritisk sektion (t.ex. genom att förvärva ett annat lås eller utföra I/O), avbryts den kritiska sektionen tillfälligt - alla lås frigörs - och återupptas sedan när blockeringsoperationen är klar.

Det här beteendet liknar det som händer med GIL när en tråd gör ett blockerande anrop. De viktigaste skillnaderna är:

  • Kritiska sektioner fungerar per objekt i stället för globalt

  • Kritiska avsnitt följer en stapeldisciplin inom varje tråd (makron ”begin” och ”end” upprätthåller detta eftersom de måste vara parade och inom samma omfattning)

  • Kritiska sektioner frigör och återtar automatiskt lås runt potentiella blockeringsoperationer

Undvikande av dödläge

Kritiska avsnitt hjälper till att undvika dödlägen på två sätt:

  1. Om en tråd försöker förvärva ett lås som redan innehas av en annan tråd, avbryter den först alla sina aktiva kritiska sektioner och frigör tillfälligt deras lås

  2. När blockeringsoperationen är klar återfås endast den översta kritiska sektionen först

Detta innebär att du inte kan förlita dig på nästlade kritiska sektioner för att låsa flera objekt samtidigt, eftersom den inre kritiska sektionen kan avbryta de yttre. Använd istället Py_BEGIN_CRITICAL_SECTION2 för att låsa två objekt samtidigt.

Observera att de lås som beskrivs ovan endast är PyMutex-baserade lås. Implementeringen av kritiska sektioner känner inte till eller påverkar andra låsmekanismer som kan användas, som POSIX-mutexar. Observera också att medan blockering på någon PyMutex gör att de kritiska sektionerna avbryts, frigörs endast de mutex som är en del av de kritiska sektionerna. Om PyMutex används utan en kritisk sektion kommer den inte att frigöras och får därför inte samma undvikande av dödlägen.

Viktiga överväganden

  • Kritiska sektioner kan tillfälligt släppa sina lås, vilket gör det möjligt för andra trådar att modifiera de skyddade data. Var försiktig med att göra antaganden om datatillståndet efter operationer som kan blockera.

  • Eftersom lås kan frigöras tillfälligt (suspenderas) garanterar inte en kritisk sektion exklusiv åtkomst till den skyddade resursen under hela sektionens varaktighet. Om kod inom en kritisk sektion anropar en annan funktion som blockerar (t.ex. förvärvar ett annat lås, utför blockerande I/O), kommer alla lås som tråden har via kritiska sektioner att frigöras. Detta liknar hur GIL kan frigöras under blockerande anrop.

  • Endast det eller de lås som är associerade med den senast inmatade (översta) kritiska sektionen garanteras att hållas vid varje given tidpunkt. Lås för yttre, nästlade kritiska sektioner kan ha upphävts.

  • Du kan låsa högst två objekt samtidigt med dessa API:er. Om du behöver låsa fler objekt måste du omstrukturera din kod.

  • Även om kritiska sektioner inte låser sig om du försöker låsa samma objekt två gånger, är de mindre effektiva än specialbyggda reentranta lås för detta användningsfall.

  • När du använder Py_BEGIN_CRITICAL_SECTION2 påverkar inte ordningen på objekten korrektheten (implementationen hanterar undvikande av deadlock), men det är god praxis att alltid låsa objekt i en konsekvent ordning.

  • Kom ihåg att makron för kritiska avsnitt främst är till för att skydda åtkomst till Python-objekt som kan vara inblandade i interna CPython-operationer som är känsliga för de deadlock-scenarier som beskrivs ovan. För att skydda rent interna tilläggstillstånd kan standardmutexar eller andra synkroniseringsprimitiver vara mer lämpliga.

Bygga tillägg för Free-Threaded-versionen

C API-tillägg måste byggas specifikt för den fritt trådade versionen. Hjulen, de delade biblioteken och binärerna anges med suffixet ”t”.

Begränsat C API och stabilt ABI

Den fritt trådade byggnaden stöder för närvarande inte Limited C API eller det stabila ABI. Om du använder setuptools för att bygga ditt tillägg och för närvarande ställer in py_limited_api=True kan du använda py_limited_api=not sysconfig.get_config_var("Py_GIL_DISABLED") för att välja bort det begränsade API:et när du bygger med den fritt trådade versionen.

Anteckning

Du kommer att behöva bygga separata hjul specifikt för den fritt trådade versionen. Om du för närvarande använder den stabila ABI kan du fortsätta att bygga ett enda hjul för flera icke-frittrådade Python-versioner.

Fönster

På grund av en begränsning i den officiella Windows-installationsprogrammet måste du manuellt definiera Py_GIL_DISABLED=1 när du bygger tillägg från källan.

Se även

Portning av tilläggsmoduler för att stödja fri trådning: En gemenskapsunderhållen portningsguide för tilläggsförfattare.