Proseminar Nichtsequentielle Programmiersprachen WS 2011/2012 Software Transactional Memory Gregor Busse 04.01.2012 1
Inhaltsverzeichnis Software Transactional Memory: Einleitung..Seite 3 Software Transactional Memory nach Nir Shavit und Dan Touitou Seite 3 Algorithmus. Seite 4 Performance von STM. Seite 5 DSTM2 und Anwendungsbeispiel. Seite 6 Quellen. Seite 7 Anhang. Seite 7 2
Einleitung: Als Mittel zur Synchronisierung von Zugriffen auf geteilte Ressourcen haben wir bereits Semaphoren und Monitore kennengelernt. Beide Verfahren haben dabei eine steigende Komplexität mit der Anzahl an zu synchronisierende Prozessen und Ressourcen gemein. Ein weiteres Konzept zur Synchronisierung stellt Transactional Memory dar. Transaktionen kennen wir bereits von Datenbanken. Zugriffe auf Datenbanken (Lesen und Schreiben) werden entweder erfolgreich durchgeführt ( Commit ) oder abgebrochen ( Abort ) und müssen gegebenenfalls wiederholt werden. Ziel einer Datenbank ist dabei, die Wahrung der Datenkonsistenz bei gleichzeitig performanter Verarbeitung von möglichst vielen Zugriffen auf große Datenbestände. Die Entwicklung erfordert mit steigenden Prozessorzahlen gut skalierende Konzepte zur Verwaltung der geteilten Ressourcen bzw. des geteilten Speichers. Eine Möglichkeit ist dabei Transactional Memory (TM), die Anwendung des Konzepts der Datenbanktransaktion auf den Speicher eines Computers. TM lässt sich dabei sowohl in Hardware als auch in Software implementieren. Während Hardware Transactional Memory (HTM) potentiell die bessere Performance bietet (wie üblich bei festverdrahteten Funktionen) lässt dessen Verfügbarkeit noch zu wünschen übrig. Existierende Implementierungen sind experimenteller Natur. Erst kommende Prozessorgenerationen wie IBM BlueGene/Q oder Intel Haswell Prozessoren sollen mit HTM ausgestattet sein. Im Folgenden wollen wir daher Software Transactional Memory (STM) betrachten und eine mögliche Implementierung in Java. Software Transactional Memory nach Nir Shavit und Dan Touitou STM [1] als Konzept zur Synchronisierung von geteiltem Speicher, auf höherer Abstraktionsebene als Semaphoren und Monitoren, besitzt keine klassische kritische Sektion. Es ist ein optimistisches nonblocking/lock-free Verfahren unter Verwendung des Load_Linked/Store_Conditional Befehls. Ein Speicherbereich wird als Transactional Memory definiert. Prozesse und deren Transaktionen müssen von den Teilen des TM, welche sie benötigen, den Besitz erwerben. Dabei sind die einzelnen Speicherstellen des TM aufsteigend nummeriert/sortiert. Die Inbesitznahme durch Transaktionen erfolgt sequentiell von deren niedrigstem bis zum höchsten benötigtem Speicherbereich. Eine abgeschlossene Transaktion gibt anschließend ihren Speicher wieder frei. Daraus ergibt sich die non-blocking Eigenschaft dieser STM-Implementierung. Eine Transaktion T1 welche den Besitz einer Speicherstelle nicht erwerben kann, weil eine Transaktion T2 ihr zuvor kam kann T2 im weiteren Verlauf nicht blockieren. T2 besitzt alle benötigten niedrigeren Speicherstellen schon und T1 kann keine höhere Speicherstelle besitzen als die an der sie gescheitert ist. Nur eine Transaktion T3, welche höhere Speicherstellen besitzt die T2 benötigt, kann T2 zum Abbruch bringen. Letztendlich wird jene Transaktion, welche zuerst die höchsten von mehreren Transaktionen gleichzeitig benötigten Speicherstellen besitzt erfolgreich abschließen. Alle anderen hat sie schon in Besitz genommen und keine weitere Transaktion benötigt eine höhere Speicherstelle. 3
Eine Prozess, welcher seine Transaktion T1 wegen Absturz, Kontextwechsel etc. nicht abschließen kann, aber bereits Speicherbereich in Besitz genommen hat, würde andere Prozesse und deren Transaktionen auf den selben Speicherbereich blockieren. Um dies zu vermeiden, versuchen die blockierten Prozesse selbst T1 abzuschließen. Sie ermitteln die blockierende Transaktion und starten sie einmalig neu. Solange noch wenigstens ein lauffähiger Prozess im System versucht, wiederholt seine Transaktion durchzuführen, wird er in endlich vielen Schritten Erfolg haben. Mit jedem Versuch seiner Transaktion wird er eine blockierte Transaktion (von einem blockiertem Prozess) abschließen und somit deren Speicherbereich freigeben. Algorithmus (gekürzt): TM besteht aus einem Feld Memory[M], welcher die Daten enthält und einem Feld Ownerships[M], welches jeder Stelle in Memory einen Besitzer zuordnet. Prozesse die Transaktionen auf Memory ausführen wollen besitzen: - ein Variable Size, welche die Anzahl an benötigten Speicherstellen angibt. - ein Feld Add[], welches die Adressen der Speicherstellen enthält. -Eine Variable Input, als Eingabewerte der Transaktion. -ein Feld Oldvalues[], initialisiert mit Null und gefüllt mit gelesenen Werten bei erfolgreich ablaufender Transaktion. - Eine Variable Status welche: -mit NULL für initialisiert ist für Transaktionen welche noch nicht alle benötigten Speicherstellen besitzen. -Failure enthält, falls eine Speicherstelle nicht in Besitzt genommen werden konnte. -Success enthält, falls alle Speicherstellen in Besitz genommen werden konnten. StartTransaction(input,DataSet) Initialize(Tran,input,DataSet) Tran->Stable = TrueA Transaction(Tran,Tran->Version,True) Tran->Stable = False Tran->Version++ If Tran->Status = Success then return(sucess,calcoutput(tran->oldvalues,input)) return Failure Transaction(tran,version,IsInitiator) AcquireOwnerships(tran,version) (status,failadd) = LL(tran->status) If status = Null then If(version!= tran->version then return SC(tran->status,(Success,0) (status,failadd) = LL(tran->status) If status = Success then AgreeOldValues(tran,version) NewValues = CalcNewValues(tran->OldValues, tran->input) UpdateMemory(tran,version,NewValues) ReleaseOwnership(tran,version) 4
ReleaseOwnership(tran,version) If IsInitiator then failtran = Ownership[failadd] if failtran = Nobody then return failversion = failtran->version if failtran->stable Transaction(failtran,failversion,False) Performance von STM: Der Vergleich von STM mit anderen non-blocking Methoden für nicht optimierte Versionen der Algorithmen. Herlihy s method [1, 2]: Kopieren des geteilten Speicherbereichs, Operation auf Kopie ausführen, Pointer des geteilten Speicherbereichs auf Kopie setzten. Engpass: Kopieren großer Speicherbereiche. Cooperative method[1,3]: An Sperren gescheiterte Prozesse helfen rekursiv (der Abhängigkeitskette entlang) den Inhabern der Sperre ihre Operationen auszuführen. Engpass: Rekursiver Hilfs-Overhead. Throughput ratio of STM/other Herlihy s method Counter Bus 0,34 Alewife 0,30 Double linked queue Bus 6,07 Alewife 2,44 Resource Allocation Bus 22,5 Alewife 24,14 Priority queue Bus 0,42 Alewife 0,41 1,98 1,92 1,44 1,75 1,09 1,12 1,26 1,27 (Bus= klassisches Mehrkern System, Alewife= Netzwerkspeicher ) 10 processors 60 processors Cooperative Herlihy s method method 0,74 0,45 58,9 12,9 85,61 59,8 2,8 1,1 Eindeutig zu sehen ist die bessere Skalierung vom STM mit steigender Anzahl an Prozessoren. Cooperative method 8,44 7,6 3,36 7,28 1,69 2,35 2,16 2,24 5
DSTM2: DSTM2 [4] ist eine Java-Bibliothek, welche ein Framework zur einfachen Benutzung von Software Transactional Memory erlaubt. Hierzu bietet DSTM2 factories. Diese erstellen Objekte, welche nach dem Konzept des STM synchronisieren. Anwendungsbeispiel: Erstellen eines Interface Node für Objekte, die mittels STM synchronisieren und somit geteilt nutzbar sind: @atomic public interface INode{ int getvalue(); void setvalue(int value); INode getnext(); void setnext(inode value); } Erstellen einer Factory, welche Objekte vom Typ Node erstellen kann: Factory <INode> factory = dstm2.thread.makefactory(inode.class); Das Erstellen eines Objektes vom Typ Node kann nun die factory übernehmen: INode node = factory.create(); Exemplarisch wollen wir nun eine Node mit einem Wert in eine verkettete Liste einfügen. Der Aufruf der Transaktion mittel DSTM2 lautet: result = Thread.doIt(new Callable <Boolean>(){ public Boolean call(){ return alist.insert(value); } } doit lößt hier die eigentlich Transaktion aus und wird bis zum Erfolg oder Abbruch durch Exception wiederholt. alist ist ein Objekt einer gewöhnlichen Klasse, mit den üblichen Methoden zu Verwaltung einer verketteten Liste. Einzige Ausnahmen sind die Verwendung der DSTM2 Factory und @atomic INode Objekte: public class List{ static Factory <INode> factory = Thread.makeFactory(INode.class); public boolean insert(int v){ INode newnode = factory.create(); newnode.setvalue(v); //...newnode.setnext etc //...return true/false public int remove(){ //... }//... Durch die Verwendung der DTSM2 Factory und @atomic INode können schließlich beliebige Prozesse auf die verkettete Liste alist mittels doit zugreifen, ohne sich um die Synchronisierung kümmern zu müssen. 6
Quellen: [1] Nir Shavit and Dan Touitou. Software Transactional Memory [2] M. Herlihy. A methodology for implementing highly concurrent data objects. [3] A. Israeli and L Rappoprt. Disjoint-Access-Parallel Implementations of Strong Shared Memory [4] Maurice Herlihy, Victor Luchangco, Mark Moir. A Flexible Framework for Implementing Software Transactional Memory [5] http://en.wikipedia.org/wiki/transactional_memory [6] http://en.wikipedia.org/wiki/software_transactional_memory Anhang: Vollständiger STM Algorithmus von Shavit und Touitou: StartTransaction(input,DataSet) Initialize(Tran,input,DataSet) Tran->Stable = TrueA Transaction(Tran,Tran->Version,True) Tran->Stable = False Tran->Version++ If Tran->Status = Success then return(sucess,calcoutput(tran->oldvalues,input)) return Failure Transaction(tran,version,IsInitiator) AcquireOwnerships(tran,version) (status,failadd) = LL(tran->status) If status = Null then If(version!= tran->version then return SC(tran->status,(Success,0) (status,failadd) = LL(tran->status) If status = Success then AgreeOldValues(tran,version) NewValues = CalcNewValues(tran->OldValues, tran->input) UpdateMemory(tran,version,NewValues) ReleaseOwnership(tran,version) ReleaseOwnership(tran,version) If IsInitiator then failtran = Ownership[failadd] if failtran = Nobody then return failversion = failtran->version if failtran->stable Transaction(failtran,failversion,False) 7
AquireOwnerships(tran,version) transize = tran->size for i = 1 to size do while true do location = tran->add[i] if LL(tran->status)!= Null then return owner = LL (Ownershipüs[tran->Add[i]]) if tran->version!= version return if owner = tran then exit while loop if owner = Nobody then if SC(tran->status, (Null, 0)) then if SC(Ownerships[location], tran) then exit while loops if SC(tran->status, (Failure,i)) then return ReleaseOwnerships(tran, version) size = tran->size for i = 1 to size do location = tran->add[i] if LL(Ownerships[location]) = tran then if tran->version!= version then return SC(Ownerships[location], Nobody) AgreeOldValues(tran, version) size = tran->size for i = 1 to size do location = tran->add[i] if LL(tran->OldValues[location])!= Null then if tran->version!= version then return SC(tran->OldValues[location], Memory[location]) UpdateMemory(tran, version, newvalues) size = tran->size for i = 1 to size do location = tran->add[i] oldvalue = LL(Memory[location]) if tran->allwritten then return if version!= tran->version then return if version!= newvalues[i] then SC(Memory[location], newvallues[i]) if(not LL(tran->AllWritten)) then if version!= tran->version then return SC(tran->AllWritten, True) 8