Frage im Vorstellungsgespräch zur Multithreading-Synchronisation: Finden Sie n Wörter mit m Threads

23

Gibt es eine Möglichkeit, dieses Problem von einer Lösung mit mehreren Threads anstelle eines einzelnen Threads zu profitieren?


In einem Interview wurde ich gebeten, ein Problem mit mehreren Threads zu lösen. Es scheint mir, dass die mehreren Threads keinen Nutzen bringt.

Hier ist das Problem:

Sie erhalten einen Absatz, der n Wörter enthält, Sie erhalten m Threads. Was Sie tun müssen, ist, dass jeder Thread ein Wort drucken und die Steuerung an den nächsten Thread übergeben soll. Auf diese Weise druckt jeder Thread weiterhin ein Wort. Falls der letzte Thread kommt, sollte er den ersten Thread aufrufen. Der Druckvorgang wird wiederholt, bis alle Wörter in einem Absatz gedruckt wurden. Schließlich sollten alle Threads ordnungsgemäß beendet werden. Welche Art von Synchronisation wird verwendet?

Ich bin der festen Überzeugung, dass wir hier keine Vorteile aus den Threads ziehen können, glaube aber, dass der Interviewer versucht, meine Synchronisationsfähigkeiten zu messen. Vermisse ich etwas in diesem Problem, das mehrere Threads wertvoll machen würde?

Keine Notwendigkeit von Code, nur ein paar Gedanken. Ich werde es selbst umsetzen.

rplusg
quelle
Das Hinzufügen eines C ++ - Tags wird hier wahrscheinlich nicht viel helfen. Diese Fragen hier sind eher konzeptionelle Dinge, die über eine bestimmte Sprache hinausgehen.
CHAO
Vertraue deinen Gefühlen. Ich verstehe, wofür sie gehen, aber ich mochte nie Interviewfragen, die so weit davon abweichen, wie Sie das Problem in der realen Welt lösen sollten .
G_P
16
@rplusg - Ein Befragter, der darauf hinwies, dass die Lösung das Problem serialisiert und lediglich den Thread-Overhead hinzufügt, ohne tatsächlich eine gleichzeitige Verarbeitung durchzuführen, würde mich viel mehr beeindrucken. Der Interviewer kann jederzeit darauf bestehen, dass Sie die gestellte Frage beantworten.
David Harkness
Wenn "jeder Thread ein Wort drucken und die Steuerung an den nächsten Thread übergeben soll", klingt das wie serielle Arbeit, dh ein Thread wartet darauf, dass der vorherige beendet wird, und es ist, als würde er ein Relais passieren. warum nicht einfach eine Singlethread-App daraus machen?
Amphibient
1
Ich bekomme es @Blrfl. Es ist so, als müsste ich nachprüfen, ob Sie wissen, wie man Tool X verwendet, aber es war zu faul oder schlampig, um ein authentisches Anwendungsszenario zu entwerfen, das die Verwendung dieses Tools wirklich rechtfertigt schlampig hinein. ehrlich gesagt, wenn ich das in einem Interview gefragt wäre, würde ich ihn darauf rufen und würde wahrscheinlich nicht arbeiten wollen , mit jemand schlampig und halfast wie dieser
amphibient

Antworten:

22

Es klingt für mich, als würden Sie zu einer Semaphorlösung geführt. Semaphoren werden verwendet, um einem anderen Thread zu signalisieren, dass er an der Reihe ist. Sie werden viel seltener verwendet als Mutexe, weshalb sie meiner Meinung nach eine gute Interviewfrage sind. Es ist auch der Grund, warum das Beispiel erfunden zu sein scheint.

Grundsätzlich würden Sie mSemaphoren erstellen . Jeder Thread xwartet auf ein Semaphor xund postet dann auf ein Semaphor, x+1nachdem er seine Sache erledigt hat. Im Pseudocode:

loop:
    wait(semaphore[x])
    if no more words:
        post(semaphore[(x+1) % m])
        exit
    print word
    increment current word pointer
    post(semaphore[(x+1) % m])
Karl Bielefeldt
quelle
Danke für das Kopfgeld. Ich habe eine Weile gebraucht, um herauszufinden, wer es gegeben hat, wenn ich darüber nachgedacht habe.
kdgregory
Entschuldigen Sie meine Unwissenheit, können Sie näher erläutern, wie diese Lösung richtig ist? Ist dies eine neue, ausgefallene Art von Semaphoren? Ich bin mir jedoch sicher, dass die Frage durch eine Wait / Notify-Lösung gelöst wird [welche Semaphoren verwenden].
AJed
Es ist nur eine Reihe von Standardsemaphoren. Nichts besonderes an ihnen. Notify wird in einigen Implementierungen als "post" bezeichnet.
Karl Bielefeldt
@KarlBielefeldt Nun, wenn jeder Thread x auf das Semaphor x wartet, werden alle Threads blockiert und es passiert nichts. Wenn wait (sem) tatsächlich erworben wird (sem), werden sie gleichzeitig eingehen und es gibt keinen Ausschluss. Bis es weitere Klarstellungen gibt, glaube ich, dass in diesem Pseudocode etwas nicht stimmt und es nicht die beste Antwort sein sollte.
AJed
Dies zeigt nur die Schleife für jeden Thread. Der Setup-Code müsste auf das erste Semaphor gepostet werden, um die Dinge zu starten.
Karl Bielefeldt
23

Meiner Meinung nach ist dies eine fantastische Interviewfrage - zumindest unter der Annahme, dass (1) der Kandidat über fundierte Kenntnisse im Threading verfügt und (2) der Interviewer über fundierte Kenntnisse verfügt und die Frage verwendet, um den Kandidaten zu untersuchen. Es ist immer möglich, dass der Interviewer nach einer bestimmten, engen Antwort suchte, aber ein kompetenter Interviewer sollte Folgendes suchen:

  • Fähigkeit, abstrakte Konzepte von konkreten Umsetzungen zu unterscheiden. Ich werfe diesen hauptsächlich als Metakommentar zu einigen Kommentaren ein. Nein, es macht keinen Sinn, eine einzelne Liste von Wörtern auf diese Weise zu verarbeiten. Wichtig ist jedoch das abstrakte Konzept einer Pipeline von Operationen, die mehrere Maschinen mit unterschiedlichen Fähigkeiten umfassen kann.
  • Nach meiner Erfahrung (fast 30 Jahre Erfahrung mit verteilten Anwendungen, Anwendungen mit mehreren Prozessen und Anwendungen mit mehreren Threads) ist das Verteilen der Arbeit nicht der schwierige Teil. Das Sammeln der Ergebnisse und das Koordinieren unabhängiger Prozesse sind der Ort, an dem die meisten Threading-Fehler auftreten (nach meiner Erfahrung). Indem der Interviewer das Problem auf eine einfache Kette herunterdestilliert, kann er sehen, wie gut der Kandidat über Koordination denkt. Außerdem hat der Interviewer die Möglichkeit, alle möglichen weiteren Fragen zu stellen, z. B. "OK, was ist, wenn jeder Thread sein Wort zur Wiederherstellung an einen anderen Thread senden muss."
  • Überlegt der Kandidat, wie sich das Speichermodell des Prozessors auf die Implementierung auswirken könnte? Wenn die Ergebnisse einer Operation nie aus dem L1-Cache gelöscht werden, ist dies ein Fehler, auch wenn keine offensichtliche Parallelität vorliegt.
  • Trennt der Kandidat das Threading von der Anwendungslogik?

Dieser letzte Punkt ist meiner Meinung nach der wichtigste. Aus meiner Erfahrung heraus wird es wiederum exponentiell schwieriger, Thread-Code zu debuggen, wenn das Threading mit der Anwendungslogik gemischt wird (sehen Sie sich zum Beispiel alle Swing-Fragen zu SO an). Ich glaube, dass der beste Multithread-Code als in sich geschlossener Single-Thread-Code mit klar definierten Übergaben geschrieben wird.

In diesem Sinne würde mein Ansatz darin bestehen, jedem Thread zwei Warteschlangen zuzuweisen: eine für die Eingabe und eine für die Ausgabe. Der Thread blockiert beim Lesen der Eingabewarteschlange, entfernt das erste Wort aus der Zeichenfolge und übergibt den Rest der Zeichenfolge an die Ausgabewarteschlange. Einige der Merkmale dieses Ansatzes:

  • Der Anwendungscode ist dafür verantwortlich, eine Warteschlange zu lesen, etwas mit den Daten zu tun und die Warteschlange zu schreiben. Es ist egal, ob es sich um eine Multithread-Warteschlange handelt oder ob es sich bei der Warteschlange um eine In-Memory-Warteschlange auf einem Computer oder um eine TCP-basierte Warteschlange zwischen Computern handelt, die sich auf verschiedenen Seiten der Welt befinden.
  • Da der Anwendungscode als Singlethread geschrieben ist, kann er auf deterministische Weise getestet werden, ohne dass viel Gerüst benötigt wird.
  • Während seiner Ausführungsphase besitzt der Anwendungscode die Zeichenfolge, die verarbeitet wird. Die Synchronisation mit Threads, die gleichzeitig ausgeführt werden, muss keine Rolle spielen.

Dennoch gibt es noch viele Grauzonen, die ein kompetenter Interviewer untersuchen kann:

  • "OK, aber wir sind gespannt auf Ihr Wissen über Parallelitätsprimitive. Können Sie eine Blockierungswarteschlange implementieren?" Ihre erste Antwort sollte natürlich sein, dass Sie eine vorgefertigte Blockierungswarteschlange von der Plattform Ihrer Wahl verwenden. Wenn Sie sich jedoch mit Threads auskennen, können Sie eine Warteschlangenimplementierung in weniger als einem Dutzend Codezeilen erstellen, wobei Sie die von Ihrer Plattform unterstützten Synchronisationsprimitive verwenden.
  • "Was ist, wenn ein Schritt in diesem Prozess sehr lange dauert?" Sie sollten überlegen, ob Sie eine begrenzte oder eine unbegrenzte Ausgabewarteschlange möchten, wie Sie mit Fehlern umgehen können und welche Auswirkungen Sie auf den Gesamtdurchsatz haben, wenn Sie eine Verzögerung haben.
  • So stellen Sie die Quellzeichenfolge effizient in eine Warteschlange. Dies ist nicht unbedingt ein Problem, wenn Sie mit Warteschlangen im Arbeitsspeicher zu tun haben, kann aber ein Problem sein, wenn Sie zwischen Computern wechseln. Sie können auch schreibgeschützte Wrapper über einem zugrunde liegenden unveränderlichen Bytearray untersuchen.

Wenn Sie Erfahrung in der gleichzeitigen Programmierung haben, sprechen Sie möglicherweise über einige Frameworks (z. B. Akka für Java / Scala), die bereits diesem Modell folgen.

kdgregory
quelle
Diese ganze Notiz über den L1-Cache des Prozessors hat mich wirklich fasziniert. Abgestimmt.
Marc DiMillo
Ich habe kürzlich projectReactor mit Spring 5 verwendet, mit dem ich threadunabhängigen Code schreiben kann.
Kundan Bora
16

Interviewfragen sind manchmal reine Trickfragen, die Sie über das Problem nachdenken lassen sollen, das Sie zu lösen versuchen. Das Stellen von Fragen zu einer Frage ist ein wesentlicher Bestandteil bei der Lösung eines Problems, sei es in der realen Welt oder in einem Interview. Im Internet gibt es eine Reihe von Videos, in denen erläutert wird, wie Fragen in technischen Interviews beantwortet werden können (insbesondere Google und möglicherweise Microsoft).

"Versuch einfach zu antworten und verschwinde."

Wenn Sie sich Interviews mit diesem Gedankenmuster nähern, werden Sie jedes Interview für jedes Unternehmen bombardieren, für das es sich lohnt, zu arbeiten.

Wenn Sie nicht der Meinung sind, dass Sie viel gewinnen (wenn überhaupt durch das Einfädeln), sagen Sie ihnen dies. Sagen Sie ihnen, warum Sie glauben, dass es keinen Nutzen gibt. Besprechen Sie sich mit ihnen. Technische Interviews sollen eine offene Diskussionsplattform sein. Sie können am Ende etwas zu lernen , wie man es kann nützlich sein. Versuchen Sie nicht einfach blindlings, etwas umzusetzen, das Ihnen Ihr Interviewer gesagt hat.

Demian Brecht
quelle
3
Ich habe diese Antwort herabgestimmt (obwohl sie aus unerklärlichen Gründen 4 positive Stimmen hat), weil sie die gestellte Frage nicht beantwortet.
Robert Harvey
1
@RobertHarvey: Manchmal stellen Leute die falschen Fragen . Das OP hat eine schlechte Einstellung, um technische Interviews anzugehen, und diese Antwort war ein Versuch, ihn / sie auf den richtigen Weg zu bringen.
Demian Brecht
1
@RobertHarvey Ich glaube ehrlich, das ist die richtige Antwort auf die Frage. Das Schlüsselwort hier ist "Interviewfrage", die im Titel und im Text der Frage erwähnt wird. Für eine solche Frage ist dies die richtige Antwort. Wenn die Frage nur "Ich habe m Threads und einen Absatz mit n Wörtern, und ich möchte dies und das mit ihnen tun, was ist der bessere Ansatz" wäre diese Antwort für die Frage nicht angemessen gewesen. So wie es ist, finde ich es großartig. Umschreibung: Ich habe einige Interviewfragen bombardiert, weil ich den hier gegebenen Rat nicht
befolgt habe
@RobertHarvey beantwortet eine verwandte Frage, Down-Voting hat nichts gebracht.
Marc DiMillo
0
  • Token Sie zuerst den Absatz mit den entsprechenden Trennzeichen und fügen Sie die Wörter zu einer Warteschlange hinzu.

  • Erstellen Sie N Threads und speichern Sie sie in einem Thread-Pool.

  • Durchlaufen Sie den Thread-Pool und starten Sie den Thread. Warten Sie,
    bis sich der Thread anschließt. Starten Sie den nächsten Thread, sobald der erste Thread endet und so weiter.

  • Jeder Thread sollte nur die Warteschlange abfragen und ausdrucken.

  • Wenn alle Threads im Thread-Pool verwendet wurden, beginnen Sie am Anfang des Pools.

java_mouse
quelle
0

Wie Sie sagten, glaube ich nicht, dass dieses Szenario von Threading stark profitiert, wenn überhaupt. Es ist wahrscheinlich langsamer als eine Single-Thread-Implementierung.

Meine Antwort wäre jedoch, dass jeder Thread in einer engen Schleife versucht, auf eine Sperre zuzugreifen, die den Zugriff auf den Wort-Array-Index steuert. Jeder Thread greift nach der Sperre, ruft den Index ab, ruft das entsprechende Wort aus dem Array ab, druckt es aus, erhöht den Index und gibt die Sperre frei. Der Thread wird beendet, wenn sich der Index am Ende des Arrays befindet.

Etwas wie das:

while(true)
{
    lock(index)
    {
        if(index >= array.length())
          break;
        Console.WriteLine(array[index]);
        index++;
    }
}

Ich denke, dies sollte den einen Thread nach dem anderen erfordern, aber die Reihenfolge der Threads ist nicht garantiert. Ich bin gespannt auf weitere Lösungen.

ConditionRacer
quelle
-1

Verwenden Sie Condition Wait / Signal-APIs, um dieses Problem zu beheben.

Nehmen wir an, der erste Thread wählt 1 Wort und in der Zwischenzeit warten alle Threads auf ein Signal. Der erste Thread druckt das erste Wort und generiert ein Signal für den nächsten Thread und der zweite Thread druckt das zweite Wort und generiert ein Signal für den dritten Thread und so weiter.

#include <iostream>
#include <fstream>
#include <pthread.h>
#include <signal.h>
pthread_cond_t cond[5] = {PTHREAD_COND_INITIALIZER,};
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

using namespace std;

string gstr;

void* thread1(void*)
{
    do {
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond[0],&mutex);
    cout <<"thread1 :"<<gstr<<endl;
    pthread_mutex_unlock(&mutex);
    }while(1);
}

void* thread2(void*)
{
    do{
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond[1],&mutex);
    cout <<"thread2 :"<<gstr<<endl;
    pthread_mutex_unlock(&mutex);
    }while(1);
}

void* thread3(void*)
{
    do{
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond[2],&mutex);
    cout <<"thread3 :"<<gstr<<endl;
    pthread_mutex_unlock(&mutex);
    }while(1);
}

void* thread4(void*)
{
    do{
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond[3],&mutex);
    cout <<"thread4 :"<<gstr<<endl;
    pthread_mutex_unlock(&mutex);
    }while(1);
}

void* thread5(void*)
{
    do{
    pthread_mutex_lock(&mutex);
    pthread_cond_wait(&cond[4],&mutex);
    cout <<"thread5 :"<<gstr<<endl;
    pthread_mutex_unlock(&mutex);
    }while(1);
}

int main()
{
    pthread_t t[5];
    void* (*fun[5])(void*);
    fun[0]=thread1;
    fun[1]=thread2;
    fun[2]=thread3;
    fun[3]=thread4;
    fun[4]=thread5;

    for (int i =0 ; i < 5; ++i)
    {
        pthread_create(&t[i],NULL,fun[i],NULL);
    }
    ifstream in;
    in.open("paragraph.txt");
    int i=0;
    while(in >> gstr)
    {

        pthread_cond_signal(&cond[i++]);
        if(i == 5)
            i=0;
        usleep(10);
    }
    for (int i =0 ; i < 5; ++i)
    {
        int ret = pthread_cancel(t[i]);
        if(ret != 0)
            perror("pthread_cancel:");
        else
            cout <<"canceled\n";
    }
    pthread_exit(NULL);
}
shashank
quelle
-1

[Die hier verwendeten Begriffe beziehen sich möglicherweise auf POSIX-Threads.]

Es sollte auch möglich sein, einen FIFO-Mutex zu verwenden, um dieses Problem zu lösen.

Wo zu verwenden:

Angenommen, zwei Threads T1 und T2 versuchen, einen kritischen Abschnitt auszuführen. Beide haben außerhalb dieses kritischen Abschnitts nicht viel zu tun und halten die Sperren für eine Weile. Somit kann T1 sperren, ausführen und entsperren und T2 zum Aufwecken signalisieren. Bevor T2 jedoch aufgeweckt und die Sperre aktiviert werden konnte, ruft T1 die Sperre erneut ab und führt sie aus. Auf diese Weise muss T2 möglicherweise sehr lange warten, bis es tatsächlich die Sperre erhält, oder auch nicht.

Wie es funktioniert / Wie implementiert man:

Habe einen Mutex zum Aufstecken. Initialisieren Sie Thread-spezifische Daten (TSD) für jeden Thread für einen Knoten, der die Thread-ID und das Semaphor enthält. Haben Sie auch zwei Variablen - besessen (WAHR oder FALSCH oder -1), Eigentümer (Eigentümer-Thread-ID). Stellen Sie außerdem eine Warteschlange für Kellner und einen Zeiger waiterLast bereit, der auf den letzten Knoten in der Warteschlange für Kellner verweist.

Sperrbetrieb:

node = get_thread_specific_data(node_key);
lock(mutex);
    if(!owned)
    {
        owned = true;
        owner = self;
        return success;
    }

    node->next = nullptr;
    if(waiters_queue == null) waiters_queue = node;
    else waiters_last->next = node;

    waiters_last = node;
unlock(mutex);
sem_wait(node->semaphore);

lock(mutex);
    if(owned != -1) abort();
    owned = true;
    owner = self;
    waiters_queue = waiters_queue->next;
 unlock(mutex);

entsperren betrieb:

lock(mutex);
    owner = null;
    if(waiters_queue == null)
    {
        owned = false;
        return success;
    }
    owned = -1;
    sem_post(waiters_queue->semaphore);
unlock(mutex);
user2615724
quelle
-1

Interessante Frage. Hier ist meine Lösung in Java, die SynchronousQueue verwendet, um einen Rendezvous-Kanal zwischen Threads zu erstellen:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.concurrent.SynchronousQueue;

public class FindNWordsGivenMThreads {

    private static final int NUMBER_OF_WORDS = 100;
    private static final int NUMBER_OF_THREADS = 5;
    private static final Stack<String> POISON_PILL = new Stack<String>();

    public static void main(String[] args) throws Exception {
        new FindNWordsGivenMThreads().run();
    }

    private void run() throws Exception {
        final Stack<String> words = loadWords();
        SynchronousQueue<Stack<String>> init = new SynchronousQueue<Stack<String>>();
        createProcessors(init);
        init.put(words);
    }

    private void createProcessors(SynchronousQueue<Stack<String>> init) {
        List<Processor> processors = new ArrayList<Processor>();

        for (int i = 0; i < NUMBER_OF_THREADS; i++) {

            SynchronousQueue in;
            SynchronousQueue out;

            if (i == 0) {
                in = init;
            } else {
                in = processors.get(i - 1).getOut();
            }

            if (i == (NUMBER_OF_THREADS - 1)) {
                out = init;
            } else {
                out = new SynchronousQueue();
            }

            Processor processor = new Processor("Thread-" + i, in, out);
            processors.add(processor);
            processor.start();

        }

    }

    class Processor extends Thread {

        private SynchronousQueue<Stack<String>> in;
        private SynchronousQueue<Stack<String>> out;

        Processor(String name, SynchronousQueue in, SynchronousQueue out) {
            super(name);
            this.in = in;
            this.out = out;
        }

        @Override
        public void run() {

            while (true) {

                Stack<String> stack = null;
                try {
                    stack = in.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if (stack.empty() || stack == POISON_PILL) {
                    System.out.println(Thread.currentThread().getName() + " Done!");
                    out.offer(POISON_PILL);
                    break;
                }

                System.out.println(Thread.currentThread().getName() + " " + stack.pop());
                out.offer(stack);
            }
        }

        public SynchronousQueue getOut() {
            return out;
        }
    }

    private Stack<String> loadWords() throws Exception {

        Stack<String> words = new Stack<String>();

        BufferedReader reader = new BufferedReader(new FileReader(new File("/usr/share/dict/words")));
        String line;
        while ((line = reader.readLine()) != null) {
            words.push(line);
            if (words.size() == NUMBER_OF_WORDS) {
                break;
            }
        }
        return words;
    }
}
Sid
quelle
-2

Ich würde sagen, dass diese Art von Frage sehr schwer zu beantworten ist, weil sie nach dem besten Weg fragt, etwas völlig Dummes zu tun. Mein Gehirn funktioniert einfach nicht so. Es kann keine Lösung für dumme Fragen finden. Mein Gehirn würde sofort sagen, dass unter diesen Umständen die Verwendung mehrerer Threads sinnlos ist, also würde ich einen einzelnen Thread verwenden.

Ich würde sie dann bitten, entweder eine reale Frage zum Thema Threading zu stellen oder mir ein reales Beispiel für ernsthaftes Threading zu geben.

gnasher729
quelle