Pergunta da entrevista sobre sincronização multithreading: encontre n palavras com m threads

23

Existe uma maneira de esse problema se beneficiar de uma solução com vários threads, em vez de um único thread?


Em uma entrevista, fui solicitado a resolver um problema usando vários threads. Parece-me que os vários threads não trazem benefícios.

Aqui está o problema:

Você recebe um parágrafo, que contém n número de palavras, e m tópicos. O que você precisa fazer é que cada thread imprima uma palavra e dê o controle para o próximo thread, dessa forma cada thread continuará imprimindo uma palavra, caso o último thread chegue, ele deve chamar o primeiro thread. A impressão será repetida até que todas as palavras sejam impressas no parágrafo. Finalmente, todos os threads devem sair normalmente. Que tipo de sincronização usará?

Sinto fortemente que não podemos tirar proveito dos threads aqui, mas acredito que o entrevistador está tentando medir minhas habilidades de sincronização. Estou faltando algo neste problema que faria com que vários threads tivessem valor?

Não há necessidade de código, basta colocar alguns pensamentos. Vou implementar sozinho.

rplusg
fonte
Adicionar uma tag C ++ provavelmente não ajudará muito aqui. Essas perguntas por aqui são mais conceituais que transcendem qualquer linguagem específica.
cHao 10/10/12
Confie em seus sentimentos. Entendo o que eles estão buscando, mas nunca gostei de perguntas para entrevistas que se afastam até agora de como você deve resolver o problema no mundo real.
G_P
16
@rplusg - Eu ficaria muito mais impressionado com um entrevistado que apontou que a solução serializa o problema e apenas adiciona sobrecarga de thread sem realmente executar qualquer processamento simultâneo. O entrevistador sempre pode insistir para que você responda à pergunta, conforme solicitado.
precisa
se "cada thread imprime uma palavra e dá o controle para o próximo thread", isso soa como trabalho em série, ou seja, um thread está aguardando o término do anterior e é como passar um relé. por que não torná-lo um aplicativo de thread único nesse caso?
anfíbio
1
Eu entendo @Blrfl. é como se eu precisasse verificar se você sabe usar a ferramenta X, mas era muito preguiçoso ou desleixado para projetar um cenário de caso de uso de aplicativo autêntico que justificasse genuinamente o uso dessa ferramenta. nele desleixadamente. francamente, se me perguntassem isso em uma entrevista, eu o chamaria e provavelmente não iria querer trabalhar com alguém desleixado e semiaberto assim
anfíbio

Respostas:

22

Parece-me que eles estão levando você a uma solução de semáforo. Os semáforos são usados ​​para sinalizar para outro segmento que é a vez deles. Eles são usados ​​com muito menos frequência do que mutexes, o que eu acho que é por que eles acham que é uma boa pergunta para entrevista. É também por isso que o exemplo parece artificial.

Basicamente, você criaria msemáforos. Cada encadeamento xespera no semáforo e xdepois é postado no semáforo x+1depois de fazer o seu trabalho. No 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
fonte
Obrigado pela recompensa. Demorei um pouco para descobrir que, passando o mouse sobre quem diria.
Kdgregory
Desculpe minha ignorância, você pode elaborar mais sobre como esta solução está correta? isso é algum novo tipo sofisticado de semáforos? Estou certo, no entanto, de que a questão foi resolvida por uma solução de espera / notificação [que os semáforos usam].
precisa saber é
É apenas uma variedade de semáforos padrão. Nada de especial neles. O Notify é chamado "post" em algumas implementações.
Karl Bielefeldt
@KarlBielefeldt Bem, se cada thread x aguardar o semáforo x, todos os threads serão bloqueados e nada acontecerá. Se wait (sem) for realmente adquirir (sem) - eles entrarão ao mesmo tempo e não haverá exclusão. Até que haja mais esclarecimentos, acredito que haja algo errado nesse pseudocódigo e não deve ser a melhor resposta.
precisa saber é
Isso está apenas mostrando o loop para cada thread. O código de configuração teria que ser postado no primeiro semáforo para começar.
Karl Bielefeldt
23

Na minha opinião, essa é uma pergunta de entrevista fabulosa - pelo menos supondo (1) que o candidato tenha profundo conhecimento de encadeamento e (2) o entrevistador também tenha profundo conhecimento e esteja usando a pergunta para investigar o candidato. É sempre possível que o entrevistador esteja procurando uma resposta específica e restrita, mas um entrevistador competente deve procurar o seguinte:

  • Capacidade de diferenciar conceitos abstratos de implementação concreta. Eu jogo este aqui principalmente como um meta-comentário em alguns dos comentários. Não, não faz sentido processar uma única lista de palavras dessa maneira. No entanto, o conceito abstrato de um pipeline de operações, que pode abranger várias máquinas de diferentes capacidades, é importante.
  • Na minha experiência (quase 30 anos de aplicativos distribuídos, com vários processos e com vários threads), distribuir o trabalho não é a parte mais difícil. Reunir os resultados e coordenar processos independentes é o local onde a maioria dos erros de segmentação ocorre (novamente, na minha experiência). Ao destilar o problema em uma cadeia simples, o entrevistador pode ver o quão bem o candidato pensa sobre coordenação. Além disso, o entrevistador tem a oportunidade de fazer todos os tipos de perguntas subsequentes, como "OK, e se cada tópico tiver que enviar sua palavra para outro tópico para reconstrução".
  • O candidato pensa em como o modelo de memória do processador pode afetar a implementação? Se os resultados de uma operação nunca forem liberados do cache L1, isso é um bug, mesmo que não haja simultaneidade aparente.
  • O candidato separa o encadeamento da lógica do aplicativo?

Este último ponto é, na minha opinião, o mais importante. Novamente, com base na minha experiência, torna-se exponencialmente mais difícil depurar código encadeado se o encadeamento for misturado com a lógica do aplicativo (basta ver todas as perguntas do Swing no SO para exemplos). Acredito que o melhor código multithread é escrito como um código single-thread independente, com transferências claramente definidas.

Com isso em mente, minha abordagem seria fornecer a cada thread duas filas: uma para entrada e outra para saída. O encadeamento bloqueia durante a leitura da fila de entrada, retira a primeira palavra da sequência e passa o restante da sequência para a fila de saída. Algumas das características dessa abordagem:

  • O código do aplicativo é responsável por ler uma fila, fazer alguma coisa nos dados e gravar a fila. Não importa se é multiencadeado ou não, ou se a fila é uma fila na memória de uma máquina ou uma fila baseada em TCP entre máquinas que vivem em lados opostos do mundo.
  • Como o código do aplicativo é escrito como um único encadeamento, é testável de maneira determinística, sem a necessidade de muitos andaimes.
  • Durante sua fase de execução, o código do aplicativo possui a sequência que está sendo processada. Ele não precisa se preocupar com a sincronização com threads em execução simultânea.

Dito isto, ainda existem muitas áreas cinzentas que um entrevistador competente pode investigar:

  • "OK, mas estamos olhando para conhecer seu conhecimento das primitivas de simultaneidade; você pode implementar uma fila de bloqueio?" Sua primeira resposta, é claro, deve ser que você usaria uma fila de bloqueio pré-criada da sua plataforma preferida. No entanto, se você entender os encadeamentos, poderá criar uma implementação de fila em menos de uma dúzia de linhas de código, usando as primitivas de sincronização suportadas pela plataforma.
  • "E se uma etapa do processo demorar muito tempo?" Você deve pensar se deseja uma fila de saída limitada ou ilimitada, como poderá lidar com erros e efeitos na taxa de transferência geral se tiver um atraso.
  • Como enfileirar com eficiência a cadeia de origem. Não é necessariamente um problema se você estiver lidando com filas na memória, mas pode ser um problema se estiver se movendo entre máquinas. Você também pode explorar wrappers somente leitura sobre uma matriz de bytes imutáveis ​​subjacente.

Finalmente, se você possui experiência em programação simultânea, pode falar sobre algumas estruturas (por exemplo, Akka para Java / Scala) que já seguem esse modelo.

kdgregory
fonte
Toda essa nota sobre o cache L1 do processador realmente me intrigou. Votado.
Marc DiMillo
Recentemente, usei projectReactor com o Spring 5. O que me permite escrever código independente de thread.
kundan bora 7/04
16

Às vezes, as perguntas da entrevista são realmente truques, destinadas a fazer você pensar sobre o problema que está tentando resolver. Fazer perguntas sobre uma pergunta é parte integrante da abordagem de qualquer problema, seja no mundo real ou em uma entrevista. Existem vários vídeos circulando na Internet sobre como abordar perguntas em entrevistas técnicas (procure particularmente o Google e talvez a Microsoft).

"Apenas tente responder e dê o fora daqui .."

A aproximação de entrevistas com esse padrão de pensamento o levará a bombardear qualquer entrevista para qualquer empresa que valha a pena trabalhar.

Se você não acha que ganha muito (se houver algo com rosqueamento), diga isso a eles. Diga a eles por que você acha que não há nenhum benefício. Converse com eles. As entrevistas técnicas devem ser uma plataforma de discussão aberta. Você pode aprender algo sobre como isso pode ser útil. Não basta avançar cegamente tentando implementar algo que seu entrevistador lhe disse.

Demian Brecht
fonte
3
Eu diminuí a votação desta resposta (mesmo que inexplicavelmente tenha 4 votos positivos), porque não responde à pergunta que foi feita.
Robert Harvey
1
@RobertHarvey: Às vezes as pessoas fazem perguntas erradas . O OP tem uma mentalidade ruim para lidar com entrevistas técnicas e essa resposta foi uma tentativa de ajudar a colocá-lo no caminho certo.
Demian Brecht
1
@RobertHarvey Eu sinceramente acredito que esta é a resposta certa para a pergunta. A palavra-chave aqui é "pergunta da entrevista", mencionada no título e no corpo da pergunta. Para essa pergunta, esta é a resposta certa. Se a pergunta era apenas "Eu tenho m tópicos e um parágrafo de n palavras, e quero fazer isso e aquilo com eles, qual é a melhor abordagem", então sim, essa resposta não seria apropriada para a pergunta. Como é, acho ótimo. Parafraseando: Eu bombardeado completamente uma pergunta da entrevista poucos porque eu não seguir o conselho dado aqui
Shivan Dragão
O @RobertHarvey responde a uma pergunta relacionada, o voto negativo não conseguiu nada.
Marc DiMillo
0
  • Primeiro, tokenize o parágrafo com delimitadores apropriados e adicione as palavras a uma Fila.

  • Crie um número N de threads e mantenha-o em um conjunto de threads.

  • Itere sobre o conjunto de encadeamentos e inicie o encadeamento e aguarde
    a junção do encadeamento. E inicie o próximo segmento assim que o primeiro segmento terminar e assim por diante.

  • Cada thread deve apenas pesquisar a fila e imprimi-la.

  • Depois que todos os threads forem usados ​​no pool de threads, inicie do início do pool.

java_mouse
fonte
0

Como você disse, não acho que esse cenário se beneficie muito, se é que existe um encadeamento. Provavelmente é mais lento que uma única implementação encadeada.

No entanto, minha resposta seria ter cada thread em um loop restrito tentando acessar um bloqueio que controla o acesso ao índice de matriz de palavras. Cada thread pega o bloqueio, obtém o índice, obtém a palavra correspondente da matriz, imprime, incrementa o índice e libera o bloqueio. Os encadeamentos são encerrados quando o índice está no final da matriz.

Algo assim:

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

Eu acho que isso deve atingir um segmento após outro requisito, mas a ordem dos segmentos não é garantida. Estou curioso para ouvir outras soluções também.

ConditionRacer
fonte
-1

Use APIs de espera / sinal de condição para resolver esse problema.

Digamos que o primeiro thread escolha 1 palavra e, enquanto isso, descanse todos os threads aguardando um sinal. 1ª linha imprime a 1ª palavra e gera sinal para a próxima linha e, em seguida, a 2ª linha imprime a segunda palavra e gera sinal para a 3ª linha e assim por diante.

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

[Termos usados ​​aqui talvez específicos para threads POSIX]

Também deve ser possível usar um mutex FIFO para resolver esse problema.

Onde usar:

Suponha que dois segmentos T1 e T2 estão tentando executar uma seção crítica. Ambos não têm muito o que fazer fora desta seção crítica e mantêm bloqueios por um bom tempo. Portanto, T1 pode bloquear, executar e desbloquear e sinalizar T2 para ativação. Porém, antes que T2 pudesse ativar e adquirir bloqueio, T1 recupera o bloqueio e a execução. Dessa forma, o T2 pode ter que esperar muito tempo antes de realmente obter o bloqueio ou não.

Como funciona / Como implementar:

Tenha um mutex para travar. Inicialize os Dados Específicos do Encadeamento (TSD) para cada encadeamento em um nó contendo o ID e o semáforo do encadeamento. Além disso, possui duas variáveis ​​- de propriedade (TRUE ou FALSE ou -1), owner (ID do encadeamento do proprietário). Além disso, mantenha uma fila de garçons e um ponteiro waiterLast apontando para o último nó na fila de garçons.

operação de bloqueio:

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

operação de desbloqueio:

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

Pergunta interessante. Aqui está minha solução em Java usando SynchronousQueue para criar um canal de encontro entre threads:

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

Eu diria que esse tipo de pergunta é muito difícil de responder, porque pede a melhor maneira de fazer algo completamente estúpido. Meu cérebro simplesmente não funciona assim. Não consegue encontrar soluções para perguntas estúpidas. Meu cérebro diria imediatamente que, nessas condições, o uso de vários threads não faz sentido, então eu usaria um único thread.

Depois, eu pediria que eles fizessem uma pergunta do mundo real sobre a segmentação ou deixassem que eu desse um exemplo do mundo real de uma segmentação séria.

gnasher729
fonte