static und thread_local



  • Hallo,

    ich habe hier eine C-Library, die globale static Variablen benutzt und einige Funktionen daher nicht thread safe sind. Reicht es aus, wenn ich die Variablen thread_local deklariere?


  • Mod

    Vielleicht? Es gibt ja gewiss einen logischen Grund, wieso die Werte global sind. Wenn du jetzt machst, dass jeder Thread eine eigene Kopie hat, dann wird daraus so eine Art semi-lokaler Wert und es ändert sich dadurch das logische Verhalten der Funktion. Das kann je nach Kontext falsch oder richtig sein. Beim berühmte Beispiel strtok wäre das Verhalten beispielsweise korrekt (sofern man den gleichen String immer nur im selben Thread bearbeitet). Bei etwas wie rand wäre es hingegen viel fragwürdiger einen lokalen Zustand zu haben (kann aber auch richtig sein, je nach Bedarf).

    Denk's dir so: Wenn es so einfach wäre, würde es doch jeder machen.



  • Es geht dabei um eine C-Library, die eine Tabelle für einen Verschlüsselungsalgo benutzt, die vor jeder Benutzung initialisiert werden muss. Ich habe das in eine eigene Funktion gekapselt, die den init-Aufruf erledigt. Die einzige Sorge, die ich noch habe, ist, dass zwei verschiedene Threads an der Tabelle rumfummeln, wenn der Aufruf iwann aus zwei Threads kommt.



  • Sollen denn dann alle weiteren Funktionsaufrufe jeweils auf thread-eigenen Daten dieser Variablen arbeiten?
    Evtl. wäre ein Locking-Mechanismus hier sinnvoller?


  • Mod

    @DocShoe sagte in static und thread_local:

    Es geht dabei um eine C-Library, die eine Tabelle für einen Verschlüsselungsalgo benutzt, die vor jeder Benutzung initialisiert werden muss. Ich habe das in eine eigene Funktion gekapselt, die den init-Aufruf erledigt. Die einzige Sorge, die ich noch habe, ist, dass zwei verschiedene Threads an der Tabelle rumfummeln, wenn der Aufruf iwann aus zwei Threads kommt.

    Kommt halt auch wieder drauf an. Da muss man halt den Algorithmus verstanden haben, dann kann man das entscheiden. Eine pauschale Antwort gibt es da nicht. Wie schon gesagt: Wenn es so einfach wäre, dann würde es ja jeder machen.

    Wenn ich jetzt mal rate, welche Eigenschaften ein Verschlüsselungsalgorithmus sinnvoll global speichern könnte, dann kommen mir nur irgendwelche festen Lookup-Tabellen in den Sinn, die einmalig zum Programmstart initialisiert werden müssen und danach nur gelesen werden. In dem Fall wäre das eher eine Koordinationsfrage: Bleibt es nicht-threadlokal, dann muss man alles so koordinieren, dass die Initialisierung vor dem ersten Aufruf aus egal welchem Thread erfolgt, und dass nur ein Thread die Initialisierung durchführt. Macht man es threadlokal, dann muss man es so koordinieren, dass jeder Thread vor dem ersten Aufruf seine lokale Tabelle initialisiert, und das auch nicht vergisst. Ist beides gangbar, wobei mir persönlich Variante 1 einfacher vorkommt.

    Das setzt aber natürlich voraus, dass meine Annahme auch korrekt ist, dass es einen tieferen Sinn hatte, dass dieser Zustand global ist. Wenn das hingegen ein globaler Zustand ist, der eigentlich gar nicht global sein sollte. Dann hilft das natürlich nicht, dann wäre der Algorithmus schlecht programmiert und müsste allgemein geändert werden. Da sind wir wieder beim Beispiel rand, welches vor >50 Jahren so entworfen wurde, ohne an Parallelität zu denken, wohingegen ein zeitgenössischer Zufallsgenerator typischerweise einen lokalen Zustand hat; man also die Threadsicherheit dadurch geschenkt bekommt, dass man das Datenmodell grundsätzlich ändert.



  • Das sind die S-Boxes und Rundenschlüssel für den Blowfish Algo. Ist ne alte lib, wo´s jetzt Probleme in einer Multithreading Umgebung gab. Hab das jetzt alles gekapselt und jeder Aufruf kopiert sich die Initialtabellen in lokale Tabellen und arbeitet mit denen. Waren jetzt auch nur 5 Zeilen Code mehr, aber ich würde echt gern wissen, ob da thread_local nicht gereicht hätte.



  • @DocShoe sagte in static und thread_local:

    Hab das jetzt alles gekapselt und jeder Aufruf kopiert sich die Initialtabellen in lokale Tabellen und arbeitet mit denen. Waren jetzt auch nur 5 Zeilen Code mehr, aber ich würde echt gern wissen, ob da thread_local nicht gereicht hätte.

    Ohne den Code der Bibliothek einigermassen gut vertsanden zu haben, sehe ich da auch keine Möglichkeit zu sagen, ob der Algorithmus mit thread_local wirklich noch korrekt arbeitet... oder auch nur mit den lokalen Tabellen, die du da oben erwähnst. Gibt es da überhaupt einen Unterschied? Für mich hört sich das fast wie ein selbstgestricktes thread_local an (?).

    Ich hatte schon etliche Facepalm-Momente bei externem Code, wo ich dachte das müsse doch auch so und so gehen. Da werden manchmal die absonderlichsten Dinge gemacht, die dafür sorgen, dass der Code ausschließlich so und nicht anders funktioniert 😞

    Nur so ne spontane Idee - vielleicht ist das ja was:

    Falls die Tabellen, wie von @SeppJ vermutet, nach der Initialisierung ausschließlich gelesen werden, gäbe es noch die Möglichkeit diese nur einmal für alle Threads in einem Speicherbereich erzeugen zu lassen, dessen Speicherseiten dann mittels OS-Funktionen als Readonly markiert werden. Unter Windows z.B. mit VirtualProtect und PAGE_READONLY oder unter Linux mit mprotect und PROT_READ.

    Möglich, dass das zu aufwändig in der Umsetzung ist, aber das hätte den Vorteil, dass das Programm sofort vor die Wand liefe, wenn es etwas komisches mit diesen globalen Tabellen macht.

    Sind alle globalen Daten readonly und läuft das Ganze, ohne dass die Page Protection meckert, würde ich mich so weit aus dem Fenster lehnen zu sagen, dass das Programm threadsicher und korrekt arbeitet (so korrekt wie die Lib ohnehin schon single-treaded ist). Natürlich nur, wenn alle Daten, auf denen gearbeitet wird (übergebene Kontext-Objektpointer, Keys, Klartext, Chiffre und was es da so geben mag) entweder ebenfalls funktions-/thread-lokal oder synchronisiert sind (Mutex und sowas). Zustimmung?



  • Das hier ist fast identisch mit dem, was ich jetzt habe. Den Code, den ich habe ist eine C Implementation und sieht etwas anders aus, ist aber im Prinzip gleich. Meine Frage bezieht sich auf die initial_pary und initial_sbox. Im verlinkten C++ Code sind die static const und werden kopiert, in meiner C Implementation waren sie non-static global und wurden vor der Ver- und Entschlüsselung initialisiert und währenddessen verändert.



  • @DocShoe sagte in static und thread_local:

    Meine Frage bezieht sich auf die initial_pary und initial_sbox. Im verlinkten C++ Code sind die static const und werden kopiert, in meiner C Implementation waren sie non-static global und wurden vor der Ver- und Entschlüsselung initialisiert und währenddessen verändert.

    Wurden die Tabellen nur einmalig initialisiert oder vor jedem Ver-/Entschlüsselungs-Run? Weisst du, wozu die erwähnten Veränderungen gut sind? Werden die Tabellen vielleicht einfach nur als "Abreitsbereich" für einen Durchlauf den Algorithmus benötigt und vorher immer in einen wohldefinierten Anfangszustand gebracht, so wie sie da hartcodiert in dem von dir verlinketen Code stehen?

    Wenn letzteres der Fall ist, würde ich aus dem Bauch heraus auch dazu tendieren, dass thread_local wohl ausreicht. Festlegen will ich mich da aber lieber nicht, da ich mir die Mühe sparen will, das Ding vollständig zu verstehen 😉

    Wenn dieser "Arbeitsbereich" aber ohnehin jedes mal neu initialisiert werden muss, dann halte ich das memcpy in eine lokale Tabelle allerdings für eine gar nicht mal so schlechte Lösung, die möglicherweise sogar noch flotter ist als das, was da vorher gemacht wurde (Tabelle "berechnet"?)

    Ich denke dir ist auch bewusst, dass bezüglich Thread-Safety für die von dir verlinkte Klasse jeder Thread seine eigene Blowfish-Instanz haben sollte oder aber alle Member-Funktionen-Aufrufe, die irgendwie mit pary_ oder sbox_ herumhantieren z.B. mit einem Mutex synchronisiert werden müssen - genau wie die std::vector<char>& für Klartext, Chiffre und Key (?).

    Und auch wenn das nicht dein Code ist, möchte ich noch erwähnen, dass so eine Initialisierung lokaler statischer Variablen wie in Zeilen 257 und 258 vom Compiler garantiert threadsicher vonstatten geht. Das ist zwar eigentlich zu begrüssen, aber bedeutet auch, dass einem der Compiler da im ungünstigsten Fall einen Mutex dazwischenschiebt, der bei jedem Aufruf der Funktion gelockt werden muss, auch wenn die Variable nur ein einziges mal beim ersten Funktionsaufruf geschrieben wird. Da es sich mit dem sizeof um compile-time-konstante Werte handelt, wäre ein Mutex hier wirklich unnötiger Overhead. static constexpr oder auch ein static const Member würden das vermeiden.


Anmelden zum Antworten