Câu hỏi phỏng vấn đồng bộ hóa đa luồng: Tìm n từ cho m chủ đề


23

Có cách nào mà vấn đề này có thể được hưởng lợi từ một giải pháp có nhiều luồng hơn là một luồng không?


Trong một cuộc phỏng vấn, tôi được yêu cầu giải quyết vấn đề bằng cách sử dụng nhiều luồng. Nó xuất hiện với tôi rằng nhiều chủ đề cho vay không có lợi ích.

Đây là vấn đề:

Bạn được cung cấp một đoạn văn, trong đó có n số từ, bạn được cung cấp m chủ đề. Những gì bạn cần làm là, mỗi luồng nên in một từ và đưa điều khiển cho luồng tiếp theo, bằng cách này, mỗi luồng sẽ tiếp tục in một từ, trong trường hợp luồng cuối cùng xuất hiện, nó sẽ gọi luồng đầu tiên. In sẽ lặp lại cho đến khi tất cả các từ được in trong đoạn văn. Cuối cùng tất cả các chủ đề nên thoát một cách duyên dáng. Những loại đồng bộ hóa sẽ sử dụng?

Tôi cảm thấy mạnh mẽ rằng chúng ta không thể tận dụng bất kỳ lợi thế nào của các chủ đề ở đây, nhưng tin rằng người phỏng vấn đang cố gắng đo lường các kỹ năng đồng bộ hóa của tôi. Tôi có thiếu một cái gì đó trong vấn đề này sẽ làm cho nhiều chủ đề có giá trị?

Không cần mã, chỉ cần đặt một số suy nghĩ. Tôi sẽ tự thực hiện.


Thêm thẻ C ++ có lẽ sẽ không giúp được nhiều ở đây. Câu hỏi này ở đây là những thứ mang tính khái niệm hơn bất kỳ ngôn ngữ cụ thể nào.
cHao

Hãy tin vào cảm xúc của bạn. Tôi hiểu những gì họ sẽ làm, nhưng tôi chưa bao giờ thích các câu hỏi phỏng vấn đi lệch khỏi cách bạn nên giải quyết vấn đề trong thế giới thực.
G_P

16
@rplusg - Tôi sẽ ấn tượng hơn nhiều bởi một người được phỏng vấn đã chỉ ra rằng giải pháp tuần tự hóa vấn đề và chỉ thêm chi phí xử lý luồng mà không thực sự xử lý đồng thời. Người phỏng vấn luôn có thể yêu cầu bạn trả lời câu hỏi khi được hỏi.
David Harkness

nếu "mỗi luồng nên in một từ và đưa điều khiển cho luồng tiếp theo", âm thanh đó giống như công việc nối tiếp, tức là một luồng đang chờ chuỗi trước kết thúc và nó giống như chuyển tiếp. Tại sao không làm cho nó trở thành một ứng dụng đơn luồng trong trường hợp đó?
lưỡng cư

1
tôi hiểu rồi @Blrfl. Nó giống như tôi cần xác minh bạn biết cách sử dụng công cụ X nhưng quá lười biếng hoặc cẩu thả để thiết kế một kịch bản trường hợp sử dụng ứng dụng xác thực mà thực sự bảo đảm việc sử dụng công cụ đó vì vậy tôi chỉ nắm lấy bất cứ điều gì đầu tiên trong tay và ví dụ như tôi vào nó một cách chậm chạp. thẳng thắn, nếu tôi đã hỏi rằng trong một cuộc phỏng vấn, tôi muốn gọi anh ta ra về nó và có lẽ sẽ không muốn làm việc với một ai đó luộm thuộm và halfast như thế
amphibient

Câu trả lời:


22

Nghe có vẻ như họ đang dẫn bạn tới một giải pháp semaphore. Semaphores được sử dụng để báo hiệu một chủ đề khác đến lượt của họ. Chúng được sử dụng ít thường xuyên hơn nhiều so với mutexes, mà tôi đoán là lý do tại sao họ nghĩ rằng đó là một câu hỏi phỏng vấn tốt. Đó cũng là lý do tại sao ví dụ này dường như bị chiếm đoạt.

Về cơ bản, bạn sẽ tạo ra msemaphores. Mỗi luồng xchờ trên semaphore xsau đó đăng lên semaphore x+1sau khi thực hiện. Trong mã giả:

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

Cảm ơn vì tiền thưởng. Mất một lúc tôi mới nhận ra rằng việc di chuyển qua nó sẽ nói ai đã đưa nó.
kdgregory

Xin lỗi cho sự thiếu hiểu biết của tôi, bạn có thể giải thích thêm về cách giải pháp này là chính xác? Đây có phải là một số loại semaphores mới? Tuy nhiên, tôi chắc chắn rằng câu hỏi được giải quyết bằng giải pháp chờ / thông báo [sử dụng semaphores].
AJed

Nó chỉ là một mảng các semaphores tiêu chuẩn. Không có gì đặc biệt về họ. Thông báo được gọi là "bài" trong một số triển khai.
Karl Bielefeldt

@KarlBielefeldt Chà, nếu mọi luồng x sẽ đợi semaphore x, thì tất cả các luồng sẽ bị chặn và sẽ không có gì xảy ra. Nếu chờ đợi (sem) thực sự có được (sem) - thì họ sẽ vào cùng một lúc và không có loại trừ. Cho đến khi có nhiều giải thích rõ ràng hơn, tôi tin rằng có một cái gì đó sai trong mã giả này và nó không phải là câu trả lời tốt nhất.
AJed

Đây chỉ là hiển thị các vòng lặp cho mỗi chủ đề. Mã thiết lập sẽ phải đăng lên semaphore đầu tiên để khởi động mọi thứ.
Karl Bielefeldt

23

Theo tôi, đây là một câu hỏi phỏng vấn tuyệt vời - ít nhất là giả sử (1) ứng viên dự kiến ​​sẽ có kiến ​​thức sâu về luồng, và (2) người phỏng vấn cũng có kiến ​​thức sâu và đang sử dụng câu hỏi để thăm dò ứng viên. Luôn luôn có khả năng người phỏng vấn đang tìm kiếm một câu trả lời cụ thể, hẹp, nhưng một người phỏng vấn có thẩm quyền nên tìm kiếm những điều sau đây:

  • Khả năng phân biệt các khái niệm trừu tượng với thực hiện cụ thể. Tôi ném cái này vào chủ yếu như một bình luận meta về một số ý kiến. Không, không có nghĩa gì khi xử lý một danh sách các từ theo cách này. Tuy nhiên, khái niệm trừu tượng về một đường ống hoạt động, có thể trải rộng trên nhiều máy có khả năng khác nhau, rất quan trọng.
  • Theo kinh nghiệm của tôi (gần 30 năm ứng dụng phân tán, đa quy trình và đa luồng), phân phối công việc không phải là phần khó. Thu thập các kết quả và phối hợp các quy trình độc lập là nơi xảy ra hầu hết các lỗi luồng (một lần nữa, theo kinh nghiệm của tôi). Bằng cách chắt lọc vấn đề xuống một chuỗi đơn giản, người phỏng vấn có thể thấy ứng viên nghĩ tốt như thế nào về sự phối hợp. Thêm vào đó, người phỏng vấn có cơ hội hỏi tất cả các loại câu hỏi tiếp theo, chẳng hạn như "OK, nếu mỗi chủ đề phải gửi từ của nó đến một chủ đề khác để tái thiết."
  • Ứng viên có nghĩ về cách mô hình bộ nhớ của bộ xử lý có thể ảnh hưởng đến việc thực hiện không? Nếu kết quả của một thao tác không bao giờ bị xóa khỏi bộ đệm L1, thì đó là một lỗi ngay cả khi không có sự tương tranh rõ ràng.
  • Có ứng cử viên tách luồng từ logic ứng dụng?

Điểm cuối cùng này, theo tôi, là quan trọng nhất. Một lần nữa, dựa trên kinh nghiệm của tôi, việc gỡ lỗi mã luồng trở nên khó khăn hơn theo cấp số nhân nếu luồng được trộn với logic ứng dụng (chỉ cần xem tất cả các câu hỏi Xoay trên SO chẳng hạn). Tôi tin rằng mã đa luồng tốt nhất được viết dưới dạng mã đơn luồng độc lập, với các chuyển giao được xác định rõ ràng.

Với ý nghĩ này, cách tiếp cận của tôi sẽ là cung cấp cho mỗi luồng hai hàng đợi: một cho đầu vào, một cho đầu ra. Các chuỗi xử lý trong khi đọc hàng đợi đầu vào, loại bỏ từ đầu tiên của chuỗi và chuyển phần còn lại của chuỗi vào hàng đợi đầu ra của nó. Một số tính năng của phương pháp này:

  • Mã ứng dụng chịu trách nhiệm đọc hàng đợi, làm gì đó với dữ liệu và viết hàng đợi. Nó không quan tâm liệu nó có đa luồng hay không, hoặc hàng đợi là hàng đợi trong bộ nhớ trên một máy hay hàng đợi dựa trên TCP giữa các máy sống ở hai phía đối diện của thế giới.
  • Bởi vì mã ứng dụng được viết dưới dạng đơn luồng, nên nó có thể kiểm tra theo cách xác định mà không cần nhiều giàn giáo.
  • Trong giai đoạn thực thi, mã ứng dụng sở hữu chuỗi đang được xử lý. Nó không phải quan tâm đến việc đồng bộ hóa với các luồng thực thi đồng thời.

Điều đó nói rằng, vẫn còn rất nhiều khu vực màu xám mà một người phỏng vấn có thẩm quyền có thể thăm dò:

  • "OK, nhưng chúng tôi đang muốn xem kiến ​​thức của bạn về các nguyên thủy đồng thời; bạn có thể thực hiện một hàng đợi chặn không?" Tất nhiên, câu trả lời đầu tiên của bạn là bạn nên sử dụng hàng đợi chặn được xây dựng trước từ nền tảng bạn chọn. Tuy nhiên, nếu bạn hiểu các luồng, bạn có thể tạo một triển khai hàng đợi trong hàng tá dòng mã, sử dụng bất kỳ nguyên tắc đồng bộ hóa nào mà nền tảng của bạn hỗ trợ.
  • "Điều gì sẽ xảy ra nếu một bước trong quy trình mất một thời gian rất dài?" Bạn nên suy nghĩ về việc bạn muốn một hàng đợi đầu ra bị ràng buộc hoặc không bị ràng buộc, làm thế nào bạn có thể xử lý lỗi và ảnh hưởng đến thông lượng tổng thể nếu bạn có độ trễ.
  • Làm thế nào để hiệu quả enqueue chuỗi nguồn. Không nhất thiết là vấn đề nếu bạn đang xử lý hàng đợi trong bộ nhớ, nhưng có thể là vấn đề nếu bạn di chuyển giữa các máy. Bạn cũng có thể khám phá các trình bao bọc chỉ đọc ở trên một mảng byte bất biến nằm bên dưới.

Cuối cùng, nếu bạn có kinh nghiệm về lập trình đồng thời, bạn có thể nói về một số khung công tác (ví dụ: Akka cho Java / Scala) đã theo mô hình này.


Toàn bộ ghi chú về bộ đệm L1 của bộ xử lý thực sự khiến tôi tò mò. Bỏ phiếu lên.
Marc DiMillo

Gần đây tôi đã sử dụng projectReactor với Spring 5. Điều này cho phép tôi viết mã bất khả tri.
kundan bora

16

Các câu hỏi phỏng vấn đôi khi thực sự là những câu hỏi mẹo, nhằm mục đích khiến bạn suy nghĩ về vấn đề mà bạn đang cố gắng giải quyết. Đặt câu hỏi về một câu hỏi là một phần không thể thiếu trong việc tiếp cận bất kỳ vấn đề nào , cho dù đó là trong thế giới thực hay trong một cuộc phỏng vấn. Có một số video lưu hành trên internet về cách tiếp cận các câu hỏi trong các cuộc phỏng vấn kỹ thuật (đặc biệt là đối với Google và có lẽ Microsoft).

"Chỉ cần cố gắng trả lời, và rời khỏi đó .."

Tiếp cận các cuộc phỏng vấn với mô hình suy nghĩ này sẽ dẫn bạn đến đánh bom bất kỳ cuộc phỏng vấn nào cho bất kỳ công ty nào đáng làm việc.

Nếu bạn không nghĩ rằng bạn đạt được nhiều (nếu có bất cứ điều gì từ luồng), hãy nói với họ điều đó. Nói với họ tại sao bạn không nghĩ rằng có bất kỳ lợi ích. Có một cuộc thảo luận với họ. Phỏng vấn kỹ thuật có nghĩa là một nền tảng thảo luận mở. Bạn có thể sẽ học được điều gì đó về cách nó thể hữu ích. Đừng giả mạo trước một cách mù quáng cố gắng thực hiện một cái gì đó mà người phỏng vấn của bạn nói với bạn.


3
Tôi đã đánh giá thấp câu trả lời này (mặc dù nó không thể giải thích được có 4 câu trả lời), vì nó không trả lời được câu hỏi đã được hỏi.
Robert Harvey

1
@RobertHarvey: Đôi khi người ta hỏi những câu hỏi sai . OP có tư duy kém để giải quyết các cuộc phỏng vấn kỹ thuật và câu trả lời này là một nỗ lực giúp đưa anh ấy / cô ấy đi đúng hướng.
Demian Brecht

1
@RobertHarvey Tôi thành thật tin rằng đây là câu trả lời đúng cho câu hỏi. Từ khóa ở đây là "câu hỏi phỏng vấn" được đề cập trong tiêu đề và trong nội dung câu hỏi. Đối với một câu hỏi như vậy, đây là câu trả lời đúng. Nếu câu hỏi chỉ là "Tôi có m chủ đề và một đoạn gồm n từ và tôi muốn làm điều này và với họ, cách tiếp cận tốt hơn", thì có, câu trả lời này sẽ không phù hợp với câu hỏi. Vì tôi nghĩ nó rất tuyệt Diễn giải: Tôi đã ném bom khá nhiều câu hỏi phỏng vấn vì tôi đã không làm theo lời khuyên được đưa ra ở đây
Shivan Dragon

@RobertHarvey nó trả lời một câu hỏi liên quan, bỏ phiếu không hoàn thành bất cứ điều gì.
Marc DiMillo

0
  • Đầu tiên mã thông báo đoạn văn với các dấu phân cách thích hợp và thêm các từ vào Hàng đợi.

  • Tạo N số lượng chủ đề và giữ nó trong một nhóm chủ đề.

  • Lặp lại trên nhóm luồng và bắt đầu chuỗi và chờ cho
    chuỗi tham gia. Và bắt đầu chuỗi tiếp theo khi luồng đầu tiên kết thúc và cứ thế.

  • Mỗi chủ đề chỉ nên thăm dò hàng đợi và in nó.

  • Khi tất cả các luồng được sử dụng trong nhóm luồng, bắt đầu từ đầu của nhóm.


0

Như bạn đã nói, tôi không nghĩ kịch bản này mang lại lợi ích lớn, nếu hoàn toàn từ luồng. Nó hầu như chậm hơn so với thực hiện một luồng.

Tuy nhiên, câu trả lời của tôi sẽ là để mỗi luồng trong một vòng lặp chặt chẽ cố gắng truy cập vào một khóa kiểm soát truy cập vào chỉ mục mảng từ. Mỗi luồng lấy khóa, lấy chỉ mục, lấy từ tương ứng từ mảng, in nó, tăng chỉ mục sau đó giải phóng khóa. Các luồng thoát khi chỉ mục ở cuối mảng.

Một cái gì đó như thế này:

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

Tôi nghĩ rằng điều này sẽ đạt được một chủ đề sau một yêu cầu khác, nhưng thứ tự của các chủ đề không được đảm bảo. Tôi tò mò muốn nghe giải pháp khác là tốt.


-1

Sử dụng API điều kiện chờ / tín hiệu để giải quyết vấn đề này.

Giả sử chủ đề đầu tiên chọn 1 từ và trong khi đó, tất cả các chủ đề đang chờ tín hiệu. Chủ đề thứ 1 in từ thứ 1 và tạo tín hiệu cho chủ đề tiếp theo và sau đó chủ đề thứ 2 in từ thứ hai và tạo tín hiệu cho chủ đề thứ 3, v.v.

#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ều khoản được sử dụng ở đây có thể cụ thể cho chủ đề POSIX]

Cũng có thể sử dụng một mutex FIFO để giải quyết vấn đề này.

Sử dụng ở đâu :

Giả sử hai luồng T1 và T2 đang cố thực hiện một phần quan trọng. Cả hai không có nhiều việc phải làm ngoài phần quan trọng này và giữ khóa trong một khoảng thời gian tốt. Vì vậy, T1 có thể khóa, thực thi và mở khóa và báo hiệu T2 để thức dậy. Nhưng trước khi T2 có thể thức dậy và có được khóa, T1 sẽ lấy lại khóa và thực thi. Theo cách này, T2 có thể phải chờ một thời gian rất dài trước khi nó thực sự có được khóa hoặc có thể không.

Cách thức hoạt động / Cách thực hiện:

Có một mutex để khóa trên. Khởi tạo dữ liệu cụ thể của luồng (TSD) cho mỗi luồng vào một nút chứa id luồng và semaphore. Ngoài ra, có hai biến - sở hữu (TRUE hoặc FALSE hoặc -1), chủ sở hữu (id chủ đề chủ sở hữu). Ngoài ra, giữ một hàng đợi bồi bàn và một con trỏ waiterLast trỏ đến nút cuối cùng trong hàng đợi bồi bàn.

thao tác khóa:

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

hoạt động mở khóa:

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

-1

Câu hỏi thú vị. Đây là giải pháp của tôi trong Java bằng cách sử dụng SynousQueue để tạo kênh điểm hẹn giữa các luồng:

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

Tôi muốn nói rằng loại câu hỏi này rất khó trả lời, bởi vì nó yêu cầu cách tốt nhất để làm điều gì đó hoàn toàn ngu ngốc. Bộ não của tôi không hoạt động theo cách đó. Nó không thể tìm ra giải pháp cho những câu hỏi ngu ngốc. Bộ não của tôi sẽ ngay lập tức nói rằng trong những điều kiện này, sử dụng nhiều luồng là vô nghĩa, vì vậy tôi sẽ sử dụng một luồng.

Sau đó, tôi yêu cầu họ đưa ra một câu hỏi trong thế giới thực về việc xâu chuỗi, hoặc để tôi đưa ra một ví dụ thực tế về một số luồng nghiêm trọng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.