Looking for multithreading Keywords? Try Ask4Keywords

multithreadingErste Schritte mit Multithreading


Bemerkungen

Multithreading ist eine Programmiertechnik, bei der eine Task in separate Ausführungsthreads unterteilt wird. Diese Threads werden gleichzeitig ausgeführt, entweder indem sie verschiedenen Prozessorkernen zugewiesen werden oder durch Zeitaufteilung.

Beim Entwerfen eines Multithread-Programms sollten die Threads so unabhängig wie möglich gemacht werden, um die höchste Geschwindigkeit zu erreichen.
In der Praxis sind die Threads selten völlig unabhängig, was eine Synchronisation erforderlich macht.
Die maximale theoretische Beschleunigung kann nach dem Amdahlschen Gesetz berechnet werden.

Vorteile

  • Beschleunigen Sie die Ausführungszeit, indem Sie die verfügbaren Verarbeitungsressourcen effizient nutzen
  • Ermöglichen, dass ein Prozess reaktionsfähig bleibt, ohne lange Berechnungen oder teure E / A-Vorgänge aufteilen zu müssen
  • Priorisieren Sie einfach bestimmte Vorgänge gegenüber anderen

Nachteile

  • Ohne sorgfältiges Design können schwer zu findende Fehler eingeführt werden
  • Das Erstellen von Threads ist mit einem gewissen Aufwand verbunden

Kann der gleiche Thread zweimal laufen?

Die häufigste Frage war, dass ein Thread zweimal ausgeführt werden kann.

Die Antwort darauf ist, dass ein Thread nur einmal ausgeführt werden kann.

Wenn Sie versuchen, denselben Thread zweimal auszuführen, wird er zum ersten Mal ausgeführt, gibt jedoch einen Fehler zum zweiten Mal aus und der Fehler ist IllegalThreadStateException.

Beispiel :

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();  
 }  
}  
 

Ausgabe :

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

Deadlocks

Ein Deadlock tritt auf, wenn jedes Mitglied einer Gruppe von zwei oder mehr Threads warten muss, bis eines der anderen Mitglieder etwas unternimmt (z. B. um eine Sperre aufzuheben), bevor es fortfahren kann. Ohne Eingriff warten die Threads für immer.

Ein Pseudocode-Beispiel für ein Deadlock-Design ist:

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

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

Ein Deadlock kann auftreten, wenn thread_1 A , aber noch nicht B und thread_2 B , nicht aber A . Wie im folgenden Diagramm gezeigt, warten beide Threads für immer. Deadlock-Diagramm

Wie vermeide ich Deadlocks?

Als Faustregel sollten Sie die Verwendung von Sperren und den Code zwischen Sperren und Entsperren minimieren.

Locks in derselben Reihenfolge erwerben

Ein Redesign von thread_2 löst das Problem:

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

Beide Threads erfassen die Ressourcen in derselben Reihenfolge und vermeiden so Deadlocks.

Diese Lösung wird als "Ressourcenhierarchielösung" bezeichnet. Es wurde von Dijkstra als Lösung für das Problem der "Essensphilosien" vorgeschlagen.

Auch wenn Sie eine strikte Reihenfolge für die Sperrenerfassung angeben, kann eine solche statische Sperrenerfassungsreihenfolge zur Laufzeit dynamisiert werden.

Betrachten Sie den folgenden Code:

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

Selbst wenn die Reihenfolge der Sperrenerfassung sicher aussieht, kann dies zu einem Deadlock führen, wenn thread_1 auf diese Methode mit Objekt_1 als Parameter A und Object_2 als Parameter B zugreift, und thread_2 in entgegengesetzter Reihenfolge, dh Object_2 als Parameter A und Object_1 als Parameter B.

In einer solchen Situation ist es besser, einige eindeutige Bedingungen über Objekt_1 und Objekt_2 mit einer Art von Berechnung ableiten zu lassen, z. B. unter Verwendung von Hashcode beider Objekte. Wenn also unterschiedliche Threads in dieser Methode in beliebiger parametrischer Reihenfolge eingegeben werden, wird der eindeutige Zustand jedes Mal abgeleitet Erfassungsauftrag sperren.

Beispiel: Objekt hat einen eindeutigen Schlüssel, z. B. accountNumber im Falle eines Account-Objekts.

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 - Neue Threads erstellen

Dieses einfache Beispiel zeigt, wie Sie mehrere Threads in Java starten. Beachten Sie, dass die Ausführung der Threads nicht garantiert ist und die Ausführungsreihenfolge für jeden Lauf unterschiedlich sein kann.

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);
        }

    }

}
 

Zweck

Threads sind die untergeordneten Teile eines Computersystems, bei dem die Befehlsverarbeitung erfolgt. Es wird von CPU / MCU-Hardware unterstützt / bereitgestellt. Es gibt auch Softwaremethoden. Der Zweck des Multithreading ist, wenn möglich, parallele Berechnungen durchzuführen. Somit kann das gewünschte Ergebnis in einer kleineren Zeitscheibe erhalten werden.

Rennbedingungen

Ein Datenrennen oder eine Race-Bedingung ist ein Problem, das auftreten kann, wenn ein Multithread-Programm nicht ordnungsgemäß synchronisiert wird. Wenn zwei oder mehr Threads ohne Synchronisation auf denselben Speicher zugreifen und mindestens einer der Zugriffe eine "Schreiboperation" ist, tritt ein Datenrennen auf. Dies führt zu einem plattformabhängigen, möglicherweise inkonsistenten Verhalten des Programms. Beispielsweise kann das Ergebnis einer Berechnung von der Thread-Planung abhängen.

Leser-Schriftsteller Problem :

writer_thread {
    write_to(buffer)
}

reader_thread {
    read_from(buffer)
}
 

Eine einfache Lösung:

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

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

Diese einfache Lösung funktioniert gut, wenn es nur einen Leser-Thread gibt, aber wenn es mehr als einen gibt, verlangsamt sich die Ausführung unnötig, da die Leser-Threads gleichzeitig lesen können.

Eine Lösung, die dieses Problem vermeidet, könnte sein:

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)
}
 

Beachten Sie, dass reader_count während des gesamten Schreibvorgangs gesperrt ist, sodass kein Leser mit dem Lesen beginnen kann, solange das Schreiben noch nicht abgeschlossen ist.

Jetzt können viele Reader gleichzeitig lesen, aber es kann ein neues Problem auftreten: Der reader_count möglicherweise niemals 0 , sodass der Writer-Thread niemals in den Puffer schreiben kann. Dies nennt man Hungern , es gibt verschiedene Lösungen, um dies zu vermeiden.


Selbst Programme, die möglicherweise korrekt erscheinen, können problematisch sein:

boolean_variable = false 

writer_thread {
    boolean_variable = true
}

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

Das Beispielprogramm wird möglicherweise niemals beendet, da der Reader-Thread das Update möglicherweise nie vom Writer-Thread sehen kann. Wenn die Hardware beispielsweise CPU-Caches verwendet, werden die Werte möglicherweise zwischengespeichert. Und da ein Schreib- oder Lesevorgang in ein normales Feld nicht zu einer Aktualisierung des Caches führt, wird der geänderte Wert möglicherweise nie vom lesenden Thread gesehen.

C ++ und Java definiert im sogenannten Speichermodell, was richtig synchronisiert bedeutet: C ++ - Speichermodell , Java-Speichermodell .

In Java wäre es eine Lösung, das Feld als flüchtig zu deklarieren:

volatile boolean boolean_field;
 

In C ++ wäre eine Lösung das Feld als atomar zu deklarieren:

std::atomic<bool> data_ready(false)
 

Ein Datenrennen ist eine Art Rennbedingung. Aber nicht alle Rennbedingungen sind Datenrennen. Folgendes, das von mehr als einem Thread aufgerufen wird, führt zu einer Race-Bedingung, jedoch nicht zu einem Datenrennen:

class Counter {
    private volatile int count = 0;

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

Es ist gemäß der Java-Speichermodellspezifikation korrekt synchronisiert, daher kein Datenwettlauf. Dennoch führt es zu Wettlaufbedingungen, zB hängt das Ergebnis von der Verschachtelung der Fäden ab.

Nicht alle Datenrennen sind Fehler. Ein Beispiel für eine sogenannte benigne Rennbedingung ist 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 ist die Leistung des Codes wichtiger als die Richtigkeit der Zählung von numInvocation.