• Keine Ergebnisse gefunden

Deadlockvermeidung

Im Dokument Programmiertechniken (NF) (Seite 100-105)

III. Nebenl¨ aufigkeit 91

9.5. Deadlockvermeidung

Es gibt zwei wichtige Strategieen, wie man einen Deadlock vermeiden kann. Die einfachste ist es, einen Thread seine beiden St¨abchen in umgekehrter Reihenfolge nehmen zu lassen.

Damit konkurriert er zun¨achst mit einem Nachbarn um das gleiche St¨abchen, ohne bereits ein anderes St¨abchen zu halten. Nur einer kann das St¨abchen bekommen und es kann nicht zu der Situation kommen, in der alle Philosophen einen Stab halten und den zweiten haben wollen. der Deadlock wurde vermieden. Dies l¨asst sich in unserem Programm sehr einfach dadurch realisieren, dass wir dem letzten Philosophen die Stabe in umgekehrter Reihenfolge ¨ubergeben:

t 5 = t h r e a d i n g . T hr ea d ( t a r g e t=p h i l , a r g s =(5 , s1 , s 5 ) ) Nach dieser kleinen ¨Anderung ist das Programm deadlockfrei.

Diese Strategie l¨asst sich auch verallgemeinern, in dem man Threads alle Locks immer gem¨aß einer globalen Reihenfolge nehmen l¨asst, also s1 vor s5, da s1 “kleiner” als s5 ist.

Wir werden hierauf sp¨ater noch einmal eingehen.

Nachteil dieser Deadlocksvermeisungsstrategie ist es, dass die Threads sich unterschiedlich verhalten. M¨ochte man alle Threads mit demselben Code ausstatten, kann man dies mit der zweiten Strategie realisieren.

Kritisch war es ja, wenn eine Thread versuchte, den zweiten Stab zu nehmen, obwohl dieser nicht verf¨ugbar war. Eine m¨ogliche L¨osung w¨are es, den ersten Stab wieder zur¨uckzulegen, wenn man den zweiten nicht bekommen kann. dann steht dieser wieder einem anderen Philosophen zur Verf¨ugung und dieser kann essen. Wir warten also, bis dieser komplett fertig ist. Die Implementierung soll in der ¨Ubung vorgenommen werden. Hierf¨ur kann man die Lock-Methode locked () verwenden, welche einen booleschen Wert zur¨uckgibt, der anzeigt, ob ein Lock belegt ist.

Analysiert man diesen Ansatz, bekommt man den Eindruck, dass es wichtig ist, das Testen, ob der zweite Lock verf¨ugbar ist und das Nehmen dieses Locks atomar ausgef¨uhrt werden m¨usste. Dies ist aber nicht der Fall, wie ihr in der ¨Ubung genauer untersuchen werdet.

Die Situation, in der der zweite Lock immer genau in dem Moment weggeschnappt wird, nachdem man ihn gesehen hat, aber bevor man ihn nehmen kann, kann sich zwar einmal rund um den kompletten Tisch abspielen. Der letzte Philosoph wird aber sehen, dass sein zweiter Stab bereits belegt ist und entsprechend seinen ersten Stab wieder zur¨ucklegen, was die Deadlocksituation aufl¨ost.

auch dieses Verfahren kann in anderen Verklemmungssituationen in nebenl¨aufigen Sys-temen zur Aufl¨osung des Deadlocks verwendet werden. Die Implementierung erfolgt als Ubung.¨

Als weiteres Beispiel betrachten wir die Definition von Bankkonten, auf welchen nebenl¨aufig Operationen ausgef¨uhrt werden k¨onnen. Wir beginnen mit der Definition der grundlegenden Klasse:

c l a s s A c c o u n t :

d e f i n i t ( s e l f , amount ) : s e l f . b a l a n c e = amount d e f g e t b a l a n c e ( s e l f ) :

r e t u r n s e l f . b a l a n c e d e f d e p o s i t ( s e l f , amount ) :

s e l f . b a l a n c e = s e l f . b a l a n c e + amount d e f w i t h d r a w ( s e l f , amount ) :

i f s e l f . b a l a n c e >= amount :

s e l f . b a l a n c e = s e l f . b a l a n c e − amount r e t u r n True

e l s e :

r e t u r n F a l s e

Unsere Klasse hat einen Konstruktor, eine getter-Methode f¨ur den aktuellen Kontostand und Methoden zum Ein- und Auszahlen. In der Methode zum Auszahlen gew¨ahrleisten wir, dass der Kontostand nicht negativ werden darf, indem wir vorher testen, ob ausreichend Geld auf dem Konto ist.

Wenn nun mehrere Threads gleichzeitig von einem Konto abbuchen und/oder einzahlen, operieren diese Threads ja auf dem gleichen Konto, wodurch dies eine geteilte Ressource ist und der Code von deposit und withdraw zum kritischen Bereich wird. Insbesondere der Code von withdraw ist hierbei kritisch, da wir zwei mal auf self . balance zugreifen.

Als Beispiel k¨onnte dies problematisch sein, wenn wir ein Konto mit 50 Euro haben und gleichzeitig, zweimal 42 Euro abheben wollen. In einem ung¨unstigen Schedule, k¨onnte dies dazu f¨uhren, dass beide Threads zun¨achst checken, ob ausreichend Geld verf¨ugbar ist und beide sehen, dass der Kontostand reicht, um 42 Euro abzuheben. Dann treten beide Threads in den if-Zweig ein und heben das Geld ab, was aber dazu f¨uhrt, dass der Kontostand letztlich -32 wird, also negativ, was wir ja verhindern wollten.

Wir m¨ussen also einen Lock einf¨uhren, mit welchem wir die kritischen Bereiche vor sol-chen Threadwechseln sch¨utzen. Der Code ist jeweils aber nur dann kritisch, wenn er auf demselben Konto operiert, d.h. mehrere gleichezeitige withdraw-Aufrufe auf unterschied-lichen Konten sind unkritisch. Deshalb ist es sinnvoll, den Lock als Attribut in der Klasse abzulegen:

c l a s s A c c o u n t :

d e f i n i t ( s e l f , amount ) : s e l f . b a l a n c e = amount

s e l f . l o c k = t h r e a d i n g . Lock ( ) d e f g e t b a l a n c e ( s e l f ) :

r e t u r n s e l f . b a l a n c e d e f d e p o s i t ( s e l f , amount ) :

s e l f . l o c k . a c q u i r e ( )

s e l f . b a l a n c e = s e l f . b a l a n c e + amount s e l f . l o c k . r e l e a s e ( )

d e f w i t h d r a w ( s e l f , amount ) : s e l f . l o c k . a c q u i r e ( )

i f s e l f . b a l a n c e >= amount :

s e l f . b a l a n c e = s e l f . b a l a n c e − amount s e l f . l o c k . r e l e a s e ( )

r e t u r n True e l s e :

s e l f . l o c k . r e l e a s e ( )

r e t u r n F a l s e

Wichtig ist, dass man in jedem kritischen Bereich zun¨achst den Lock nimmt und ihn dann auch wieder frei gibt, also auch in beiden F¨allen der Verzweigung. Damit man dies nicht mal irgendwo vergisst, ist es sinnvoll, das with-Konstrukt aus Python zu verwenden, welches sicherstellt, dass zu Beginn ein Lock genommen und sp¨ater dann auch wieder frei gegeben wird, auch korrekt vor einenreturn:

c l a s s A c c o u n t :

d e f i n i t ( s e l f , amount ) : s e l f . b a l a n c e = amount

s e l f . l o c k = t h r e a d i n g . Lock ( ) d e f g e t b a l a n c e ( s e l f ) :

r e t u r n s e l f . b a l a n c e d e f d e p o s i t ( s e l f , amount ) :

w i t h s e l f . l o c k :

s e l f . b a l a n c e = s e l f . b a l a n c e + amount d e f w i t h d r a w ( s e l f , amount ) :

w i t h s e l f . l o c k :

i f s e l f . b a l a n c e >= amount :

s e l f . b a l a n c e = s e l f . b a l a n c e − amount r e t u r n True

e l s e :

r e t u r n F a l s e

Als n¨achstes k¨onnte man auf die Idee kommen, in der Methode withdraw die Methode deposit wiederzuverwenden:

d e f w i t h d r a w ( s e l f , amount ) : w i t h s e l f . l o c k :

i f s e l f . b a l a n c e >= amount : s e l f . d e p o s i t (−amount ) r e t u r n True

e l s e :

r e t u r n F a l s e

F¨uhrt man nun diese Methode aus, zeigt sich, dass es zu einer Verklemmung kommt, selbst wenn nur ein Thread verwendet wird. Dies liegt daran, dass nun zwei Mal hintereinander derselbe Lock genommen wird (in jedem with). Um dieses Problem zu l¨osen, stellen die meisten Programmiersprachen sogenannte reentrant-Locks zur Verf¨ugung, bei welchen ein Thread beliebig h¨aufig eine und denselben Lock nehmen darf, ohne zu blockieren. Au-ßerdem gibt erst das release den Lock wieder frei, welches zu dem ¨außersten release geh¨ort, so dass bei einem verschachtelten with der Lock erst freigegeben beim Verlassen

des ¨außersten with frei gegeben wird.

Ein reentrant Lock wird in Python mittels folgenden Codes definiert:

d e f i n i t ( s e l f , amount ) : s e l f . b a l a n c e = amount

s e l f . l o c k = t h r e a d i n g . RLock ( )

Nun ist eine Verklemmung wegen mehrfachem Nehmens eines Locks nicht mehr m¨oglich.

Als n¨achstes wollen wir unsere Klasse um eine Methode zum ¨Uberweisen erweitern. Nat¨urlich soll auch eine ¨Uberweisung unseren Kontostand nicht negativ machen k¨onnen.

d e f t r a n s f e r ( s e l f , o t h e r , amount ) : i f s e l f . w i t h d r a w ( amount ) :

o t h e r . d e p o s i t ( amount ) r e t u r n True

e l s e :

r e t u r n F a l s e

Die Methode bucht zun¨achst das Geld von einem Konto ab und zahlt es, falls genug Geld verf¨ugbar ist, auf das andere Konto ein. Unsch¨on an diesem Code ist, dass der Zwischen-zustand, in dem das Geld zwar schon abgebucht wurde, es aber noch nicht beim anderen Konto angekommen ist, von außen beobachtet werden kann. So ist es z.B. nicht m¨oglich, die gesamte Geldmenge der Bank zu berechnen, da sich gerade immer ein Thread in diesem Zustand befinden k¨onnte, welcher einen Teil der Geldmenge unseres Kreditinstituts h¨alt.

Deshalb w¨are es sch¨oner, den gesamten Rumpf von transfer zu synchronisieren, was wegen der Reentrant-Semantik unseres Locks auch m¨oglich ist:

d e f t r a n s f e r ( s e l f , o t h e r , amount ) : w i t h s e l f . l o c k :

i f s e l f . w i t h d r a w ( amount ) : o t h e r . d e p o s i t ( amount ) r e t u r n True

e l s e :

r e t u r n F a l s e

Nun haben wir aber ein anderes Problem mit unserem Code. Es kann ein Deadlock auftre-ten, wenn wir von einem Kontok1 auf eine anderes Kontok2 ¨uberweisen und gleichzeitig von k2 auf k1 ¨uberweisen7. Dies liegt daran, dass es sein kann, dass der eine Thread zun¨achst den Lock vonk1 nimmt und der andere Thread den Lock vonk2. Danach versu-chen in deposit beide Threads den jeweils anderen Lock zu nehmen und warten zyklisch aufeinander. Wir haben also exakt dieselbe Situation, wie bei zwei Philosophen, die sich gegen¨uber sitzen.

Um den Deadlock zu vermeiden kann man auch dieselben Strategien verwenden, wie im Philosophen-Beispiel. Hier verwenden wir die Strategie, dass ein Philosoph die St¨abchen in umgekehrter Reihenfolge aufnimmt. Dies kann dadurch verallgemeinert werden, dass

7Dieser Deadlock kann auch auftreten, wenn wir zyklisch zwischen mehreren Konten ¨uberweisen, also vonk1nachk2, vonk2 nachk3 und vonk3nachk1.

wir immer zuerst das Konto mit der kleineren Kontonummer locken und danach dann das Konto mit der gr¨oßeren Kontonummer. Beide threads w¨urden in obigem Beispiel also das Konto k1locken (nur einer bekommt den Lock) und erst danach das Kontok2locken.

Eine Kontonummer k¨onnen wir ¨uber ein statisches Attribut realisieren:

c l a s s A c c o u n t : n r s u p p l y = 0

d e f i n i t ( s e l f , amount ) : s e l f . b a l a n c e = amount

s e l f . l o c k = t h r e a d i n g . Lock ( ) s e l f . n r = A c c o u n t . n r s u p p l y

A c c o u n t . n r s u p p l y = A c c o u n t . n r s u p p l y . . .

d e f t r a n s f e r ( s e l f , o t h e r , amount ) : i f s e l f . n r < o t h e r . n r :

w i t h s e l f . l o c k : w i t h o t h e r . l o c k :

i f s e l f . w i t h d r a w ( amount ) : o t h e r . d e p o s i t ( amount ) r e t u r n True

e l s e :

r e t u r n F a l s e e l s e :

w i t h o t h e r . l o c k : w i t h s e l f . l o c k :

i f s e l f . w i t h d r a w ( amount ) : o t h e r . d e p o s i t ( amount ) r e t u r n True

e l s e :

r e t u r n F a l s e

Unter Verwendung von acquire und release ist es m¨oglich etwas mehr Code wiederzuver-wenden, was genauer in der ¨Ubung untersucht werden kann.

Außerdem soll in der ¨Ubung auch die zweite Methode der Deadlockvermeidung mittels Zur¨ucklegen, wenn der zweite Stab nicht verf¨ugbar ist, auf das Kontobeispiel ¨ubertragen werden.

Im Dokument Programmiertechniken (NF) (Seite 100-105)