Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 539 – Eine neue C-API für Thread-Lokalen Speicher in CPython

Autor:
Erik M. Bray, Masayuki Yamamoto
BDFL-Delegate:
Alyssa Coghlan
Status:
Final
Typ:
Standards Track
Erstellt:
20. Dez. 2016
Python-Version:
3.7
Post-History:
16. Dez. 2016, 31. Aug. 2017, 08. Sep. 2017
Resolution:
Python-Dev Nachricht

Inhaltsverzeichnis

Zusammenfassung

Der Vorschlag besteht darin, eine neue API für Thread-Lokalen Speicher (TLS) zu CPython hinzuzufügen, die die Verwendung der bestehenden TLS-API innerhalb des CPython-Interpreters ersetzen und die bestehende API als veraltet kennzeichnen würde. Die neue API wird „Thread Specific Storage (TSS) API“ genannt (siehe Begründung für die vorgeschlagene Lösung für den Ursprung des Namens).

Da die bestehende TLS-API nur intern verwendet wird (sie wird in der Dokumentation nicht erwähnt und der Header, der sie definiert, pythread.h, wird weder direkt noch indirekt in Python.h eingebunden), betrifft dieser Vorschlag wahrscheinlich nur CPython, könnte aber auch andere Interpreter-Implementierungen (PyPy?) beeinflussen, die Teile der CPython-API implementieren.

Dies wird hauptsächlich durch die Tatsache motiviert, dass die alte API int verwendet, um TLS-Schlüssel auf allen Plattformen darzustellen, was weder POSIX-konform noch in irgendeinem praktischen Sinne portabel ist [1].

Hinweis

Im gesamten Dokument bezieht sich die Abkürzung „TLS“ auf Thread Local Storage und sollte nicht mit „Transportation Layer Security“-Protokollen verwechselt werden.

Spezifikation

Die aktuelle API für TLS, die innerhalb des CPython-Interpreters verwendet wird, besteht aus 6 Funktionen

PyAPI_FUNC(int) PyThread_create_key(void)
PyAPI_FUNC(void) PyThread_delete_key(int key)
PyAPI_FUNC(int) PyThread_set_key_value(int key, void *value)
PyAPI_FUNC(void *) PyThread_get_key_value(int key)
PyAPI_FUNC(void) PyThread_delete_key_value(int key)
PyAPI_FUNC(void) PyThread_ReInitTLS(void)

Diese würden durch einen neuen Satz analoger Funktionen ersetzt werden

PyAPI_FUNC(int) PyThread_tss_create(Py_tss_t *key)
PyAPI_FUNC(void) PyThread_tss_delete(Py_tss_t *key)
PyAPI_FUNC(int) PyThread_tss_set(Py_tss_t *key, void *value)
PyAPI_FUNC(void *) PyThread_tss_get(Py_tss_t *key)

Die Spezifikation fügt auch einige neue Funktionen hinzu

  • Ein neuer Typ Py_tss_t – ein opaker Typ, dessen Definition von der zugrunde liegenden TLS-Implementierung abhängen kann. Er ist definiert als
    typedef struct {
        int _is_initialized;
        NATIVE_TSS_KEY_T _key;
    } Py_tss_t;
    

    wobei NATIVE_TSS_KEY_T ein Makro ist, dessen Wert von der zugrunde liegenden nativen TLS-Implementierung abhängt (z. B. pthread_key_t).

  • Ein Initialisierer für Py_tss_t-Variablen, Py_tss_NEEDS_INIT.
  • Drei neue Funktionen
    PyAPI_FUNC(Py_tss_t *) PyThread_tss_alloc(void)
    PyAPI_FUNC(void) PyThread_tss_free(Py_tss_t *key)
    PyAPI_FUNC(int) PyThread_tss_is_created(Py_tss_t *key)
    

    Die ersten beiden werden für die dynamische (De-)Allokation einer Py_tss_t benötigt, insbesondere in Erweiterungsmodulen, die mit Py_LIMITED_API erstellt wurden, wo die statische Allokation dieses Typs aufgrund seiner opak Implementierung zur Build-Zeit nicht möglich ist. Ein von PyThread_tss_alloc zurückgegebener Wert befindet sich im selben Zustand wie ein mit Py_tss_NEEDS_INIT initialisierter Wert oder NULL im Falle eines Fehlers bei der dynamischen Allokation. Das Verhalten von PyThread_tss_free beinhaltet das präventive Aufrufen von PyThread_tss_delete oder ist eine No-Operation, wenn der Wert, auf den das Argument key zeigt, NULL ist. PyThread_tss_is_created gibt einen Wert ungleich Null zurück, wenn die gegebene Py_tss_t initialisiert wurde (d. h. durch PyThread_tss_create).

Die neue TSS-API bietet keine Funktionen, die PyThread_delete_key_value und PyThread_ReInitTLS entsprechen, da diese Funktionen nur für CPythons inzwischen veraltete eingebaute TLS-Implementierung benötigt wurden; das heißt, das bestehende Verhalten dieser Funktionen wird wie folgt behandelt: PyThread_delete_key_value(key) ist äquivalent zu PyThread_set_key_value(key, NULL) und PyThread_ReInitTLS() ist eine No-Operation [8].

Die neuen PyThread_tss_-Funktionen sind fast exakt analog zu ihren ursprünglichen Gegenstücken mit einigen geringfügigen Unterschieden: Während PyThread_create_key keine Argumente nimmt und einen TLS-Schlüssel als int zurückgibt, nimmt PyThread_tss_create ein Py_tss_t* als Argument und gibt einen int-Statuscode zurück. Das Verhalten von PyThread_tss_create ist undefiniert, wenn der Wert, auf den das Argument key zeigt, nicht mit Py_tss_NEEDS_INIT initialisiert ist. Der zurückgegebene Statuscode ist bei Erfolg Null und bei Fehlschlag ungleich Null. Die Bedeutung von nicht-null Statuscodes wird von dieser Spezifikation nicht weiter definiert.

Ähnlich werden die anderen PyThread_tss_-Funktionen mit einem Py_tss_t* aufgerufen, während zuvor der Schlüssel per Wert übergeben wurde. Diese Änderung ist notwendig, da der Py_tss_t-Typ als opaker Typ hypothetisch fast jede Größe haben könnte. Dies ist insbesondere für Erweiterungsmodule erforderlich, die mit Py_LIMITED_API erstellt wurden, bei denen die Größe des Typs nicht bekannt ist. Außer bei PyThread_tss_free sind die Verhaltensweisen der PyThread_tss_-Funktionen undefiniert, wenn der Wert, auf den das Argument key zeigt, NULL ist.

Darüber hinaus gibt es aufgrund der Verwendung von Py_tss_t anstelle von int Verhaltensunterschiede in der neuen API im Vergleich zur alten API hinsichtlich der Schlüsselerstellung und -löschung. PyThread_tss_create kann mehrmals für denselben Schlüssel aufgerufen werden – der Aufruf für einen bereits initialisierten Schlüssel ist eine No-Operation und gibt sofort Erfolg zurück. Ähnlich für den Aufruf von PyThread_tss_delete mit einem nicht initialisierten Schlüssel.

Das Verhalten von PyThread_tss_delete ist so definiert, dass der Initialisierungsstatus des Schlüssels auf „nicht initialisiert“ geändert wird – dies ermöglicht beispielsweise, dass statisch allokierte Schlüssel beim Neustart des CPython-Interpreters ohne Beendigung des Prozesses (z. B. Einbetten von Python in eine Anwendung) in einen sinnvollen Zustand zurückgesetzt werden können [12].

Die alten PyThread_*_key*-Funktionen werden in der Dokumentation als veraltet gekennzeichnet, generieren aber keine Laufzeit-Warnungen bezüglich veralteter Funktionalität.

Zusätzlich wird auf Plattformen, wo sizeof(pthread_key_t) != sizeof(int) ist, PyThread_create_key sofort mit einem Fehlerstatus zurückkehren, und die anderen TLS-Funktionen werden auf solchen Plattformen alle No-Ops sein.

Vergleich der API-Spezifikation

API Thread Local Storage (TLS) Thread Specific Storage (TSS)
Version Bestehend Neu
Schlüsseltyp int Py_tss_t (opaker Typ)
Native Schlüssel handhaben Cast zu int In internes Feld einbetten
Funktionsargument int Py_tss_t *
Funktionen
  • Schlüssel erstellen
  • Schlüssel löschen
  • Wert setzen
  • Wert abrufen
  • Wert löschen
  • Schlüssel neu initialisieren (nach fork)
  • Schlüssel erstellen
  • Schlüssel löschen
  • Wert setzen
  • Wert abrufen
  • (stattdessen NULL setzen) [8]
  • (unnötig) [8]
  • Schlüssel dynamisch (de-)allozieren
  • Initialisierungsstatus des Schlüssels prüfen
Schlüssel-Initialisierer (-1 als Fehler bei Schlüsselerstellung) Py_tss_NEEDS_INIT
Voraussetzung native Threads (seit CPython 3.7 [9]) native Threads
Einschränkung Keine Unterstützung für Plattformen, auf denen der native TLS-Schlüssel so definiert ist, dass er nicht sicher in int gecastet werden kann. Kann Schlüssel nicht statisch allozieren, wenn Py_LIMITED_API definiert ist.

Beispiel

Mit den vorgeschlagenen Änderungen wird ein TSS-Schlüssel wie folgt initialisiert:

static Py_tss_t tss_key = Py_tss_NEEDS_INIT;
if (PyThread_tss_create(&tss_key)) {
    /* ... handle key creation failure ... */
}

Der Initialisierungsstatus des Schlüssels kann dann wie folgt geprüft werden:

assert(PyThread_tss_is_created(&tss_key));

Die restliche API wird analog zur alten API verwendet

int the_value = 1;
if (PyThread_tss_get(&tss_key) == NULL) {
    PyThread_tss_set(&tss_key, (void *)&the_value);
    assert(PyThread_tss_get(&tss_key) != NULL);
}
/* ... once done with the key ... */
PyThread_tss_delete(&tss_key);
assert(!PyThread_tss_is_created(&tss_key));

Wenn Py_LIMITED_API definiert ist, muss ein TSS-Schlüssel dynamisch allokiert werden

static Py_tss_t *ptr_key = PyThread_tss_alloc();
if (ptr_key == NULL) {
    /* ... handle key allocation failure ... */
}
assert(!PyThread_tss_is_created(ptr_key));
/* ... once done with the key ... */
PyThread_tss_free(ptr_key);
ptr_key = NULL;

Änderungen der Plattformunterstützung

Ein neuer Abschnitt „Native Thread Implementation“ wird zu PEP 11 hinzugefügt, der besagt:

  • Ab CPython 3.7 müssen alle Plattformen eine native Thread-Implementierung (wie pthreads oder Windows) bereitstellen, um die TSS-API zu implementieren. Alle TSS-API-Probleme, die in einer Implementierung ohne native Threads auftreten, werden als „wird nicht behoben“ geschlossen.

Motivation

Das Hauptproblem hierbei ist der Typ der Schlüssel (int), die für TLS-Werte verwendet werden, wie in der ursprünglichen PyThread TLS-API definiert.

Die ursprüngliche TLS-API wurde 1997 von GvR zu Python hinzugefügt, und damals war der Schlüssel, der zur Darstellung eines TLS-Werts verwendet wurde, ein int, und so ist es bis zum Zeitpunkt des Schreibens geblieben. Dies nutzte CPythons eigene TLS-Implementierung, die lange Zeit in Python/thread.c ungenutzt und weitgehend unverändert blieb. Die Unterstützung für die Implementierung der API auf Basis nativer Thread-Implementierungen (pthreads und Windows) wurde viel später hinzugefügt, und die eingebaute Implementierung wurde als nicht mehr notwendig erachtet und inzwischen entfernt [9].

Das Problem bei der Wahl von int zur Darstellung eines TLS-Schlüssels ist, dass es zwar für CPythons eigene TLS-Implementierung in Ordnung war und zufällig mit Windows kompatibel ist (das DWORD für die analoge Daten verwendet), aber nicht mit dem POSIX-Standard für die pthreads-API kompatibel ist, der pthread_key_t als opaken Typ definiert, der vom Standard nicht weiter definiert wird (wie bei Py_tss_t oben beschrieben) [14]. Dies überlässt es der zugrunde liegenden Implementierung, wie ein pthread_key_t-Wert zur Suche nach Thread-spezifischen Daten verwendet wird.

Dies war im Allgemeinen kein Problem für Pythons API, da auf Linux pthread_key_t als unsigned int definiert ist und somit vollständig mit Pythons TLS-API kompatibel ist – pthread_key_t-Werte, die von pthread_create_key erstellt wurden, können frei in int gecastet und zurückkonvertiert werden (nun ja, nicht ganz, selbst das hat einige Einschränkungen, wie in Issue #22206 erwähnt).

Wie jedoch in Issue #25658 darauf hingewiesen wird, gibt es mindestens einige Plattformen (nämlich Cygwin, CloudABI, aber wahrscheinlich auch andere), die ansonsten moderne und POSIX-konforme pthreads-Implementierungen haben, aber nicht mit Pythons API kompatibel sind, da ihre pthread_key_t so definiert ist, dass sie nicht sicher in int gecastet werden kann. Tatsächlich wurde die Möglichkeit, auf dieses Problem zu stoßen, von MvL zum Zeitpunkt der Hinzufügung von pthreads TLS angesprochen [2].

Man könnte argumentieren, dass PEP 11 spezifische Anforderungen für die Unterstützung einer neuen, ansonsten nicht offiziell unterstützten Plattform (wie CloudABI) stellt und dass der Status der Cygwin-Unterstützung derzeit zweifelhaft ist. Dies schafft jedoch eine sehr hohe Hürde für die Unterstützung von Plattformen, die ansonsten Linux- und/oder POSIX-kompatibel sind und auf denen CPython sonst „einfach funktionieren“ könnte, abgesehen von diesem einen Hindernis. CPython selbst auferlegt diese Implementierungshürde durch eine API, die nicht mit POSIX kompatibel ist (und tatsächlich ungültige Annahmen über pthreads trifft).

Begründung für die vorgeschlagene Lösung

Die Verwendung eines opaken Typs (Py_tss_t) für TLS-Schlüsselwerte ermöglicht es der API, mit allen aktuellen (POSIX und Windows) und zukünftigen (C11?) nativen TLS-Implementierungen, die von CPython unterstützt werden, kompatibel zu sein, da die Definition von Py_tss_t von der zugrunde liegenden Implementierung abhängen kann.

Da die bestehende TLS-API auf einigen Plattformen (z. B. Linux) *in der begrenzten API* [13] verfügbar war, unternimmt CPython Anstrengungen, die neue TSS-API auf dieser Ebene ebenfalls bereitzustellen. Beachten Sie jedoch, dass die Py_tss_t-Definition zu einer opaken Struktur wird, wenn Py_LIMITED_API definiert ist, da die Exposition von NATIVE_TSS_KEY_T als Teil der begrenzten API uns daran hindern würde, die native Thread-Implementierung zu wechseln, ohne Erweiterungsmodule neu zu kompilieren.

Eine neue API muss eingeführt werden, anstatt die Funktionssignaturen der aktuellen API zu ändern, um die Abwärtskompatibilität zu gewährleisten. Die neue API gruppiert diese verwandten Funktionen auch deutlicher unter einem einzigen Präfix, PyThread_tss_. Das „tss“ im Namen steht für „thread-specific storage“ und wurde von der Benennung und dem Design der „tss“-API, die Teil der C11 Threads-API ist, beeinflusst [15]. Dies soll jedoch in keiner Weise Kompatibilität mit oder Unterstützung für die C11 Threads-API implizieren oder eine zukünftige Absicht zur Unterstützung von C11 signalisieren – es ist nur der Einfluss für die Benennung und das Design.

Die Aufnahme des speziellen Initialisierers Py_tss_NEEDS_INIT ist darauf zurückzuführen, dass nicht alle nativen TLS-Implementierungen einen Sentinel-Wert für nicht initialisierte TLS-Schlüssel definieren. Auf Windows wird ein TLS-Schlüssel beispielsweise durch ein DWORD (unsigned int) dargestellt und sein Wert muss als opak behandelt werden [3]. Es gibt also keinen vorzeichenlosen Ganzzahlwert, der sicher verwendet werden kann, um einen nicht initialisierten TLS-Schlüssel unter Windows darzustellen. Ebenso spezifiziert POSIX keinen Sentinel für ein nicht initialisiertes pthread_key_t, sondern verlässt sich stattdessen auf die pthread_once-Schnittstelle, um sicherzustellen, dass ein gegebener TLS-Schlüssel pro Prozess nur einmal initialisiert wird. Daher enthält der Typ Py_tss_t explizit ._is_initialized, das den Initialisierungsstatus des Schlüssels unabhängig von der zugrunde liegenden Implementierung anzeigen kann.

Die Änderung von PyThread_create_key, um auf Systemen mit pthreads, bei denen sizeof(int) != sizeof(pthread_key_t) ist, sofort einen Fehlerstatus zurückzugeben, dient als Überprüfung: Derzeit kann PyThread_create_key auf solchen Systemen einen anfänglichen Erfolg melden, aber Versuche, den zurückgegebenen Schlüssel zu verwenden, werden wahrscheinlich fehlschlagen. Obwohl dieser Fehler in der Praxis früher in der Interpreter-Initialisierung auftritt, ist es besser, sofort an der Quelle des Problems (PyThread_create_key) zu fehlschlagen, anstatt später, wenn ein ungültiger Schlüssel verwendet wird. Mit anderen Worten, dies zeigt deutlich an, dass die alte API auf Plattformen, auf denen sie nicht zuverlässig verwendet werden kann, nicht unterstützt wird und keine Anstrengungen unternommen werden, um eine solche Unterstützung hinzuzufügen.

Abgelehnte Ideen

  • Nichts tun: Der Status quo ist in Ordnung, da er unter Linux funktioniert, und Plattformen, die von CPython unterstützt werden möchten, sollten die Anforderungen von PEP 11 erfüllen. Wie oben erläutert, wäre dies ein fairer Argument, wenn CPython aufgefordert würde, Änderungen zur Unterstützung spezifischer Eigenheiten oder Merkmale einer bestimmten Plattform vorzunehmen. In diesem Fall ist es eine Eigenheit von CPython, die es daran hindert, sein volles Potenzial auf ansonsten POSIX-kompatiblen Plattformen auszuschöpfen. Die Tatsache, dass die aktuelle Implementierung unter Linux funktioniert, ist ein glücklicher Zufall, und es gibt keine Garantie, dass dies niemals geändert wird.
  • Betroffene Plattformen sollten Python einfach mit --without-threads konfigurieren: Dies ist keine Option mehr, da die Option --without-threads für Python 3.7 entfernt wurde [16].
  • Betroffene Plattformen sollten CPythons eingebaute TLS-Implementierung anstelle einer nativen TLS-Implementierung verwenden: Dies ist eine akzeptablere Alternative zur vorherigen Idee, und tatsächlich gab es einen Patch, der dies tat [4]. Da die eingebaute Implementierung im Allgemeinen „langsamer und umständlicher“ als native Implementierungen ist, beeinträchtigt sie die Leistung auf betroffenen Plattformen unnötig. Mindestens ein anderes Modul (tracemalloc) ist ebenfalls fehlerhaft, wenn Python ohne native TLS-Implementierung kompiliert wird. Diese Idee kann auch nicht übernommen werden, da die eingebaute Implementierung inzwischen entfernt wurde.
  • Beibehalten der bestehenden API, aber Umgehung des Problems durch Bereitstellung einer Abbildung von pthread_key_t-Werten zu int-Werten. Einige Versuche wurden unternommen ([5], [6]), aber dies führt unnötige Komplexität und Overhead in leistungskritischen Code auf Plattformen ein, die derzeit nicht von diesem Problem betroffen sind (wie Linux). Selbst wenn die Verwendung dieser Umgehung plattformkompatibilitätsabhängig gemacht würde, führt dies plattformspezifischen Code zur Wartung ein und hat immer noch das Problem der zuvor abgelehnten Ideen, die Leistung auf betroffenen Plattformen unnötig zu beeinträchtigen.

Implementierung

Eine erste Version eines Patches [7] ist im Bugtracker für dieses Problem verfügbar. Seit der Migration zu GitHub wurde die Entwicklung im Feature-Branch pep539-tss-api [10] in Masayuki Yamamotos Fork des CPython-Repositorys auf GitHub fortgesetzt. Ein Work-in-Progress PR ist unter [11] verfügbar.

Diese Referenzimplementierung deckt nicht nur die neuen API-Implementierungsfunktionen ab, sondern auch die Aktualisierungen des Client-Codes, die erforderlich sind, um die vorhandene TLS-API durch die neue TSS-API zu ersetzen.

Referenzen und Fußnoten


Quelle: https://github.com/python/peps/blob/main/peps/pep-0539.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT