Pregunta de entrevista de sincronización de subprocesos múltiples: Encuentra n palabras dadas m hilos

23

¿Hay alguna manera de que este problema pueda beneficiarse de una solución con múltiples hilos, en lugar de un solo hilo?


En una entrevista, me pidieron que resolviera un problema usando múltiples hilos. Me parece que los múltiples hilos no brindan ningún beneficio.

Aquí está el problema:

Te dan un párrafo, que contiene n número de palabras, te dan m hilos. Lo que debe hacer es, cada hilo debe imprimir una palabra y dar el control al siguiente hilo, de esta manera cada hilo seguirá imprimiendo una palabra, en caso de que llegue el último hilo, debe invocar el primer hilo. La impresión se repetirá hasta que todas las palabras se impriman en un párrafo. Finalmente todos los hilos deben salir con gracia. ¿Qué tipo de sincronización usará?

Creo firmemente que no podemos aprovechar los hilos aquí, pero creo que el entrevistador está tratando de medir mis habilidades de sincronización. ¿Me estoy perdiendo algo en este problema que haría que varios hilos tengan valor?

No necesita código, solo piense. Lo implementaré por mí mismo.

rplusg
fuente
Agregar una etiqueta C ++ probablemente no ayudará mucho aquí. Estas preguntas por aquí son cosas más conceptuales que trascienden cualquier lenguaje en particular.
cHao
Confía en tus sentimientos. Entiendo a qué se dirigen, pero nunca me han gustado las preguntas de la entrevista que se desvían tanto de cómo debe resolver el problema en el mundo real.
G_P
16
@rplusg: Me sorprendería mucho más un entrevistado que señaló que la solución serializa el problema y simplemente agrega una sobrecarga de hilo sin hacer ningún proceso concurrente. El entrevistador siempre puede insistir en que responda la pregunta tal como se le preguntó.
David Harkness
si "cada hilo debe imprimir una palabra y dar el control al siguiente hilo", eso suena como un trabajo en serie, es decir, un hilo está esperando que el anterior termine y es como pasar un relevo. ¿por qué no simplemente convertirlo en una aplicación de subproceso único en ese caso?
anfibio
1
Lo entiendo @Blrfl. es como si necesitara verificar que sabes cómo usar la herramienta X, pero era demasiado vago o descuidado para diseñar un escenario de caso de uso de aplicación auténtico que realmente garantizara el uso de esa herramienta, así que simplemente tomé lo que estaba primero a la mano y encasiqué mi ejemplo en ella descuidadamente. francamente, si me preguntaran eso en una entrevista, lo llamaría y probablemente no querría trabajar con alguien descuidado y mediocre como ese
anfibio

Respuestas:

22

Me parece que te están guiando hacia una solución de semáforo. Los semáforos se utilizan para indicar a otro hilo que es su turno. Se usan con mucha menos frecuencia que los mutexes, por lo que creo que es por eso que piensan que es una buena pregunta para la entrevista. También es por qué el ejemplo parece artificial.

Básicamente, mcrearías semáforos. Cada hilo xespera en el semáforo y xluego se publica en el semáforo x+1después de hacer lo suyo. En pseudocódigo:

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
fuente
Gracias por la generosidad. Me tomó un tiempo darme cuenta de que pasar el rato sobre eso diría quién lo dio.
kdgregory
Disculpe mi ignorancia, ¿puede dar más detalles sobre cómo esta solución es correcta? ¿Es este un nuevo tipo elegante de semáforos? Sin embargo, estoy seguro de que la pregunta se resuelve con una solución de espera / notificación [que utilizan los semáforos].
AJed
Es solo una serie de semáforos estándar. Nada especial sobre ellos. Notify se llama "post" en algunas implementaciones.
Karl Bielefeldt
@KarlBielefeldt Bueno, si cada hilo x esperará el semáforo x, entonces todos los hilos se bloquearán y no pasará nada. Si wait (sem) es realmente adquirir (sem), entonces entrarán al mismo tiempo y no hay exclusión. Hasta que haya más aclaraciones, creo que hay algo mal en este pseudocódigo y no debería ser la mejor respuesta.
AJed
Esto solo muestra el bucle de cada hilo. El código de configuración debería publicarse en el primer semáforo para comenzar.
Karl Bielefeldt
23

En mi opinión, esta es una pregunta fabulosa para la entrevista: al menos suponiendo que (1) se espera que el candidato tenga un conocimiento profundo de los hilos, y (2) el entrevistador también tiene un conocimiento profundo y está usando la pregunta para sondear al candidato. Siempre es posible que el entrevistador buscara una respuesta específica y estrecha, pero un entrevistador competente debería buscar lo siguiente:

  • Capacidad para diferenciar conceptos abstractos de una implementación concreta. Lanzo este principalmente como un meta-comentario en algunos de los comentarios. No, no tiene sentido procesar una sola lista de palabras de esta manera. Sin embargo, el concepto abstracto de una tubería de operaciones, que puede abarcar varias máquinas de diferentes capacidades, es importante.
  • En mi experiencia (casi 30 años de aplicaciones distribuidas, multiproceso y multiproceso), distribuir el trabajo no es la parte difícil. Recopilar los resultados y coordinar procesos independientes es donde ocurren la mayoría de los errores de subprocesamiento (de nuevo, en mi experiencia). Al resumir el problema en una cadena simple, el entrevistador puede ver qué tan bien piensa el candidato acerca de la coordinación. Además, el entrevistador tiene la oportunidad de hacer todo tipo de preguntas de seguimiento, como "OK, qué pasa si cada hilo tiene que enviar su palabra a otro hilo para la reconstrucción".
  • ¿Piensa el candidato en cómo el modelo de memoria del procesador podría afectar la implementación? Si los resultados de una operación nunca se eliminan del caché L1, eso es un error, incluso si no hay concurrencia aparente.
  • ¿El candidato separa el subproceso de la lógica de la aplicación?

Este último punto es, en mi opinión, el más importante. Nuevamente, según mi experiencia, se vuelve exponencialmente más difícil depurar el código enhebrado si el subproceso se mezcla con la lógica de la aplicación (solo mire todas las preguntas de Swing en SO para ver ejemplos). Creo que el mejor código de subprocesos múltiples está escrito como un código de subprocesos único autónomo, con transferencias claramente definidas.

Con esto en mente, mi enfoque sería dar a cada hilo dos colas: una para entrada y otra para salida. El hilo se bloquea mientras lee la cola de entrada, quita la primera palabra de la cadena y pasa el resto de la cadena a su cola de salida. Algunas de las características de este enfoque:

  • El código de la aplicación es responsable de leer una cola, hacer algo a los datos y escribir la cola. No le importa si es multiproceso o no, o si la cola es una cola en memoria en una máquina o una cola basada en TCP entre máquinas que viven en lados opuestos del mundo.
  • Debido a que el código de la aplicación está escrito como si fuera un solo subproceso, es comprobable de manera determinista sin la necesidad de muchos andamios.
  • Durante su fase de ejecución, el código de la aplicación posee la cadena que se está procesando. No tiene que preocuparse por la sincronización con hilos que se ejecutan simultáneamente.

Dicho esto, todavía hay muchas áreas grises que un entrevistador competente puede investigar:

  • "Está bien, pero estamos buscando ver su conocimiento de las primitivas de concurrencia; ¿puede implementar una cola de bloqueo?" Su primera respuesta, por supuesto, debería ser que usaría una cola de bloqueo preconstruida desde su plataforma de elección. Sin embargo, si comprende los hilos, puede crear una implementación de cola en menos de una docena de líneas de código, utilizando las primitivas de sincronización que su plataforma admita.
  • "¿Qué pasa si un paso en el proceso lleva mucho tiempo?" Debe pensar si desea una cola de salida limitada o ilimitada, cómo podría manejar los errores y los efectos en el rendimiento general si tiene un retraso.
  • Cómo poner en cola eficientemente la cadena de origen. No necesariamente es un problema si se trata de colas en memoria, pero podría ser un problema si se mueve entre máquinas. También puede explorar envoltorios de solo lectura sobre una matriz de bytes inmutable subyacente.

Finalmente, si tiene experiencia en programación concurrente, podría hablar sobre algunos marcos (por ejemplo, Akka para Java / Scala) que ya siguen este modelo.

kdgregory
fuente
Toda esa nota sobre el caché L1 del procesador realmente me intrigó. Votado
Marc DiMillo
Recientemente usé projectReactor con Spring 5. Lo que me permite escribir código agnóstico de hilo.
Kundan Bora
16

Las preguntas de la entrevista a veces son preguntas engañosas, destinadas a hacerte pensar sobre el problema que estás tratando de resolver. Hacer preguntas sobre una pregunta es una parte integral de abordar cualquier problema, ya sea en el mundo real o en una entrevista. Hay una serie de videos que circulan por Internet sobre cómo abordar las preguntas en entrevistas técnicas (busque particularmente Google y quizás Microsoft).

"Solo trata de responder y sal de ahí ..."

Acercarse a entrevistas con este patrón de pensamiento lo llevará a bombardear cualquier entrevista para cualquier empresa por la que valga la pena trabajar.

Si no crees que ganas mucho (si es que hay algo de subprocesos), diles eso. Dígales por qué no cree que haya ningún beneficio. Ten una discusión con ellos. Las entrevistas técnicas están destinadas a ser una plataforma de discusión abierta. Puede terminar aprendiendo algo sobre cómo puede ser útil. No siga adelante a ciegas tratando de implementar algo que su entrevistador le dijo que hiciera.

Demian Brecht
fuente
3
Voté esta respuesta (aunque inexplicablemente obtuvo 4 votos a favor), porque no responde la pregunta que se hizo.
Robert Harvey
1
@RobertHarvey: A veces las personas hacen las preguntas equivocadas . El OP tiene una mentalidad deficiente para abordar entrevistas técnicas y esta respuesta fue un intento de ayudar a ponerlo en el camino correcto.
Demian Brecht
1
@RobertHarvey Sinceramente, creo que esta es la respuesta correcta para la pregunta. La palabra clave aquí es "pregunta de entrevista" que se menciona en el título y en el cuerpo de la pregunta. Para tal pregunta, esta es la respuesta correcta. Si la pregunta era solo "Tengo m hilos y un párrafo de n palabras, y quiero hacer esto y aquello con ellos, cuál es el mejor enfoque", entonces sí, esta respuesta no habría sido apropiada para la pregunta. Tal como está creo que es genial. Parafraseando: He bombardeado algunas preguntas de la entrevista porque no seguí los consejos dados aquí
Shivan Dragon el
@RobertHarvey responde una pregunta relacionada, el voto negativo no logró nada.
Marc DiMillo
0
  • Primero tokenice el párrafo con delimitadores apropiados y agregue las palabras a una Cola.

  • Cree N número de subprocesos y manténgalo en un grupo de subprocesos.

  • Itere sobre el grupo de subprocesos e inicie el subproceso y espere a que el
    subproceso se una. Y comience el siguiente hilo una vez que termine el primer hilo y así sucesivamente.

  • Cada subproceso solo debe sondear la cola e imprimirla.

  • Una vez que todos los hilos se usan dentro del grupo de hilos, comience desde el principio del grupo.

java_mouse
fuente
0

Como dijiste, no creo que este escenario se beneficie mucho, si es que lo hace, del enhebrado. Es más probable que sea más lento que una implementación de subproceso único.

Sin embargo, mi respuesta sería tener cada hilo en un bucle cerrado intentando acceder a un bloqueo que controla el acceso al índice de matriz de palabras. Cada hilo agarra el bloqueo, obtiene el índice, obtiene la palabra correspondiente de la matriz, la imprime, incrementa el índice y luego libera el bloqueo. Los subprocesos salen cuando el índice está al final de la matriz.

Algo como esto:

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

Creo que esto debería alcanzar el requisito de un subproceso tras otro, pero el orden de los subprocesos no está garantizado. Tengo curiosidad por escuchar otras soluciones también.

CondiciónRacer
fuente
-1

Utilice las API de condición de espera / señal para resolver este problema.

Digamos que el primer hilo elige 1 palabra y mientras tanto, todos los hilos están esperando una señal. El primer hilo imprime la primera palabra y genera la señal al siguiente hilo y luego el segundo hilo imprime la segunda palabra y genera la señal al tercer hilo y así sucesivamente.

#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
fuente
-1

[Los términos utilizados aquí pueden ser específicos para hilos POSIX]

También debería ser posible utilizar un mutex FIFO para resolver este problema.

Dónde utilizar :

Suponga que dos hilos T1 y T2 están intentando ejecutar una sección crítica. Ambos no tienen mucho que hacer fuera de esta sección crítica y mantienen bloqueos por una buena cantidad de tiempo. Entonces, T1 puede bloquear, ejecutar y desbloquear y señalar a T2 para la activación. Pero antes de que T2 pueda despertarse y adquirir el bloqueo, T1 vuelve a adquirir el bloqueo y la ejecución. De esta manera, T2 puede tener que esperar mucho tiempo antes de que realmente se bloquee o no.

Cómo funciona / Cómo implementar:

Tener un mutex para bloquear. Inicialice datos específicos de subproceso (TSD) para cada subproceso en un nodo que contenga ID de subproceso y semáforo. Además, tenga dos variables: propiedad (VERDADERO o FALSO o -1), propietario (identificación del hilo del propietario). Además, mantenga una cola de camareros y un puntero de camarero Último apuntando al último nodo en la cola de camareros.

operación de bloqueo:

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

Operación de desbloqueo:

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

Interesante pregunta. Aquí está mi solución en Java usando SynchronousQueue para crear un canal de encuentro entre hilos:

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
fuente
-2

Diría que este tipo de preguntas es muy difícil de responder, porque pide la mejor manera de hacer algo completamente estúpido. Mi cerebro simplemente no funciona de esa manera. No puede encontrar soluciones para preguntas estúpidas. Mi cerebro inmediatamente diría que, en estas condiciones, usar múltiples hilos no tiene sentido, así que usaría un solo hilo.

Luego les pediría que hicieran una pregunta del mundo real sobre el enhebrado, o que me dejaran dar un ejemplo del mundo real de algún enhebrado serio.

gnasher729
fuente