(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的字符顺序。

其他:

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

谢谢。

原文地址:https://www.cnblogs.com/waterfall/p/7994384.html