multithreadingAan de slag met multithreading


Opmerkingen

Multithreading is een programmeertechniek die bestaat uit het verdelen van een taak in afzonderlijke uitvoeringsdraden. Deze threads worden gelijktijdig uitgevoerd, hetzij door te worden toegewezen aan verschillende verwerkingskernen, hetzij door tijd te snijden.

Bij het ontwerpen van een multithreaded programma moeten de threads zo onafhankelijk mogelijk van elkaar worden gemaakt om de grootste versnelling te bereiken.
In de praktijk zijn de threads zelden volledig onafhankelijk, waardoor synchronisatie noodzakelijk is.
De maximale theoretische versnelling kan worden berekend met behulp van de wet van Amdahl .

voordelen

  • Versnel de uitvoeringstijd door de beschikbare verwerkingsbronnen efficiënt te gebruiken
  • Laat een proces responsief blijven zonder de noodzaak om langdurige berekeningen of dure I / O-bewerkingen te splitsen
  • Prioriteer eenvoudig bepaalde bewerkingen boven andere

nadelen

  • Zonder zorgvuldig ontwerp kunnen moeilijk te vinden bugs worden geïntroduceerd
  • Discussies maken vergt wat overhead

Kan dezelfde thread twee keer worden uitgevoerd?

Het was de meest voorkomende vraag dat dezelfde thread twee keer kan worden uitgevoerd.

Het antwoord hierop is dat één thread slechts één keer kan worden uitgevoerd.

als u dezelfde thread twee keer probeert uit te voeren, wordt het voor de eerste keer uitgevoerd, maar geeft het voor de tweede keer een foutmelding en is de fout IllegalThreadStateException.

voorbeeld :

public class TestThreadTwice1 extends Thread{  
 public void run(){  
   System.out.println("running...");  
 }  
 public static void main(String args[]){  
  TestThreadTwice1 t1=new TestThreadTwice1();  
  t1.start();  
  t1.start();  
 }  
}  
 

output :

running
       Exception in thread "main" java.lang.IllegalThreadStateException
 

impasses

Een impasse treedt op wanneer elk lid van een groep van twee of meer threads moet wachten tot een van de andere leden iets doet (bijvoorbeeld om een slot vrij te geven) voordat het verder kan gaan. Zonder tussenkomst zullen de threads voor altijd wachten.

Een voorbeeld van een pseudocode van een impasse-gevoelig ontwerp is:

thread_1 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}

thread_2 {
    acquire(B)
    ...
    acquire(A)
    ...
    release(A, B)
}
 

Een impasse kan optreden wanneer thread_1 A heeft verkregen, maar nog geen B , en thread_2 B heeft verkregen, maar niet A Zoals getoond in het volgende diagram, zullen beide threads voor altijd wachten. Deadlock-diagram

Hoe deadlocks te voorkomen

Als algemene vuistregel, minimaliseer het gebruik van sloten en minimaliseer code tussen vergrendelen en ontgrendelen.

Verwerf sloten in dezelfde volgorde

Een herontwerp van thread_2 lost het probleem op:

thread_2 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}
 

Beide threads verwerven de bronnen in dezelfde volgorde, waardoor deadlocks worden vermeden.

Deze oplossing staat bekend als de "Resource hierarchy solution". Het werd voorgesteld door Dijkstra als een oplossing voor het "Eetfilosofenprobleem".

Soms, zelfs als u een strikte volgorde voor slotverwerving opgeeft, kan een dergelijke volgorde voor statische vergrendeling tijdens runtime dynamisch worden gemaakt.

Overweeg de volgende code:

void doCriticalTask(Object A, Object B){
     acquire(A){
        acquire(B){
            
        }
    }
}
 

Zelfs als de volgorde van vergrendeling veilig lijkt, kan het een impasse veroorzaken wanneer thread_1 deze methode opent met bijvoorbeeld Object_1 als parameter A en Object_2 als parameter B en thread_2 doet in tegenovergestelde volgorde, dwz Object_2 als parameter A en Object_1 als parameter B.

In een dergelijke situatie is het beter om een unieke voorwaarde te hebben afgeleid met behulp van zowel Object_1 als Object_2 met een soort berekening, bijvoorbeeld met behulp van de hashcode van beide objecten, dus telkens wanneer een andere thread in die methode wordt ingevoerd in elke parametrische volgorde, komt elke keer dat die unieke voorwaarde de acquisitieorder vergrendelen.

bijvoorbeeld Say Object heeft een unieke sleutel, bijvoorbeeld accountNumber in het geval van Account-object.

void doCriticalTask(Object A, Object B){
    int uniqueA = A.getAccntNumber();
    int uniqueB = B.getAccntNumber();
    if(uniqueA > uniqueB){
         acquire(B){
            acquire(A){
                
            }
        }
    }else {
         acquire(A){
            acquire(B){
                
            }
        }
    }
}
 

Hallo multithreading - Nieuwe threads maken

Dit eenvoudige voorbeeld laat zien hoe u meerdere threads in Java kunt starten. Merk op dat de threads niet gegarandeerd in volgorde worden uitgevoerd en dat de volgorde van uitvoering kan variëren voor elke run.

public class HelloMultithreading {

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new MyRunnable(i));
            t.start();
        }
    }

    public static class MyRunnable implements Runnable {

        private int mThreadId;

        public MyRunnable(int pThreadId) {
            super();
            mThreadId = pThreadId;
        }

        @Override
        public void run() {
            System.out.println("Hello multithreading: thread " + mThreadId);
        }

    }

}
 

Doel

Threads zijn de onderdelen op laag niveau van een computersysteem waarin opdrachtverwerking plaatsvindt. Het wordt ondersteund / geleverd door CPU / MCU-hardware. Er zijn ook softwaremethoden. Het doel van multi-threading is om indien mogelijk parallel aan elkaar te rekenen. Aldus kan het gewenste resultaat worden verkregen in een kleinere tijdsplak.

Race omstandigheden

Een datarace of raceconditie is een probleem dat kan optreden wanneer een multithreaded programma niet correct wordt gesynchroniseerd. Als twee of meer threads toegang hebben tot hetzelfde geheugen zonder synchronisatie en ten minste een van de toegangen een 'schrijf'-bewerking is, vindt er een gegevensrace plaats. Dit leidt tot platformafhankelijk, mogelijk inconsistent gedrag van het programma. Het resultaat van een berekening kan bijvoorbeeld afhangen van de threadplanning.

Lezers-schrijvers Probleem :

writer_thread {
    write_to(buffer)
}

reader_thread {
    read_from(buffer)
}
 

Een eenvoudige oplossing:

writer_thread {
    lock(buffer)
    write_to(buffer)
    unlock(buffer)
}

reader_thread {
    lock(buffer)
    read_from(buffer)
    unlock(buffer)
}
 

Deze eenvoudige oplossing werkt goed als er slechts één reader-thread is, maar als er meer dan één is, vertraagt dit de uitvoering onnodig, omdat de reader-threads tegelijkertijd kunnen lezen.

Een oplossing die dit probleem voorkomt, kan zijn:

writer_thread {
    lock(reader_count)
    if(reader_count == 0) {
        write_to(buffer)
    }
    unlock(reader_count)
}

reader_thread {
    lock(reader_count)
    reader_count = reader_count + 1
    unlock(reader_count)

    read_from(buffer)

    lock(reader_count)
    reader_count = reader_count - 1
    unlock(reader_count)
}
 

Merk op dat reader_count gedurende de hele schrijfbewerking is vergrendeld, zodat geen lezer kan beginnen met lezen terwijl het schrijven niet is voltooid.

Nu kunnen veel lezers tegelijkertijd lezen, maar er kan zich een nieuw probleem voordoen: de reader_count kan nooit 0 bereiken, zodat de writer-thread nooit naar de buffer kan schrijven. Dit wordt verhongering genoemd , er zijn verschillende oplossingen om dit te voorkomen.


Zelfs programma's die misschien correct lijken, kunnen problematisch zijn:

boolean_variable = false 

writer_thread {
    boolean_variable = true
}

reader_thread {
    while_not(boolean_variable)
    {
       do_something()
    }         
}
 

Het voorbeeldprogramma wordt mogelijk nooit beëindigd, omdat de thread van de lezer mogelijk nooit de update van de thread van de schrijver ziet. Als de hardware bijvoorbeeld CPU-caches gebruikt, kunnen de waarden in de cache worden opgeslagen. En omdat een schrijven of lezen naar een normaal veld niet leidt tot een vernieuwing van de cache, wordt de gewijzigde waarde misschien nooit gezien door de leestekst.

C ++ en Java definieert in het zogenaamde geheugenmodel wat goed gesynchroniseerd betekent: C ++ Memory Model , Java Memory Model .

In Java zou een oplossing zijn om het veld als vluchtig te verklaren:

volatile boolean boolean_field;
 

In C ++ zou een oplossing zijn om het veld als atomair te verklaren:

std::atomic<bool> data_ready(false)
 

Een datarace is een soort raceconditie. Maar niet alle race-omstandigheden zijn data races. Het volgende dat door meer dan één thread wordt aangeroepen, leidt tot een racevoorwaarde maar niet tot een gegevensrace:

class Counter {
    private volatile int count = 0;

    public void addOne() {
     i++;
    }
}
 

Het is correct gesynchroniseerd volgens de Java Memory Model-specificatie, daarom is het geen gegevensrace. Maar toch leidt het tot raceomstandigheden, bijv. Het resultaat hangt af van het verweven van de draden.

Niet alle data races zijn bugs. Een voorbeeld van een zogenaamde goedaardige raceconditie is de sun.reflect.NativeMethodAccessorImpl:

class  NativeMethodAccessorImpl extends MethodAccessorImpl {
    private Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;
    
    NativeMethodAccessorImpl(Method method) {
        this.method = method;
    }

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
        if (++numInvocations > ReflectionFactory.inflationThreshold()) {
              MethodAccessorImpl acc = (MethodAccessorImpl)
            new MethodAccessorGenerator().
            generateMethod(method.getDeclaringClass(),
                             method.getName(),
                             method.getParameterTypes(),
                             method.getReturnType(),
                             method.getExceptionTypes(),
                             method.getModifiers());
                             parent.setDelegate(acc);
          }
          return invoke0(method, obj, args);
    }
    ...
}
 

Hier is de uitvoering van de code belangrijker dan de juistheid van het aantal numInvocation.