【學習筆記】C++并發(fā)與多線程筆記三:數(shù)據(jù)共享

一、前言

本文接上文 【學習筆記】C++并發(fā)與多線程筆記二 的內(nèi)容,主要包含創(chuàng)建多個線程、數(shù)據(jù)共享問題分析和案例代碼。

二、創(chuàng)建和等待多個線程

這里創(chuàng)建十個線程,并且使用同一個 入口函數(shù) my_thread(),代碼如下:

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

void my_thread(int num)
{
    cout << "my thread start, num = " << num << endl;
    /* 線程代碼 */
    cout << "my thread end, num = " << num << endl;
    return;
}

int main()
{
    vector<thread> m_threads;
    /* 創(chuàng)建10個線程,線程回調統(tǒng)一用 my_thread() */
    for (int i = 0; i < 10; i++)
    {
        m_threads.push_back(thread(my_thread, i)); /* 創(chuàng)建線程并開始執(zhí)行 */
    }
    for (auto iter = m_threads.begin(); iter != m_threads.end(); ++iter)
    {
        iter->join(); /* 等待線程結束 */
    }

    cout << "Hello World!" << endl;
    return 0;
}

輸出結果:

my thread start, num = 0my thread start, num = 2
my thread start, num = my thread start, num = 6my thread start, num = my thread start, num = 9
my thread end, num = 9

my thread end, num = 6
my thread start, num = 1
my thread end, num = 1
8
my thread end, num = 8
my thread start, num = 7
my thread end, num = 7

my thread end, num = 0
my thread start, num = 4
my thread end, num = 4
my thread start, num = 3
my thread end, num = 3
5
my thread end, num = 5
my thread end, num = 2
Hello World!
  1. 可以看到線程并不是按照創(chuàng)建順序執(zhí)行的,先創(chuàng)建的線程不一定先執(zhí)行,這個跟操作系統(tǒng)內(nèi)部對線程的運行調度機制有關。

  2. 主線程等待所有子線程運行結束,最后主線程結束,使用 join() 更容易寫出穩(wěn)定的程序。

  3. 把thread對象放入到 vector 容器中(類似對象數(shù)組),方便管理。

三、數(shù)據(jù)共享問題分析

3.1 只讀數(shù)據(jù)

如果一份數(shù)據(jù)是只讀的,提供給多個線程使用,每個線程讀到的數(shù)據(jù)都是一樣的,這份數(shù)據(jù)仍然是安全穩(wěn)定的,比如以下代碼中的 g_value

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

vector<int> g_value = {1, 2, 3}; /* 共享數(shù)據(jù)(只讀) */

void my_thread(int num)
{
    cout << "thread id = " << this_thread::get_id();
    cout << "\t g_v: " << g_value[0] << g_value[1] << g_value[2] << endl;
    return;
}

int main()
{
    vector<thread> m_threads;
    /* 創(chuàng)建10個線程,線程回調統(tǒng)一用 my_thread() */
    for (int i = 0; i < 10; i++)
    {
        m_threads.push_back(thread(my_thread, i)); /* 創(chuàng)建線程并開始執(zhí)行 */
    }
    for (auto iter = m_threads.begin(); iter != m_threads.end(); ++iter)
    {
        iter->join(); /* 等待線程結束 */
    }

    cout << "Hello World!" << endl;
    return 0;
}

輸出結果:

thread id = thread id = 12104    g_v: 1thread id = 9116  g_v: 123thread id = 4540        g_v: 123
thread id = 5108         g_v: 123
thread id = 2616         g_v: 123
thread id = 18056        g_v: 123
thread id = 2420         g_v: 123

thread id = 7440         g_v: 123
23
thread id = 4484         g_v: 123
4764     g_v: 123
Hello World!

3.2 讀寫數(shù)據(jù)

比如兩個線程寫g_value,八個線程讀g_value,如果代碼沒有特別處理,那程序大概率會崩潰,或者得到錯誤的結果,發(fā)生各種不可預料的結果。

最簡單的處理就是讀的時候不能寫,寫的時候不能讀,多個線程之間不能同時寫,也就說我們經(jīng)常說的線程同步問題。

3.3 其他案例

假設目前在售賣從北京到深圳的火車票,車次為T123,共有10個售票窗口,其中1號窗口和2號窗口的顧客同時都要訂99號座位,那這個時候在程序中的處理邏輯應該如下:

  1. 假設先處理1號窗口的訂單,首先需要查看這個99號座位是否為空,這里是讀數(shù)據(jù);
  2. 如果為空,則幫顧客訂購99號座位,并錄入系統(tǒng),這里是寫數(shù)據(jù);
  3. 2號窗口在1號窗口的操作的過程中必須等著,只有等1號窗口操作完成,2號窗口才能開始操作。

四、共享數(shù)據(jù)案例代碼

假設做一個網(wǎng)絡游戲服務器,有兩個子線程:

  • 子線程1收集玩家命令,并把命令數(shù)據(jù)寫到一個隊列中。
  • 子線程2從隊列中取出玩家發(fā)出來的命令并解析,然后執(zhí)行玩家需要的動作。

此處為了簡化實現(xiàn),用一個數(shù)字代表玩家發(fā)出來的命令,并且基于面向對象的思想,使用成員函數(shù)作為線程回調函數(shù)。

#include <iostream>
#include <thread>
#include <list>
using namespace std;

class A
{
public:
    /* 把收到的消息(玩家命令)存到隊列中 */
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i)
        {
            cout << "inMsgRecvQueue exec, push an elem " << i << endl;
            msgRecvQueue.push_back(i); /* 假設數(shù)字 i 就是收到的玩家命令 */
        }
    }
    /* 把數(shù)據(jù)從消息隊列中取出 */
    void outMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i)
        {
            if (!msgRecvQueue.empty())
            {
                int command = msgRecvQueue.front(); /* 返回第一個元素 */
                msgRecvQueue.pop_front();           /* 移除第一個元素 */
            }
            else
            {
                /* 消息隊列為空 */
                cout << "outMsgRecvQueue exec, but queue is empty!" << i << endl;
            }
            cout << "outMsgRecvQueue exec end!" << i << endl;
        }
    }

private:
    list<int> msgRecvQueue; /* 容器(實際上是雙向鏈表):存放玩家發(fā)生命令的隊列 */
};

int main()
{
    A obj;
    thread myInMsgObj(&A::inMsgRecvQueue, &obj);
    thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
    myInMsgObj.join();
    myOutMsgObj.join();

    cout << "Hello World!" << endl;
    return 0;
}

這里的消息隊列 msgRecvQueue 就是共享數(shù)據(jù),以上程序運行時會出現(xiàn)崩潰或數(shù)據(jù)亂套的現(xiàn)象,因為兩個線程的回調函數(shù) inMsgRecvQueue()outMsgRecvQueue() 分別在不斷讀寫消息隊列 msgRecvQueue,由于不做任何限制,可能會出現(xiàn) in 線程還沒 push 完一個數(shù)據(jù)時,out 線程就已經(jīng)把消息隊列中的首元素刪除的情況,這時 in 線程 push 的數(shù)據(jù)就不知道飛到哪里去了。

這里拋出問題,下篇我們通過互斥量來解決這個問題。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容