多线程同步面试问题:给定m个线程,找到n个单词


23

有没有办法可以从具有多个线程而不是单个线程的解决方案中受益于此问题?


在一次采访中,我被要求使用多个线程解决问题。在我看来,多个线程毫无益处。

这是问题所在:

给您一个段落,其中包含n个单词,给您m个线程。您需要做的是,每个线程应打印一个单词并将控制权交给下一个线程,这样每个线程将继续打印一个单词,以防最后一个线程到来,它应调用第一个线程。重复打印,直到段落中所有单词都打印完为止。最后,所有线程都应正常退出。将使用哪种同步?

我强烈感到我们无法利用这里的线程,但相信面试官正在尝试评估我的同步技能。我是否在此问题中缺少使多个线程有价值的内容?

无需编写代码,只需提出一些想法即可。我将自己执行。


添加C ++标记在这里可能无济于事。这里的问题是超越任何特定语言的更具概念性的东西。
cHao 2012年

相信你的感受。我了解他们的目标,但我从来不喜欢面试问题,这些问题与您如何解决现实世界中的问题相去甚远。
G_P 2012年

16
@rplusg-一位受访者会给我留下深刻的印象,他指出解决方案将问题序列化,并且仅增加线程开销,而没有实际执行任何并发处理。面试官总是可以坚持要求您回答所问的问题。
David Harkness 2012年

如果“每个线程应该打印一个单词并将控制权交给下一个线程”,这听起来像是串行工作,即一个线程正在等待上一个线程完成,就像传递一个中继一样。在这种情况下,为什么不仅仅使其成为单线程应用程序呢?
2013年

1
我明白了@Blrfl。有点像我需要确认您知道如何使用工具X,但是太懒惰或草率,无法设计出真正保证可以使用该工具的真实应用程序用例,因此我只是抓住了手头的所有内容并深入研究了我的示例放进去。坦白说,如果我在面试中被问到,我会打电话给他,也许不想和像这样马虎的混血儿一起工作
两栖动物2013年

Answers:


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

对不起,您能详细说明一下此解决方案的正确性吗?这是一些新型的信号灯吗?但是,我敢肯定,这个问题可以通过等待/通知解决方案(使用信号量)解决。
2014年

它只是一系列标准信号量。他们没什么特别的。在某些实现中,通知称为“发布”。
Karl Bielefeldt

@KarlBielefeldt好吧,如果每个线程x都将等待信号量x,那么所有线程都将被阻塞,并且什么也不会发生。如果wait(sem)实际上是sacquire(sem)-那么它们将同时进入并且没有排除。在没有更多说明之前,我相信此伪代码中存在错误,并且它不是最佳答案。
2014年

这只是显示每个线程的循环。设置代码必须发布到第一个信号量才能开始。
Karl Bielefeldt 2014年

23

我认为,这是一个神话般的面试问题-至少假设(1)预期该候选人对线程有深入的了解,并且(2)面试官也具有深入的知识,并正在使用该问题来探究该候选人。面试官总是有可能在寻找一个特定的,狭窄的答案,但是有能力的面试官应该在以下方面:

  • 区分抽象概念和具体实现的能力。我主要将它作为对某些评论的元评论。不,以这种方式处理单个单词列表没有任何意义。但是,可能会跨越功能不同的多台机器的操作流水线的抽象概念很重要。
  • 根据我的经验(近30年的分布式,多进程和多线程应用程序),分配工作并不是难事。收集结果和协调独立的流程是大多数线程错误发生的地方(同样,根据我的经验)。通过将问题简化为一个简单的链,面试官可以看到候选人对协调的想法有多好。另外,访问者有机会提出各种后续问题,例如“好,如果每个线程都必须将其单词发送到另一个线程以进行重建,该怎么办”。
  • 候选人是否考虑处理器的内存模型如何影响实施?如果一个操作的结果永远不会从L1缓存中清除,那就是一个错误,即使没有明显的并发。
  • 候选人是否将线程与应用程序逻辑分开?

我认为,最后一点是最重要的。再次,根据我的经验,如果将线程与应用程序逻辑混合在一起,调试线程化代码将成指数倍地变得困难(例如,请参考SO上的所有Swing问题)。我相信最好的多线程代码是独立的单线程代码,具有明确定义的切换。

考虑到这一点,我的方法是给每个线程两个队列:一个用于输入,一个用于输出。线程在读取输入队列时阻塞,从字符串中删除第一个单词,然后将字符串的其余部分传递到其输出队列。此方法的一些功能:

  • 应用程序代码负责读取队列,对数据进行处理以及写入队列。它不在乎它是否是多线程的,或者该队列是一台计算机上的内存中队列还是生活在世界另一端的计算机之间的基于TCP的队列。
  • 因为应用程序代码是按单线程编写的,所以可以确定性的方式对其进行测试,而无需花费很多时间。
  • 在执行阶段,应用程序代码拥有正在处理的字符串。它不必关心与并发执行线程的同步。

就是说,合格的面试官仍然可以探索很多灰色领域:

  • “好的,但是我们希望看到您对并发原语的了解;您可以实现阻塞队列吗?” 当然,您的第一个答案应该是使用所选平台上的预建阻止队列。但是,如果您确实了解线程,则可以使用平台支持的任何同步原语,在十几行代码下创建队列实现。
  • “如果过程中的一个步骤花费很长时间怎么办?” 您应该考虑是否要使用有界或无界的输出队列,如何处理错误,以及在有延迟的情况下对整体吞吐量的影响。
  • 如何有效地使源字符串入队。如果您要处理内存队列,则不一定是问题,但如果要在机器之间移动,则可能会成为问题。您还可以在基本的不可变字节数组之上探索只读包装器。

最后,如果您有并发编程的经验,则可以谈论已经遵循该模型的一些框架(例如,用于Java / Scala的Akka)。


关于处理器的L1缓存的整个笔记确实让我很感兴趣。投票了。
马克·迪米洛

我最近在Spring 5中使用了projectReactor。这使我可以编写与线程无关的代码。
kundan bora

16

面试问题有时实际上是技巧性问题,旨在使您考虑要解决的问题。大约一个问题问的问题是一个不可分割的接近的一部分的任何问题,无论是在现实世界还是在接受采访时。互联网上有许多视频,讲述如何在技术面试中解决问题(特别是对Google以及Microsoft而言)。

“只要尝试回答,就可以摆脱困境。”

以这种思维方式进行面试将使您轰炸任何值得工作的公司进行面试。

如果您认为自己没有收获太多(如果从线程获得任何收益),请告诉他们。告诉他们为什么您认为没有任何好处。与他们讨论。技术面试旨在成为一个开放的讨论平台。你可能会了解它是如何的东西可以是有用的。不要盲目地努力尝试实现面试官告诉您的事情。


3
我否决了这个答案(尽管莫名其妙地得到了4次否决),因为它没有回答所提出的问题。
罗伯特·哈维

1
@RobertHarvey:有时人们会问错问题。OP的处理技术面试的心态较差,此答案是为了帮助他/她走上正确的道路。
Demian Brecht 2012年

1
@RobertHarvey我诚实地相信这是该问题的正确答案。这里的关键字是“面试问题”,在标题和问题正文中都有提及。对于这样的问题,这是正确的答案。如果问题仅是“我有m个线程和n个单词的一个段落,并且我想和他们一起做,那是更好的方法”,那么是的,此答案不适用于该问题。因为我认为这很棒。释义:我轰炸了很多面试问题,因为我没有遵循这里给出的建议
Shivan Dragon 2013年

@RobertHarvey回答了一个相关的问题,拒绝投票没有完成任何事情。
马克·迪米洛

0
  • 首先用适当的定界符标记该段落,然后将单词添加到队列中。

  • 创建N个线程,并将其保留在线程池中。

  • 遍历线程池并启动线程,然后等待
    线程加入。并在第一个线程结束后开始下一个线程,依此类推。

  • 每个线程只应轮询队列并进行打印。

  • 在线程池中使用了所有线程后,请从池的开头开始。


0

如您所说,我认为这种情况不会从线程中受益很多。它最有可能比单线程实现慢。

但是,我的答案是让每个线程处于紧密的循环中,尝试访问一个锁,该锁控制对字数组索引的访问。每个线程都获取锁,获取索引,从数组中获取相应的单词,打印出来,递增索引,然后释放锁。当索引位于数组的末尾时,线程退出。

像这样:

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

我认为这应该可以实现一个线程接另一个要求,但是不能保证线程的顺序。我很好奇也听到其他解决方案。


-1

使用条件等待/信号API解决此问题。

假设第一个线程选择一个单词,而其余所有线程都在等待信号。第一个线程打印第一个字并生成信号到下一个线程,然后第二个线程打印第二个字并生成信号到第三个线程,依此类推。

#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)初始化到包含线程ID和信号量的节点。此外,还有两个变量-拥有的(TRUE或FALSE或-1),所有者(所有者线程ID)。此外,保留一个服务员队列和一个指针waiterLast,以指向服务员队列中的最后一个节点。

锁定操作:

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

我想说这种问题很难回答,因为它要求采取最佳方法来做完全愚蠢的事情。我的大脑无法正常工作。它找不到愚蠢问题的解决方案。我的大脑会立即说在这种情况下,使用多个线程是没有意义的,因此我将使用单个线程。

然后,我将要求他们提供有关线程的真实问题,或者让我给出一些严重的线程的真实示例。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.