Domanda di intervista alla sincronizzazione multithreading: trova n parole con m thread


23

Esiste un modo in cui questo problema potrebbe beneficiare di una soluzione con più thread anziché un singolo thread?


In un'intervista, mi è stato chiesto di risolvere un problema utilizzando più thread. Mi sembra che i thread multipli non offrano alcun vantaggio.

Ecco il problema:

Ti viene dato un paragrafo, che contiene n numero di parole, ti vengono dati m thread. Quello che devi fare è che ogni thread dovrebbe stampare una parola e dare il controllo al thread successivo, in questo modo ogni thread continuerà a stampare una parola, nel caso in cui arrivi l'ultimo thread, dovrebbe invocare il primo thread. La stampa si ripeterà fino a quando tutte le parole non saranno stampate nel paragrafo. Alla fine tutti i thread dovrebbero uscire con garbo. Che tipo di sincronizzazione utilizzerà?

Sento fortemente che qui non possiamo trarre alcun vantaggio dai thread, ma credo che l'intervistatore stia cercando di misurare le mie capacità di sincronizzazione. Mi sto perdendo qualcosa in questo problema che farebbe valere più thread?

Non c'è bisogno di codice, basta mettere alcuni pensieri. Lo implementerò da solo.


L'aggiunta di un tag C ++ probabilmente non sarà di grande aiuto qui. Queste domande qui sono cose più concettuali che trascendono qualsiasi linguaggio particolare.
cHao,

Fidati dei tuoi sentimenti. Capisco cosa stanno cercando, ma non mi sono mai piaciute le domande di intervista che si discostano così tanto da come dovresti risolvere il problema nel mondo reale.
G_P,

16
@rplusg - Sarei molto più colpito da un intervistato che ha sottolineato che la soluzione serializza il problema e aggiunge semplicemente l'overhead del thread senza eseguire alcuna elaborazione simultanea. L'intervistatore può sempre insistere sul fatto che rispondi alla domanda come richiesto.
David Harkness,

se "ogni thread deve stampare una parola e dare il controllo al thread successivo", sembra un lavoro seriale, ovvero un thread è in attesa del termine del precedente ed è come passare un relè. perché non trasformarla in un'app a thread singolo in quel caso?
anfibio

1
ho capito @Blrfl. è un po 'come se avessi bisogno di verificare che sai come usare lo strumento X, ma ero troppo pigro o sciatto per progettare uno scenario di utilizzo autentico dell'applicazione che giustifica sinceramente l'uso di quello strumento, quindi ho appena preso tutto ciò che era a portata di mano e ho analizzato il mio esempio in esso sciatto. francamente, se mi fosse stato chiesto in un'intervista, l'avrei chiamato fuori e probabilmente non avrei voluto lavorare con qualcuno così sciatto e di mezza età
anfibio

Risposte:


22

Mi sembra che ti stiano conducendo verso una soluzione di semaforo. I semafori vengono utilizzati per segnalare un altro thread che è il loro turno. Sono usati molto meno frequentemente dei mutex, il che immagino sia il motivo per cui pensano che sia una buona domanda per l'intervista. È anche il motivo per cui l'esempio sembra inventato.

Fondamentalmente, dovresti creare msemafori. Ogni thread xattende su semaforo, xquindi pubblica post su semaforo x+1dopo aver fatto le sue cose. In pseudocodice:

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

Grazie per la generosità. Mi ci è voluto un po 'per capire che il mouse su di esso direbbe chi lo ha dato.
kdgregory,

Scusa la mia ignoranza, puoi approfondire come questa soluzione è corretta? è questo un nuovo tipo di semafori? Sono sicuro, tuttavia, che la domanda è risolta da una soluzione di attesa / notifica [utilizzata dai semafori].
AJed

È solo una serie di semafori standard. Niente di speciale su di loro. Notifica è chiamato "post" in alcune implementazioni.
Karl Bielefeldt,

@KarlBielefeldt Bene, se ogni thread x attenderà il semaforo x, allora tutti i thread verranno bloccati e non accadrà nulla. Se wait (sem) viene effettivamente acquisito (sem), allora entreranno nello stesso momento e non c'è esclusione. Fino a quando non ci saranno ulteriori chiarimenti, credo che ci sia qualcosa di sbagliato in questo pseudocodice e non dovrebbe essere la risposta migliore.
AJed

Questo sta solo mostrando il loop per ogni thread. Il codice di installazione dovrebbe essere inviato al primo semaforo per dare il via alle cose.
Karl Bielefeldt,

23

Secondo me, questa è una domanda da intervista favolosa - almeno supponendo che (1) il candidato debba avere una profonda conoscenza del threading e (2) anche l'intervistatore abbia una profonda conoscenza e sta usando la domanda per sondare il candidato. È sempre possibile che l'intervistatore stesse cercando una risposta specifica e ristretta, ma un intervistatore competente dovrebbe cercare quanto segue:

  • Capacità di differenziare i concetti astratti dall'attuazione concreta. Lo inserisco principalmente come meta-commento su alcuni dei commenti. No, non ha senso elaborare un unico elenco di parole in questo modo. Tuttavia, il concetto astratto di una pipeline di operazioni, che può comprendere più macchine con capacità diverse, è importante.
  • Nella mia esperienza (quasi 30 anni di applicazioni distribuite, multi-processo e multi-thread), distribuire il lavoro non è la parte difficile. Raccogliere i risultati e coordinare i processi indipendenti è dove si verificano la maggior parte dei bug di threading (di nuovo, nella mia esperienza). Distillando il problema in una semplice catena, l'intervistatore può vedere quanto bene il candidato pensa al coordinamento. Inoltre, l'intervistatore ha l'opportunità di porre ogni sorta di domande successive, come "OK, cosa succede se ogni thread deve inviare la sua parola a un altro thread per la ricostruzione".
  • Il candidato pensa a come il modello di memoria del processore potrebbe influenzare l'implementazione? Se i risultati di un'operazione non vengono mai cancellati dalla cache L1, questo è un bug anche se non c'è concorrenza apparente.
  • Il candidato separa il threading dalla logica dell'applicazione?

Quest'ultimo punto è, a mio avviso, il più importante. Ancora una volta, in base alla mia esperienza, diventa esponenzialmente più difficile eseguire il debug del codice threaded se il threading è mescolato con la logica dell'applicazione (basta guardare tutte le domande di Swing su SO per esempi). Credo che il miglior codice multi-thread sia scritto come codice single-thread autonomo, con handoff chiaramente definiti.

Con questo in mente, il mio approccio sarebbe quello di dare a ogni thread due code: una per l'input, una per l'output. Il thread si blocca durante la lettura della coda di input, toglie la prima parola dalla stringa e passa il resto della stringa alla sua coda di output. Alcune delle caratteristiche di questo approccio:

  • Il codice dell'applicazione è responsabile della lettura di una coda, dell'elaborazione dei dati e della scrittura della coda. Non importa se è multi-thread o no, o se la coda è una coda in memoria su una macchina o una coda basata su TCP tra macchine che vivono su lati opposti del mondo.
  • Poiché il codice dell'applicazione è scritto come se fosse a thread singolo, è testabile in modo deterministico senza la necessità di un sacco di ponteggi.
  • Durante la sua fase di esecuzione, il codice dell'applicazione possiede la stringa in elaborazione. Non deve preoccuparsi della sincronizzazione con thread in esecuzione simultanea.

Detto questo, ci sono ancora molte aree grigie che un intervistatore competente può sondare:

  • "OK, ma stiamo cercando di vedere la tua conoscenza delle primitive di concorrenza; puoi implementare una coda di blocco?" La tua prima risposta, ovviamente, dovrebbe essere che useresti una coda di blocco pre-costruita dalla tua piattaforma preferita. Tuttavia, se capisci i thread, puoi creare un'implementazione della coda in una dozzina di righe di codice, usando qualunque primitiva di sincronizzazione supportata dalla tua piattaforma.
  • "Cosa succede se un passaggio nel processo richiede molto tempo?" È necessario valutare se si desidera una coda di output limitata o illimitata, come gestire gli errori e gli effetti sul throughput complessivo in caso di ritardo.
  • Come accodare in modo efficiente la stringa di origine. Non è necessariamente un problema se hai a che fare con le code in memoria, ma potrebbe esserlo se ti sposti da una macchina all'altra. È inoltre possibile esplorare i wrapper di sola lettura in cima a un array di byte immutabile sottostante.

Infine, se hai esperienza nella programmazione concorrente, potresti parlare di alcuni framework (ad esempio Akka per Java / Scala) che seguono già questo modello.


L'intera nota sulla cache L1 del processore mi ha davvero incuriosito. Votato.
Marc DiMillo,

Recentemente ho usato projectReactor con Spring 5. Il che mi permette di scrivere codice agnostico di thread.
Kundan Bora,

16

Le domande di intervista a volte sono in realtà domande truccate, intese a farti pensare al problema che stai cercando di risolvere. Fare domande su una domanda è parte integrante dell'approccio a qualsiasi problema, sia nel mondo reale che in un'intervista. Esistono numerosi video che circolano su Internet su come affrontare le domande nelle interviste tecniche (in particolare Google e forse Microsoft).

"Cerca solo di rispondere e vattene da lì ..."

Avvicinarsi alle interviste con questo modello di pensiero ti porterà a bombardare qualsiasi intervista per qualsiasi azienda per cui valga la pena lavorare.

Se non pensi di guadagnare molto (se non altro dal threading), diglielo. Di 'loro perché non pensi che ci sia alcun vantaggio. Discuti con loro. Le interviste tecniche sono pensate per essere una piattaforma di discussione aperta. Potresti finire per imparare qualcosa su come può essere utile. Non limitarti a cercare ciecamente di implementare qualcosa che il tuo intervistatore ti ha detto.


3
Ho votato in negativo questa risposta (anche se inspiegabilmente ha ottenuto 4 voti positivi), perché non risponde alla domanda che è stata posta.
Robert Harvey,

1
@RobertHarvey: a volte le persone fanno domande sbagliate . L'OP ha una mentalità scadente per affrontare le interviste tecniche e questa risposta è stata un tentativo di aiutarlo a metterlo sulla strada giusta.
Demian Brecht,

1
@RobertHarvey Credo sinceramente che questa sia la risposta giusta alla domanda. La parola chiave qui è "domanda di intervista" che è menzionata nel titolo e nel corpo della domanda. Per una domanda del genere, questa è la risposta giusta. Se la domanda fosse solo "Ho m thread e un paragrafo di n parole e voglio fare questo e quello con loro, qual è l'approccio migliore", quindi sì, questa risposta non sarebbe stata appropriata per la domanda. Così com'è, penso sia grandioso. Parafrasando: ho bombardato parecchie domande di intervista perché non ho seguito il consiglio dato qui
Shivan Dragon

@RobertHarvey risponde a una domanda correlata, il voto negativo non ha realizzato nulla.
Marc DiMillo,

0
  • Per prima cosa tokenizzare il paragrafo con delimitatori appropriati e aggiungere le parole a una coda.

  • Crea N numero di thread e conservalo in un pool di thread.

  • Scorrere sul pool di thread e avviare il thread e attendere che il
    thread si unisca. E inizia il thread successivo al termine del primo thread e così via.

  • Ogni thread dovrebbe semplicemente eseguire il polling della coda e stamparla.

  • Una volta utilizzati tutti i thread all'interno del pool di thread, iniziare dall'inizio del pool.


0

Come hai detto, non penso che questo scenario tragga grandi benefici, se non del tutto dal threading. Molto probabilmente è più lento di una singola implementazione thread.

Tuttavia, la mia risposta sarebbe di avere ogni thread in un ciclo stretto che tenta di accedere a un blocco che controlla l'accesso all'indice di array di parole. Ogni thread prende il blocco, ottiene l'indice, ottiene la parola corrispondente dall'array, la stampa, incrementa l'indice e quindi rilascia il blocco. I thread si chiudono quando l'indice si trova alla fine dell'array.

Qualcosa come questo:

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

Penso che questo dovrebbe raggiungere un thread dopo un altro requisito, ma l'ordinamento dei thread non è garantito. Sono curioso di ascoltare anche altre soluzioni.


-1

Utilizzare le API di attesa / segnale delle condizioni per risolvere questo problema.

Supponiamo che il primo thread scelga 1 parola e che nel frattempo tutti i thread siano in attesa di un segnale. Il 1 ° thread stampa la 1 ° parola e genera il segnale al thread successivo, quindi il 2 ° thread stampa la seconda parola e genera il segnale al 3 ° thread e così via.

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

-1

[I termini usati qui potrebbero essere specifici dei thread POSIX]

Dovrebbe essere possibile utilizzare anche un mutex FIFO per risolvere questo problema.

Dove usare:

Supponiamo che due thread T1 e T2 stiano tentando di eseguire una sezione critica. Entrambi non hanno molto da fare al di fuori di questa sezione critica e tengono i blocchi per un bel po 'di tempo. Pertanto, T1 può bloccare, eseguire e sbloccare e segnalare T2 per il risveglio. Ma prima che T2 potesse riattivarsi e acquisire il blocco, T1 riacquista il blocco e l'esecuzione. In questo modo, T2 potrebbe dover aspettare molto tempo prima di ottenere effettivamente il blocco o potrebbe non esserlo.

Come funziona / Come implementare:

Avere un mutex da bloccare. Inizializza i dati specifici del thread (TSD) per ciascun thread su un nodo contenente ID thread e semaforo. Inoltre, hanno due variabili di proprietà (TRUE o FALSE o -1), proprietario (ID thread proprietario). Inoltre, mantieni una coda di camerieri e un puntatore cameriere Ultimo che punta all'ultimo nodo nella coda di camerieri.

operazione di blocco:

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

operazione di sblocco:

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

-1

Domanda interessante. Ecco la mia soluzione in Java che usa SynchronousQueue per creare un canale rendezvous tra i thread:

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

-2

Direi che a questo tipo di domanda è molto difficile rispondere, perché chiede il modo migliore per fare qualcosa di completamente stupido. Il mio cervello non funziona così. Non riesce a trovare soluzioni per domande stupide. Il mio cervello direbbe immediatamente che in queste condizioni l'uso di più thread non ha senso, quindi utilizzerei un singolo thread.

Quindi chiederei loro di fare una domanda sul mondo reale sul threading o di farmi fare un esempio del mondo reale di un threading serio.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.