programmera.net -> java -> normal för utskrift | info@programmera.net |
Teori bakom trådad programmering
1. Teori bakom trådad programmering
På denna sida kommer teorin bakom trådar och trådad programmering att undersökas. Det kommer visa sig att trådad programmering är mycket mer komplicerad än normal programmering, och därför måste man använda sig av ett formellt resonemang för att garantera att inga fel uppkommer. Nya begrepp intorduceras som atomära handlingar, at-most-one och försäkrande resonemang.
2. Processer och trådar
Ett operativsystem klarar nu för tiden att hålla flera program igång samtidigt. Den lyckas med detta genom att lägga varje program i en egen process, och snabbt hoppa mellan de olika processerna så att alla ser ut att köra samtidigt. En process (ett program) kan också göra flera saker samtidigt, men då kallas det att processen har flera trådar. De flesta program man skriver behöver bara en tråd; den som kör main(). Skillnade mellan en process och en tråd är:
3. Kommunikation mellan trådar
Ofta behöver trådar något sätt att kommunicera med varandra. Kommunikation kan bara ske på två sätt:
Om trådarna inte kommunicerar med varandra kallas de oberoende. Att programmera trådar som är oberoende är inte svårare än vanlig programmering (beskrivs på sidan
oberoende trådar ). Det är först då trådarna har delade variabler som vi behöver göra en djupare analys över vad som egentligen sker.
4. Atomära handlingar
Om ett icke trådat program består av ett antal satser (kodrader), så kommer de de olika satserna utförs i följd. I ett trådat program kan en tråd när som helst avbrytas av en annan (med högre prioritet) som börjar arbeta med någonting annat.
I bilden ovan blir tråd 1 avbruten av tråd 2 efter sats 4. Tråd 2 utför satserna 21 till 30 innan tråd 1 tillåts fortsätta med sats 5. När man talar om multitrådade program inför man begreppet atomära handlingar. Varje stas består av en eller flera atomära handlingar. En atomär handling kännetecknas av att den inte kan avbrytas mitt i av någon annan tråd. Antingen har handlingen skett, eller så har den inte det.
Svar: på (T*A)!/(A!^T) sätt.
1. T1(A) T1(B) T2(A) T2(B)
2. T2(A) T2(B) T1(A) T1(B)
3. T1(A) T2(A) T1(B) T2(B)
4. T2(A) T1(A) T2(B) T1(B)
5. T1(A) T2(A) T2(B) T1(B)
6. T2(A) T1(A) T1(B) T2(B)
5. Debugga trådade program
I Exempel2 ovan visade det sig att ett program med 3 trådar som utför 2 atomära handlingar kan utföras på 90 olika sätt. Större program kan exekveras på miljontals sätt. Det går alltså inte att testköra ett trådat program på samma sätt som ett icke trådat eftersom en lyckad testkörning inte garanterar att programmet fungerar. Ett fel kanske bara uppkommer var miljonte testkörning, och ingen orkar testköra ett program en miljon gånger. När man vill debugga ett trådat program måste man därför använda sig av andra metoder, den metod som Gregory R. Andrews rekommenderar i Foundations of multithreaded.. är försäkrande resonemang (assertional reasoning). Förenklat kan man säga att det försäkrande resonemanget går ut på att man ställer upp olika tillstånd för programmet. Ett tillstånd kan vara x>0 eller Y<X. Varje atomär handling är en modifierar tillstånden. Målet med denna metod är att logiskt bevisa att ett visst oönskat tillstånd inte kan uppkomma, oavsett vilken ordning de atomära handlingarna utförs.
6. Är x++; atomär?
Vi har tidigare sagt att en sats består av en eller flera atomära handlingar.
Nedan beskrivs den längsta tänkbara kedjan av atomära händelser som utför arbetet x++;:
Händelserna read & load gör egentligen samma sak, nämligen att ladda en variabel från huvudminnet till arbetsminnet. Av någon anledning blir det ändå två atomära handlingar. Samma sak gäller för store & write, som båda lagrar en variabel till huvudminnet.
7. At-Most-One
Nu presenteras en regel som kallas för at-most-one och definieras på detta sätt: En sats x=expr; kan ses som atomär om:
Nedan ges ett exempel taget från dokumentationen av JVM:
class Sample {
int a = 1, b = 2;
void hither() {
a = b;
}
void yon() {
b = a;
}
}
Varför får vi detta märkliga resultat? Om satserna a=b; och b=a; är atomära blir svaret lätt. Antingen är a=b=1 eller a=b=2, beroende på vilken sats som exekveras först.
Satserna kan inte ses som atomära eftersom de inte uppfyller at-most-one, därför kommer det oväntade resultatet med. Följande bild beskriver de olika atomära händelserna hos funktionerna:
En möjlig väg är alltså denna:
Vi har nu fått det märkliga resultatet a=2,b=1.
8. Programmeringslogik
För att kunna utföra våra försäkrande resonemang krävs lite programmeringslogik. Programmeringslogik formuleras i formler som allmänt har följande utseende:
Där P och Q beskriver vilka värden programmets variabler har före och efter satsen Sats har körts. Ett exempel på en formel är:
{P} Sats {Q}
Denna formel kallas för ett teorem eftersom den är giltig. Även följande formel är ett teorem:
{x==0} x=x+1; {x==1}
En av de enklaste lagarna för formler är kompositionslagen:
{x==0} x=x+1; {x>0}
Man använder kompositionslagen för att bevisa att tillstånd P kommer resultera i tillståndet R genom att beskriva alla mellanled. Till exempel, bevisa:
Om: {P} Sats1 {Q}, {Q} Sats2 {R}
Så: {P} Sats1 Sats2 {R}
om varje sats antas vara atomär. För att bevisa detta måste man beskriva alla möjliga vägar som progammet kan gå som teorem. Det snabbaste sättet att skriva beviset är detta:
{x==0}
Tråd1: x=x+1;
Tråd2: x=x+2;
{x==3}
Beviset är trivialt, men illustrerar ändå principen.
{x==0}
Tråd1: {x==0 eller x==2}
x=x+1;
{x==1 eller x==3}
(Tråd1 dör)
Tråd2: {x==0 eller x==1}
x=x+2;
{x==2 eller x==3}
(Tråd2 dör)
{x==3}
9. Konflikter
Ofta finns det tillstånd som man alltid vill ska gälla, och målet med försäkrande resonemang är att garantera att dessa tillstånd alltid bevaras. Därför vill man ofta att samtliga tilldelningssatser i ett program ska vara konfliktfria med det känsliga tillståndet. En tilldelningssats A med grundtillståndet P är konfliktfritt med avseende på ett tillstånd C om:
Alltså: Om C gäller innan tilldelningssatsen A kommer C även gälla efter satsen.
{C och P} A {C}
10. Synkronsisering
Det finns två typer av synkronisering:
11. Deadlock
Trådprogrammerarens fasa är deadlock, det inträffar då tråd1 låser objektA och vill även låsa objektB. En annan tråd, tråd2 har låst objektB och väntar på att även få låsa objektA. När detta dödläge inträffar hänger sig programmet, och man måste stänga av det. Det är programmerarens uppgift att se till att deadlock inte inträffar, men det är svårt att förutse alla situationer som kan uppkomma då flera trådar körs samtidigt.
12. Typer av probelm
Det finns några grundproblem man kan råka på:
Ett annat fall som inte passar in i grundproblemen är då man använder trådar i GUI. I ett program som har direkt kontakt med användaren vill man att användaren inte ska behöva vänta på en tidskrävande uppgift. Genom att starta den tidskrävande uppgiften i en separat tråd lämnar man tillbaka kontrollen till användaren så att användaren kan fortsätta använda programmet under väntetiden, annars kanske användaren tror att programmet har hängt sig och startar om.