Питання інтерв'ю з багатопотоковим запитанням: Знайдіть n слів, заданих м потоками


23

Чи є така проблема, щоб ця проблема могла отримати користь від рішення з декількома потоками, а не з однією ниткою?


В інтерв'ю мені запропонували вирішити проблему за допомогою декількох потоків. Мені здається, що кілька потоків не приносять користі.

Ось проблема:

Вам дають абзац, який містить n кількість слів, вам дано m ниток. Що вам потрібно зробити, це те, що кожен потік повинен надрукувати одне слово і надати керування наступному потоку. Таким чином, кожен потік буде продовжувати друкувати одне слово; Друк буде повторюватися, поки всі слова не будуть надруковані в абзаці. Нарешті всі теми повинні вийти витончено. Яку синхронізацію буде використовувати?

Я настійно відчуваю, що ми не можемо використати жодних переваг ниток, але вважаю, що інтерв'юер намагається виміряти мої навички синхронізації. Чи пропускаю я щось у цій проблемі, що зробило б значення кількох потоків?

Не потрібно коду, просто покладіть деякі думки. Я реалізую сам.


Додавання тегу C ++ тут, ймовірно, не дуже допоможе. Ці питання тут - це більше концептуальні речі, які виходять за рамки будь-якої конкретної мови.
cHao

Довіряйте своїм почуттям. Я розумію, для чого вони йдуть, але мені ніколи не подобалися питання інтерв'ю, які поки що відхиляються від того, як слід вирішити проблему в реальному світі.
G_P

16
@rplusg - Я був би набагато більше вражений респондентом, який зазначив, що рішення серіалізує проблему і просто додає потокові накладні дані, фактично не здійснюючи жодної одночасної обробки. Інтерв'юер завжди може наполягати на тому, щоб ви відповіли на запитання.
Девід Харкнесс

якщо "кожен потік повинен надрукувати одне слово і дати управління наступному потоку", це звучить як послідовна робота, тобто одна нитка чекає, коли попередня закінчиться, і це як передача реле. чому б просто не зробити його однопоточним додатком у такому випадку?
амфібій

1
я отримую це @Blrfl. це начебто мені потрібно перевірити, чи знаєте ви, як використовувати інструмент X, але було занадто ледачим чи неохайним, щоб розробити справжній сценарій використання додатків, який справді вимагає використання цього інструменту, тому я просто схопив все, що було під рукою, і мій приклад в неї неохайно. відверто кажучи, якби мене попросили в інтерв'ю, я зателефонував би йому на це, і, ймовірно, не хотів би працювати з кимось неохайним і наполовину таким
амфібієм

Відповіді:


22

Мені це здається, ніби вони ведуть вас до рішення семафору. Семафори використовуються для сигналізації іншої нитки про те, що їх черга. Вони використовуються набагато рідше, ніж мютекси, тому, напевно, вони вважають, що це гарне питання інтерв'ю. І тому приклад здається надуманим.

В основному, ви створили б mсемафори. Кожна нитка xчекає семафору, xа потім пости, щоб зробити семафор, x+1зробивши свою справу. У псевдокоді:

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

Дякую за щедроту. Зайняв мене деякий час, щоб зрозуміти, що мотанка над ним скаже, хто її дав.
kdgregory

Вибачте, моє невігластво, чи можете ви детальніше розкрити, наскільки правильне це рішення? це якийсь новий фантазійний тип семафорів? Я впевнений, однак, що питання вирішується рішенням очікування / повідомлення [яке семафори використовують].
AJed

Це просто масив стандартних семафорів. Нічого особливого в них немає. Повідомлення називається "розмістити" у деяких реалізаціях.
Карл Білефельдт

@KarlBielefeldt Добре, якщо кожен потік x буде чекати семафору x, то всі потоки будуть заблоковані і нічого не відбудеться. Якщо зачекати (sem) насправді придбає (sem) - тоді вони вберуться одночасно, і виключення немає. Поки не з’явиться більше уточнень, я вважаю, що в цьому псевдокоді щось не так, і це не має бути найкращою відповіддю.
AJed

Це просто показ циклу для кожної нитки. Код налаштування повинен був опублікувати перший семафор, щоб розпочати роботу.
Карл Білефельдт

23

На мою думку, це казкове запитання про інтерв'ю - принаймні припускаючи, що (1) від кандидата очікується глибоке знання нитки, і (2) інтерв'юер також має глибокі знання та використовує питання для дослідження кандидата. Завжди можливо, що інтерв'юер шукав конкретну, вузьку відповідь, але грамотний інтерв'юер повинен шукати наступне:

  • Здатність відрізняти абстрактні поняття від конкретної реалізації. Я кидаю це в основному як мета-коментар до деяких коментарів. Ні, немає сенсу обробляти єдиний список слів таким чином. Однак важливим є абстрактне поняття конвеєра операцій, який може охоплювати кілька машин різної спроможності.
  • На мій досвід (майже 30 років розповсюджених, багатопроцесових та багатопотокових програм), розповсюдження твору - це не найважча частина. Збір результатів та узгодження незалежних процесів - це місце, де трапляється більшість помилок з прошивкою (знову ж таки, на мій досвід). Перенаправивши проблему до простого ланцюжка, інтерв'юер може побачити, наскільки добре думає кандидат про координацію. Крім того, інтерв'юер має можливість задавати всілякі подальші запитання, такі як "Гаразд, що робити, якщо кожна нитка повинна направити своє слово в інший потік для реконструкції".
  • Чи думає кандидат про те, як модель пам'яті процесора може вплинути на впровадження? Якщо результати однієї операції ніколи не стираються з кешу L1, це помилка, навіть якщо немає явної одночасності.
  • Чи відокремлює кандидат введення нитки від логіки програми?

Цей останній пункт є, на мою думку, найважливішим. Знову-таки, виходячи з мого досвіду, налагоджувати потоковий код стає експоненціально складніше, якщо введення різьби змішується з логікою програми (просто подивіться на всі питання Swing на SO для прикладів). Я вважаю, що найкращий багатопотоковий код пишеться як автономний однопотоковий код з чітко визначеними передачами передач.

Зважаючи на це, мій підхід полягав би в тому, щоб дати кожному потоку дві черги: одну для введення, другу для виводу. Блок потоку, читаючи вхідну чергу, знімає перше слово з рядка і передає решту рядка у свою вихідну чергу. Деякі особливості такого підходу:

  • Код програми відповідає за читання черги, щось робити з даними та запис черги. Не важливо, багатопотокова чи ні, чи черга є чергою в пам'яті на одній машині чи чергою на основі TCP між машинами, які живуть на різних сторонах світу.
  • Оскільки код програми написаний як би однопотоковий, його можна перевірити детерміновано, без необхідності багато лісів.
  • Під час фази виконання коду програми належить обробляється рядок. При цьому не потрібно дбати про синхронізацію з одночасно виконаними потоками.

Однак, є багато сірих ділянок, які може перевірити компетентний інтерв'ю:

  • "Гаразд, але ми хочемо побачити ваші знання про примітиви одночасності; чи можете ви реалізувати блокуючу чергу?" Ваша перша відповідь, звичайно, повинна полягати в тому, що ви будете використовувати заздалегідь вбудовану чергу блокування з вашої платформи на вибір. Однак якщо ви розумієте потоки, ви можете створити реалізацію черги під десяток рядків коду, використовуючи будь-які примітиви синхронізації, які підтримує ваша платформа.
  • "Що робити, якщо один крок у процесі займає дуже довго?" Вам слід подумати про те, чи потрібно обмежена чи необмежена черга виводу, як ви можете обробляти помилки та впливати на загальну пропускну здатність, якщо у вас затримка.
  • Як ефективно зав'язувати вихідний рядок. Це не обов'язково проблема, якщо ви маєте справу з чергами в пам'яті, але це може бути проблемою, якщо ви переходите між машинами. Ви також можете досліджувати обгортки лише для читання, розташовані поверх основного масиву незмінних байтів.

Нарешті, якщо у вас є досвід одночасного програмування, ви можете поговорити про деякі рамки (наприклад, Akka для Java / Scala), які вже відповідають цій моделі.


Ціла записка про кеш-пам'ять L1 процесора насправді заінтригувала. Голосували.
Marc DiMillo

Нещодавно я використовував projectReactor з Spring 5. Які дозволяють мені писати агностичний код теми.
кунданська бора

16

Питання для співбесіди іноді насправді хитрі питання, покликані задуматися над проблемою, яку ви намагаєтеся вирішити. Запитання щодо запитання є невід'ємною частиною підходу до будь-якої проблеми, будь то в реальному світі чи в інтерв'ю. В Інтернеті розповсюджується чимало відео про те, як підходити до питань на технічних інтерв'ю (зокрема, зверніть увагу на Google та, можливо, Microsoft).

"Просто спробуй відповісти, і піди звідти."

Наближення до інтерв'ю з цією схемою думки призведе до того, що ви бомбардуєте будь-яке інтерв'ю для будь-якої компанії, на яку варто працювати.

Якщо ви не думаєте, що заробляєте багато (якщо що-небудь від нанизування), скажіть їм це. Скажи їм, чому ти не вважаєш, що вигоди є. Поговоріть з ними. Технічні інтерв'ю мають бути платформою для відкритих дискусій. Ви можете дізнатися щось про те, як це може бути корисно. Не просто вперед сліпо намагаючись втілити те, що вам сказав ваш інтерв'юер.


3
Я відповів на цю відповідь (навіть якщо вона незрозуміло отримала 4 відгуки), оскільки вона не відповідає на запитання, яке було задано.
Роберт Харві

1
@RobertHarvey: Іноді люди задають неправильні запитання . У ОП поганий розум для вирішення технічних інтерв'ю, і ця відповідь була спробою допомогти поставити його на правильний шлях.
Дем'ян Брехт

1
@RobertHarvey Я чесно вважаю, що це правильна відповідь на питання. Ключове слово тут - "запитання про інтерв'ю", яке згадується в назві та в тілі питання. На таке питання це правильна відповідь. Якби запитання було лише "У мене є m нитки та абзац з n слів, і я хочу зробити це і те, що з ними, який краще підхід", то так, ця відповідь не була б доречною для запитання. Я думаю, що це чудово. Перефразовуючи: Я бомбардував досить багато запитань щодо інтерв'ю, тому що я не дотримувався наданих тут порад
Shivan Dragon

@RobertHarvey відповідає на відповідне запитання, а голосування під голосуванням нічого не досягло.
Marc DiMillo

0
  • Спочатку позначте абзац відповідними роздільниками та додайте слова до черги.

  • Створіть N кількість потоків і зберігайте їх у пулі ниток.

  • Ітерайте над пулом ниток і запустіть нитку та дочекайтеся
    приєднання потоку. І запускайте наступну нитку, як тільки перша нитка закінчується тощо.

  • Кожна нитка повинна просто опитувати чергу та роздруковувати її.

  • Після того, як всі потоки будуть використані в пулі потоків, почніть з початку пулу.


0

Як ви сказали, я не думаю, що цей сценарій має велику користь, якщо взагалі від нарізування різьби. Це, швидше за все, повільніше, ніж реалізація однієї потоку.

Однак моя відповідь полягає в тому, щоб кожен потік був у тісному циклі, намагаючись отримати доступ до блокування, який контролює доступ до індексу слово масиву. Кожен потік захоплює замок, отримує індекс, отримує відповідне слово з масиву, друкує його, збільшуючи індекс, потім звільняє замок. Нитки виходять, коли індекс знаходиться в кінці масиву.

Щось на зразок цього:

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

Я думаю, що це повинно досягти однієї нитки після іншої вимоги, але впорядкованість ниток не гарантована. Мені цікаво почути і інші рішення.


-1

Використовуйте API очікування / сигнал API для вирішення цієї проблеми.

Скажімо, перший потік набирає 1 слово, а тим часом усі потоки чекають сигналу. Перший потік друкує 1-е слово і генерує сигнал до наступного потоку, а потім 2-й потік друкує друге слово і генерує сигнал до 3-го потоку тощо.

#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

[Тут використовуються терміни, можливо, специфічні для потоків POSIX]

Для вирішення цієї проблеми також має бути можливість використовувати мутекс FIFO.

Де використовувати:

Припустимо, два потоки T1 і T2 намагаються виконати критичний розділ. Обом не потрібно багато робити поза цим критичним розділом і тримати замки досить довгий час. Отже, T1 може заблокувати, виконати та розблокувати та подати сигнал T2 для пробудження. Але перш ніж T2 зможе прокинутися та придбати замок, T1 знову забирає блокування та виконання. Таким чином, T2, можливо, доведеться чекати дуже довго, перш ніж він дійсно отримає замок, а може бути і ні.

Як це працює / Як реалізувати:

Мати мутекс для блокування. Ініціалізуйте специфічні дані про нитку (TSD) для кожного потоку до вузла, що містить ідентифікатор потоку та семафор. Також є дві змінні - належать (TRUE або FALSE або -1), owner (ідентифікатор потоку власника). Крім того, збережіть чергу офіціантів та покажчик офіціантаЗалишні вказують на останній вузол у черзі офіціантів.

робота блокування:

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

операція розблокування:

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

-1

Цікаве запитання. Ось моє рішення в Java з використанням SynchronousQueue для створення каналу рандеву між потоками:

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

Я б сказав, що на таке запитання дуже важко відповісти, оскільки він просить найкращий спосіб зробити щось абсолютно дурне. Мій мозок просто не працює таким чином. Він не може знайти рішення для дурних питань. Мій мозок одразу сказав, що в цих умовах використання декількох ниток є безглуздим, тому я використовував би одну нитку.

Тоді я б попросив їх або задати реальне запитання про різьблення, або дозволити мені навести реальний приклад серйозної нитки.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.