Frage

Diese Frage und meine Antwort unten ist vor allem als Reaktion auf eine Fläche von Verwirrung in einer anderen Frage.

Am Ende der Antwort, gibt es einige Probleme WRT „flüchtig“ und Thread-Synchronisation, dass ich bin mir nicht ganz sicher über - Ich begrüße Kommentare und alternative Antworten. Der Punkt der Frage bezieht sich in erster Linie auf CPU-Register und wie sie verwendet werden, jedoch.

War es hilfreich?

Lösung

CPU-Register sind kleine Bereiche der Datenspeicherung auf dem Silizium des CPU. Für die meisten Architekturen, sind sie den primären Ort alle Operationen geschehen (Daten wird in aus dem Speicher geladen, operiert und wieder herausgeschoben).

Wie auch immer Thread ausgeführt verwendet die Register und besitzt den Befehlszeiger (die sagt, welcher Befehl als nächstes kommt). Wenn die OS-Swaps in einem anderen Thread, die alle den CPU-Zustand, einschließlich den Register und die Befehlszeiger, irgendwo gespeichert weg bekommen, effektiv Gefriertrocknen des Zustandes des Fadens für, wenn es um das Leben weiter zurück.

Viele weitere Dokumentation über all dies, natürlich, ganz über den Platz. Wikipedia auf Register. Wikipedia über Kontextwechsel. für den Anfang. Edit: oder Steve314 Antwort lesen. :)

Andere Tipps

Die Register sind die „Arbeitsspeicher“ in einer CPU. Sie sind sehr schnell, aber eine sehr begrenzte Ressource. Typischerweise wird eine CPU einen kleinen festen Satz von benannten Registern hat, der Name Teil der Assemblersprache Konvention für diesen CPUs Maschinencode ist. Zum Beispiel, 32-Bit-Intel-x86-CPUs haben vier Hauptdatenregister namens EAX, EBX, ECX und EDX, zusammen mit einer Reihe von Indizierung und anderen mehr spezialisierten Registern.

Genau genommen ist dies nicht ganz in diesen Tagen wahr - Registerumbenennungs- zum Beispiel üblich ist. Einige Prozessoren haben genug registriert, daß sie sie mustern, anstatt zu benennen sie usw. Es bleibt jedoch ein gutes, einfaches Modell zur Arbeit aus. Zum Beispiel wird die Registerumbenennung verwendet, um die Illusion von diesem Grundmodell trotz Out-of-Order-Ausführung zu erhalten.

Die Nutzung von Registern in Hand geschrieben Assembler neigt ein einfaches Muster von Registern Verwendung zu haben. Einige Variablen werden rein in den Registern für die Dauer von einem Unterprogramm, oder einem wesentlichen Teil davon gehalten werden. Andere Register werden in einem Read-Modify-Write-Muster verwendet. Zum Beispiel ...

mov eax, [var1]
add eax, [var2]
mov [var1], eax

IIRC, die gültig ist (wenn auch wahrscheinlich ineffizient) x86-Assembler-Code. Auf einem Motorola 68000, könnte ich schreiben ...

move.l [var1], d0
add.l  [var2], d0
move.l d0, [var1]

Dieses Mal ist die Quelle in der Regel der linke Parameter, mit dem Ziel auf der rechten Seite. Die 68000 hatten 8 Datenregister (D0..D7) und 8 Adressregister (a0..a7) mit a7 IIRC auch als Stapelzeiger dient.

Auf einem 6510 (wieder auf dem guten alten Commodore 64) Ich könnte schreiben ...

lda    var1
adc    var2
sta    var1

Die Register sind hier meist implizit in den Anweisungen - die vor allem den Einsatz der A (Akkumulator) registrieren

.

Bitte verzeiht keine dummen Fehler in diesen Beispielen - ich habe nicht geschrieben eine signifikante Menge an „echten“ (statt virtuellen) Assembler für mindestens 15 Jahre. Das Prinzip ist der Punkt, though.

Verwendung von Registern ist spezifisch für ein bestimmtes Codefragment. Was ein Register hält, ist im Grunde, was auch immer die letzte Anweisung in ihm verlassen hat. Es ist die Verantwortung des Programmierers, um zu verfolgen, was an jedem Punkt im Code in jedes Register ist.

Wenn ein Unterprogramm aufrufen, entweder der Anrufer oder Angerufene muss Verantwortung übernehmen, um sicherzustellen, gibt es keinen Konflikt, die in der Regel bedeutet, dass die Register zu Beginn des Anrufs auf dem Stapel gespeichert und dann wieder in am Ende lesen. Ähnliche Probleme treten mit Unterbrechungen. Solche Dinge, die die Register (Anrufer oder Angerufenen) zum Speichern sind in der Regel ein Teil der Dokumentation jedes Unterprogramm verantwortlich ist.

Ein Compiler wird in der Regel entscheiden, wie Register zu verwenden, in eine viel anspruchsvollen Art und Weise als ein menschlichen Programmierer, aber es funktioniert nach dem gleichen Prinzip. Die Abbildung von Registern auf bestimmte Variablen ist dynamisch und ändert sich dramatisch, nach denen Fragment von Code, den Sie betrachten. Speicher und Register Wiederherstellung wird meist behandelt nach Standard Konventionen, obwohl der Compiler „custom Aufrufkonventionen“ unter bestimmten Umständen improvisiert kann.

Normalerweise lokale Variablen in einer Funktion sind live auf dem Stapel vorgestellt. Dies ist die allgemeine Regel mit „auto“ Variablen in C. Da „auto“ ist die Standardeinstellung, das sind normale lokale Variablen. Zum Beispiel ...

void myfunc ()
{
  int i;  //  normal (auto) local variable
  //...
  nested_call ();
  //...
}

In dem obigen Code, "i" kann auch in erster Linie in einem Register gehalten werden. Es kann sogar von einem Register zu einem anderen und zurück als die Funktion fortschreitet bewegt werden. Wenn jedoch „nested_call“ genannt wird, wird der Wert aus diesem Register an Sicherheit grenzender Wahrscheinlichkeit auf dem Stapel sein - entweder, weil die Variable ist eine Stapelgröße (kein Register), oder weil die Registerinhalte gespeichert seinen eigenen Arbeitsspeicher ermöglichen nested_call .

In einer Multithreading-Anwendung, sind normale lokale Variablen lokal für einen bestimmten Thread. Jeder Thread bekommt einen eigenen Stapel, und während er ausgeführt wird, die exklusive Nutzung der CPU-Register. In einem Kontextwechsel werden diese Register gespeichert. Ob ichn Register oder auf dem Stapel werden die lokalen Variablen nicht zwischen Threads gemeinsam genutzt.

Diese Grundsituation ist in einer Multi-Core-Anwendung erhalten, auch wenn zwei oder mehr Threads zur selben Zeit aktiv sein. Jeder Kern verfügt über einen eigenen Stapel und seine eigenen Register.

Daten im gemeinsam genutzten Speicher erfordern Sorgfalt mehr gespeichert. Dazu gehören globale Variablen, statische Variablen innerhalb der beiden Klassen und Funktionen, und Heap zugewiesenen Objekte. Zum Beispiel ...

void myfunc ()
{
  static int i;  //  static variable
  //...
  nested_call ();
  //...
}

In diesem Fall wurde der Wert von „i“ zwischen Funktionsaufrufen erhalten. Eine statische Region des Hauptspeichers reserviert ist, diesen Wert zu speichern (daher der Name „statisch“). Im Prinzip gibt es keine Notwendigkeit für spezielle Maßnahmen zu konservieren „i“ während des Anrufs auf „nested_call“, und auf den ersten Blick kann die Variable von jedem Thread zugegriffen werden, läuft auf jedem Kern (oder sogar auf einer eigenen CPU).

Allerdings ist der Compiler arbeitet immer noch schwierig, die Geschwindigkeit und die Größe Ihres Code zu optimieren. Wiederholte Lese- und Schreibvorgänge in den Hauptspeicher sind viel langsamer als Registerzugriffe. Der Compiler wählt an Sicherheit grenzender Wahrscheinlichkeit nicht die einfach folgen RMW-Befehl Muster oben beschrieben, sondern stattdessen den Wert in dem Register, für einen relativ langen Zeitraum, liest wiederholt zu vermeiden und Schreiben in den gleichen Speicher.

Dies bedeutet, dass in einem Thread Modifikationen können nicht von einem anderen Thread für einige Zeit gesehen werden. Zwei Threads könnte mit sehr unterschiedlichen Vorstellungen über den Wert von „i“ oben beenden.

Es gibt keine magischen Hardware-Lösung. Beispielsweise gibt es keinen Mechanismus für das Register zwischen Threads zu synchronisieren. Mit der CPU, die Variable und das Register sind völlig getrennte Einheiten - sie nicht wissen, dass sie synchronisiert werden müssen. Es gibt sicherlich keine Synchronisation zwischen den Registern in verschiedenen Threads oder Laufen auf verschiedenen Kernen -. Es gibt keinen Grund zu glauben, dass ein anderer Thread zu einem bestimmten Zeitpunkt das gleiche Register für den gleichen Zweck wird mit

Eine Teillösung Flag ist eine Variable als "flüchtig" ...

void myfunc ()
{
  volatile static int i;
  //...
  nested_call ();
  //...
}

Dies teilt den Compiler nicht zu optimieren, um die Variable liest und schreibt. Der Prozessor verfügt nicht über ein Konzept der Volatilität. Dieses Schlüsselwort weist den Compiler an verschiedene Codes zu generieren, dabei sofort liest und schreibt in dem Speicher, wie durch Zuweisungen angegeben, anstatt diese Zugriffe zu vermeiden, durch ein Register verwendet wird.

Dies ist nicht eine Multithreading-Synchronisationslösung, aber - zumindest für sich genommen nicht. Eine geeignete Multi-Threading-Lösung ist eine Art Sperre zu verwenden, um Zugriff auf diese „gemeinsame Ressource“ zu verwalten. Zum Beispiel ...

void myfunc ()
{
  static int i;
  //...
  acquire_lock_on_i ();
  //  do stuff with i
  release_lock_on_i ();
  //...
}

Es ist mehr los als hier ohne weiteres ersichtlich ist. Grundsätzlich anstatt Schreiben Sie den Wert von „i“ zurück in seine Variable bereit für die „release_lock_on_i“ Anruf, könnte es auf dem Stapel gespeichert werden. Soweit der Compiler betrifft, so ist dies nicht unvernünftig. Es tut Stapel Zugang sowieso (z Speicher der Absenderadresse), so auf dem Stapel das Register Speichern kann effizienter sein als es zurück zu „i“ zu schreiben -. Mehr Cache freundlicher als einen völlig separaten Speicherblock zugreifen

Leider aber die Release-Lock-Funktion nicht weiß, dass der Variable geschrieben nicht zurück in dem Speicher noch, kann so tun nichts, um es zu beheben. Schließlich ist diese Funktion nur ein Bibliotheksaufruf (die reale Entriegelungselement kann in einem tief verschachtelten Aufruf versteckt werden) und dass Bibliothek kompiliert wurde möglicherweise Jahre vor der Anwendung - es nicht weiß, wie seine Anrufer verwenden Register oder den Stapel. Das ist ein großer Teil, warum wir einen Stapel verwenden, und warum Aufrufkonventionen haben standardisiert werden (zum Beispiel, der rettet die Register). Die Freigabe-Lock-Funktion kann nicht Anrufer erzwingen „synchronisieren“ Register.

Ebenso können Sie eine alte app mit einer neuen Bibliothek erneut verknüpfen - der Anrufer nicht weiß, was „release_lock_on_i“ tut oder wie, es ist nur ein Funktionsaufruf. Es tutnicht wissen, dass es zuerst Speicherregister wieder heraus speichern muss.

Um dies zu beheben, können wir die „flüchtig“ bringen.

void myfunc ()
{
  volatile static int i;
  //...
  acquire_lock_on_i ();
  //  do stuff with i
  release_lock_on_i ();
  //...
}

Wir können einen normalen lokalen Variable verwenden, vorübergehend während der Sperre aktiv ist, der Compiler die Möglichkeit zu geben, ein Register für diese kurze Zeit zu verwenden. Grundsätzlich sollte jedoch eine Sperre so schnell wie möglich gelöst werden, so sollte es nicht so viel Code drin sein. Wenn wir das tun, obwohl, schreiben wir unsere temporäre Variable wieder auf „i“ vor dem Schloss, und die Volatilität der „i“ sorgt die Freigabe, dass es in den Hauptspeicher geschrieben zurück.

Im Prinzip ist dies nicht genug. Das Schreiben in den Hauptspeicher nicht, dass Sie in den Hauptspeicher geschrieben haben - es gibt Schichten von Cache zu durchqueren dazwischen, und Ihre Daten könnten in einer dieser Schichten für eine Weile sitzen. Es gibt ein „Speicherbarriere“ Problem hier, und ich weiß nicht viel über diese -. Aber zum Glück dieses Problem ist in der Verantwortung der Thread-Synchronisierung ruft wie die Sperre Belegungs- und Freigabe Anrufe über

Diese Speicherbarriere Problem beseitigt nicht die Notwendigkeit für das „flüchtig“ Schlüsselwort, aber.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top