(Google面試題)有四個線程1、2、3、4同步寫入數據……C++11實現

来源:http://www.cnblogs.com/waterfall/archive/2017/12/06/7994384.html
-Advertisement-
Play Games

最近在學習多線程,題目源自 MoreWindows先生的 《秒殺多線程第一篇》(http://blog.csdn.net/morewindows/article/details/7392749) 題目摘錄: 第五題(Google面試題) 有四個線程1、2、3、4。線程1的功能就是輸出1,線程2的功能 ...


最近在學習多線程,題目源自 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的字元順序。

其他:

目前網上搜到的很多代碼,同一時間只允許一個線程進行文件的寫入,或者是通過計數,一輪結束後才進行下一輪寫入。本代碼真正實現了線程間的並行。只有無文件可寫時才會進行等待。

謝謝。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 裝飾器: 定義:本質就是函數,(裝飾其它函數),就是為其它函數添加附加功能。 原則:1.不能修改被裝飾的函數的源代碼。 2.不能修改被裝飾的函數的調用方式。(被修飾函數感知不到) 實現裝飾器知識儲備: 函數即"變數" 高階函數 A:把一個函數名,當做形參傳給另外一個函數。 B:返回值中包含函數名 D... ...
  • Item 1. 考慮用靜態工廠方法替代構造器 獲得一個類的實例時我們都會採取一個公有的構造器。Foo x = new Foo(); 同時我們應該掌握另一種方法就是靜態工廠方法(static factory method)。 一句話總結,靜態工廠方法其實就是一個返回類的實例的靜態方法。 書中給出的例子 ...
  • 1. 輸入校驗章節目錄 輸入校驗概述 客戶端校驗 伺服器端校驗 手動編程校驗 重寫validate方法 重寫validateXxx()方法 輸入校驗流程 校驗框架校驗 Struts2 內置的校驗器 常用的內置校驗器的配置 客戶端校驗 伺服器端校驗 重寫validate方法 重寫validateXxx ...
  • websocket應用 手動實現的websocket 你所見過的websocket 你一定見過在網站中,有一個游客聊天的聊天框,比如人人影視。這個聊天框是如何實現即時通訊的呢,就是用到了websocket 你可以打開瀏覽器的network,會看到有個ws://xxxxx,這就代表了是websocke ...
  • ASCII表中的有些字元是列印不出來的,那麼怎樣表示這些無法列印的字元呢? C提供了3種表示方法. 一: 直接使用ASCII碼 二: 使用特殊的符號序列, 即轉義字元. 三: C90支持使用十六進位形式表示字元常量.(在這種形式中,反斜杠後跟一個x或X,再加上1到3位十六進位數字) 轉義字元 ASC ...
  • 1.在eclipse中用maven創建項目,右鍵new>>Maven Project 2.點擊next繼續 3.點擊next繼續,選擇maven-archetype-webapp, 4.點擊next繼續,填寫Group id和Artifact id, Version預設,Package可以不填 5. ...
  • 父類和子類的轉換 向上轉型: Father f1 = new son(); 向下轉型: son f2= (son)f1; 代碼如下: 父類 子類 主程式 ...
  • 這是因為切換成了java面板的原因 因為之前有切換到過 java project 項目,所以才轉到了這個面板,之後如果不手動改即便是用javaee也會是這個面板,因而用起來不方便 解決方法: 切換到javaee面板就好了 這樣的話用起來控制台等方面就更加靈活了 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...