• (Google面试题)有四个线程1、2、3、4同步写入数据……C++11实现


    最近在学习多线程,题目源自 MoreWindows先生的 《秒杀多线程第一篇》(http://blog.csdn.net/morewindows/article/details/7392749)

    题目摘录:

    第五题(Google面试题)

    有四个线程1、2、3、4。线程1的功能就是输出1,线程2的功能就是输出2,以此类推.........现在有四个文件ABCD。初始都为空。现要让四个文件呈如下格式:

    A:1 2 3 4 1 2....

    B:2 3 4 1 2 3....

    C:3 4 1 2 3 4....

    D:4 1 2 3 4 1....

    请设计程序。

    代码实现如下:

     1 #include <iostream>
     2 #include <string>
     3 #include <thread>
     4 #include "Semaphore.h"
     5 
     6 constexpr unsigned MAX_COUNT = 10;
     7 constexpr unsigned MAX_THREAD = 4;
     8 
     9 unsigned getNext(unsigned current, bool order) {
    10     if (order) {
    11         unsigned next = current + 1;
    12         return  next % MAX_THREAD; // //正序
    13     } else {
    14         unsigned next = current - 1;
    15         return next % MAX_THREAD; //反序
    16     }
    17 }
    18 
    19 class File
    20 {
    21 public:
    22     File(const std::string& name = "0") 
    23     :m_name(name)
    24     ,m_buffer(MAX_COUNT, '0')
    25     ,m_pos(0){
    26         
    27     }
    28 
    29     void write(char c) {
    30         m_buffer.at(m_pos++) = c;
    31     }
    32 
    33     std::string getName() const {
    34         return m_name;
    35     }
    36     std::string getData() const {
    37         return m_buffer;
    38     }
    39 private:
    40     std::string m_name;
    41     std::string m_buffer;
    42     size_t m_pos;
    43 };
    44 
    45 Semaphore t1(1), t2(1), t3(1), t4(1);
    46 Semaphore* semaphoreArray[MAX_THREAD] = { &t1, &t2, &t3, &t4 };
    47 File files[MAX_THREAD] = { File("A"), File("B"), File("C"), File("D") };
    48 unsigned currentFile[MAX_THREAD] = { 0, 1, 2, 3 }; //每个线程当前应该写入的文件id
    49 
    50 void write(unsigned threadId) {
    51     const char c = '1' + threadId;
    52     auto& fid = currentFile[threadId];
    53     //auto fid = threadId;
    54     auto nextThreadId = getNext(threadId, true); //正序
    55     unsigned count = 0;
    56     while (MAX_COUNT != count) {
    57         semaphoreArray[threadId]->wait();
    58         //写文件        
    59         files[fid].write(c);
    60         //将文件传递给下一个线程。
    61         semaphoreArray[nextThreadId]->signal();
    62         //更新要写入的文件id
    63         fid = getNext(fid, false); //逆序
    64         ++count;
    65     }
    66 }
    67 
    68 
    69 int main() {
    70     std::cout << "hello" << std::endl;
    71 
    72     //create thread
    73     std::thread threads[MAX_THREAD];
    74     for (unsigned i = 0; i != MAX_THREAD; ++i) {
    75         threads[i] = std::thread(write, i);
    76     }
    77 
    78     //join
    79     for (auto& thread : threads) {
    80         thread.join();
    81     }
    82 
    83     //output
    84     for (const auto& file: files) {
    85         std::cout << file.getName() << ": " << file.getData() << std::endl;
    86     }
    87 
    88     std::cout << "bye" << std::endl;
    89     return 0;
    90 }

    其中Semaphore.h的代码见:http://www.cnblogs.com/waterfall/p/7966116.html

    运行结果截图:

    代码解析:

    简单分析:

    本问题也可以认为是生产者与消费者。只不过线程既生产又消费。按文件的角度,比如A文件的内容为1 2 3 4 1…… 相当于每个线程生产完之后交个下一个线程。

    所以对于每一个线程,设置好其初始文件,调用自身信号量的wait()进行生产,生产完毕则调用下一个线程的signal()。将完成的文件交给下一个线程,形成流水作业。

    啰里八嗦:

    首先,本代码用File类模拟文件写入,方便打印调试。void File::write(char c) 方法即在文件结尾追加字符c。

    根据题目要求可以看出,当文件(比如文件A)被线程1写入结束后,需要被下一个线程即线程2写入。因此会有A: 1 2 3 4 1...  B,C,D文件同理,只不过初始写入线程不同。

    在代码中利用 :

    unsigned currentFile[MAX_THREAD] = { 0, 1, 2, 3 }; //每个线程当前应该写入的文件id

    指定每个线程初始写入哪个文件。如果不想配置也可以用threadId作为线程初始写入的文件id(write函数中被注释掉的那一句),他们刚好相等。

    代码中用信号量表示线程可执行写入的次数。write函数会在写入完一个文件后,自动计算下一个要写入的文件。顺序为A,D,C,B,A....,逆序。

    Semaphore t1(1), t2(1), t3(1), t4(1); //用于设置信号量
    Semaphore* semaphoreArray[MAX_THREAD] = { &t1, &t2, &t3, &t4 }; //放入数组只是为了方便调用

    如果只是给线程1的信号量设为1,其他都设为0。线程的运行状况可以看作:

    step1: 线程1 信号量1 A->D     线程2 信号量0 B 阻塞    线程3 信号量0 C 阻塞  线程4 信号量0 D 阻塞

    step2: 线程1 信号量0 D  阻塞  线程2 信号量1 B->A      线程3 信号量0 C 阻塞  线程4 信号量0 D 阻塞

    step3: 线程1 信号量0 D  阻塞  线程2 信号量0 A 阻塞    线程3 信号量1 C->B    线程4 信号量0 D 阻塞

    step4: 线程1 信号量0 D  阻塞  线程2 信号量0 A 阻塞    线程3 信号量0 B 阻塞  线程4 信号量1 D->C

    第一轮运行结束:线程1在文件A中写入了1,线程2在文件B中写入了2,线程3在文件C中写入了3,线程4在文件D中写入了4。

    之后第二轮开始:线程1在文件D中写入了1,线程2在文件A中写入了2……

    以此类推,相当于同一时间只有1个线程在工作。每当线程工作时,将字符写入自己当前要写入的文件中,并计算出下一个要写入的文件。

    这种情况显然是不会出现冲突的。

    如果将线程1的初始信号量置为2:

    step1: 线程1 信号量2 A->D  线程2 信号量0 B 阻塞  线程3 信号量0 C 阻塞  线程4 信号量0 D 阻塞

    step2: 线程1 信号量1 D->C  线程1 信号量1 B->A    线程3 信号量0 C 阻塞  线程4 信号量0 D 阻塞

    可以看出文件D中被写入了字符1,出现错误。原因在于文件D当前期待被线程4写入,在4写入之前处于未就绪状态。此时被任何其他线程写入都是错误的。

    如果假设线程1和线程4的初始信号量各为1,其他为0。且假设线程4先运行

    step1.1: 线程1 信号量1 A 就绪     线程2 信号量0 B 阻塞  线程3 信号量0 C 阻塞  线程4 信号量1 D->C

    step1.2: 线程1 信号量2 A->D  线程2 信号量0 B 阻塞  线程3 信号量0 C 阻塞  线程4 信号量0 C 阻塞

    step2.1: 线程1 信号量1 D->C  线程1 信号量1 B->A    线程3 信号量0 C 阻塞  线程4 信号量0 D 阻塞

    文件D先被线程4写入字符4,之后再被线程1写入字符1便符合题目要求了。(D的序列应该为:4 1 2 3 4……)

    也就是说,线程1的信号量的增加,是基于前一个线程已经完成文件写入了。此时文件处于就绪状态,可以被下一个线程使用,也即把文件传递给下一个线程。

    所以最终的代码中,将每一个线程的初始信号量设置为1。可满足题目要求。哪怕任何线程在执行中出现阻塞(可用sleep模拟),也不影响其他线程运行。

    假设在线程1中执行一个长时间的sleep。可能的运行情况如下

    step0: 线程1 信号量1 A 就绪     线程2 信号量1 B 就绪  线程3 信号量1 C 就绪  线程4 信号量1 D 就绪

    一段时间后……

    step1:线程1 信号量4 A 就绪      线程2 信号量0 A 阻塞  线程3 信号量0 A 阻塞  线程4 信号量0 A 阻塞

    所有的线程都在等待将数据写入文件A中。但只有线程1先在文件A中写入字符1,之后释放信号,B才能继续写入。之后是C,D。依然没有冲突。文件A也满足1,2,3,4的字符顺序。

    其他:

    目前网上搜到的很多代码,同一时间只允许一个线程进行文件的写入,或者是通过计数,一轮结束后才进行下一轮写入。本代码真正实现了线程间的并行。只有无文件可写时才会进行等待。

    谢谢。

  • 相关阅读:
    表单元素(控件)不可见属性“visibility”和“display”分析
    如何实现网页的右键菜单功能
    Google Chrome 11 浏览器 下Flash Debug 插件无效的解决办法
    embed区别object
    在Fedora 12 下安装Intel X4500 显卡驱动
    HDOJ 2000
    费马小定理
    HDOJ 2055
    HDOJ 1018(阶乘位数)
    大数阶乘所得数位数的定理
  • 原文地址:https://www.cnblogs.com/waterfall/p/7994384.html
Copyright © 2020-2023  润新知