異構(gòu)計算關(guān)鍵技術(shù)之多線程技術(shù)(二)

異構(gòu)計算關(guān)鍵技術(shù)之多線程技術(shù)(二)

誕生伊始,計算機處理能力就處于高速發(fā)展中。及至最近十年,隨著大數(shù)據(jù)、區(qū)塊鏈、AI 等新技術(shù)的持續(xù)火爆,人們?yōu)樘嵘嬎闾幚硭俣雀前l(fā)展了多種不同的技術(shù)思路。大數(shù)據(jù)受惠于分布式集群技術(shù),區(qū)塊鏈帶來了專用處理器(Application-Specific IC, ASIC)的春天,AI 則讓大眾聽到了“異構(gòu)計算”這個計算機界的學術(shù)名詞。

“異構(gòu)計算”(Heterogeneous computing),是指在系統(tǒng)中使用不同體系結(jié)構(gòu)的處理器的聯(lián)合計算方式。在 AI 領域,常見的處理器包括:CPU(X86,Arm,RISC-V 等),GPU,F(xiàn)PGA 和 ASIC。(按照通用性從高到低排序)。

任務分解通常包括系統(tǒng)分解、流程分解、任務分解等,而線程/進程技術(shù)是必不可少的。

本系列文章將介紹異構(gòu)計算涉及到的內(nèi)存管理技術(shù)、DMA技術(shù)、線程技術(shù)等。結(jié)合實例代碼進行詳細講解多線程、DMA scatter-gather list、PCIe TLP等核心技術(shù)。

本章將介紹核心的基本概念:主要包括用戶態(tài)的線程、進程技術(shù)。

一、什么是進程、線程

在C++/C學習過程中,要想“更上一層樓”的話,多線程編程是必不可少的一步。

所以,我們在看這篇文章的時候,大家需要更多的思考是為什么這么做?這樣做的好處是什么?以及多線程編程都可以應用在哪里?

1. 線程與進程

進程是正在運行的程序的實例,而線程是是進程中的實際運作單位。
一個程序有且只有一個進程,但是可以擁有至少一個線程。
不同進程擁有不同的地址空間(3G虛擬地址,參考上一章的虛擬地址空間內(nèi)容),互不干擾,而不同線程擁有相同的地址空間。
1705473313798.png

2. 多進程

使用多進程并發(fā)是將一個應用程序劃分為多個獨立的進程(每個進程只有一個線程),這些獨立的進程間可以互相通信,共同完成任務。

由于操作系統(tǒng)對進程提供了大量的保護機制,以避免一個進程修改了另一個進程的數(shù)據(jù),使用多進程比使用多線程更容易寫出相對安全的代碼。但是這也造就了多進程并發(fā)的兩個缺點:

  • 在進程間的通信,無論是使用信號、套接字,還是文件、管道等方式,其使用要么比較復雜,要么就是速度較慢或者兩者兼而有之。
  • 運行多個線程的開銷很大,操作系統(tǒng)要分配很多的資源來對這些進程進行管理。

當多個進程并發(fā)完成同一個任務時,不可避免的是:操作同一個數(shù)據(jù)和進程間的相互通信,上述的兩個缺點也就決定了多進程的并發(fā)并不是一個好的選擇。所以就引入了多線程的并發(fā)。

3. 多線程

傳統(tǒng)的C++(C++11標準之前)中并沒有引入線程這個概念,在C++11出來之前,如果我們想要在C++中實現(xiàn)多線程,需要借助操作系統(tǒng)平臺提供的API,比如Linux的,或者windows下的

C++11提供了語言層面上的多線程,包含在頭文件中。

它解決了跨平臺的問題,提供了管理線程、保護共享數(shù)據(jù)、線程間同步操作、原子操作等類。

多線程并發(fā)指的是在同一個進程中執(zhí)行多個線程。

優(yōu)點: - 有操作系統(tǒng)相關(guān)知識的應該知道,線程是輕量級的進程,每個線程可以獨立的運行不同的指令序列,但是線程不獨立的擁有資源,依賴于創(chuàng)建它的進程而存在。 - 同一進程中的多個線程共享相同的地址空間,可以訪問進程中的大部分數(shù)據(jù),指針和引用可以在線程間進行傳遞。 - 同一進程內(nèi)的多個線程能夠很方便的進行數(shù)據(jù)共享以及通信,也就比進程更適用于并發(fā)操作。

缺點: - 由于缺少操作系統(tǒng)提供的保護機制,在多線程共享數(shù)據(jù)及通信時,就需要程序員做更多的工作以保證對共享數(shù)據(jù)段的操作是以預想的操作順序進行的,并且要極力的避免死鎖(deadlock)。

二、std::thread

構(gòu)造&析構(gòu)函數(shù)

| 函數(shù) | 類別 | 作用 |
| thread() noexcept | 默認構(gòu)造函數(shù) | 創(chuàng)建一個線程,什么也不做 |
| explicit thread(Fn&& fn, Args&&… args) | 初始化構(gòu)造函數(shù) | 創(chuàng)建一個線程,傳入?yún)?shù),執(zhí)行fn |
| thread(thread&& x) noexcept | 移動構(gòu)造函數(shù) | 構(gòu)造一個與x相同的對象,會破壞x對象 |
| ~thread | 析構(gòu)函數(shù) | 析構(gòu)對象 |

成員函數(shù) | 函數(shù) | 類別 | |:--------:| :---------:| | void join() | 等待線程結(jié)束并清理資源(會阻塞) | | bool joinable() | 返回線程是否可以join | | void detach() | 將線程與調(diào)用其的線程分離,彼此獨立執(zhí)行 | | std::thread::id get_id() | 獲取線程id | | thread& operator=(thread &&rhs) | 見移動構(gòu)造函數(shù)(如果對象是joinable的,那么會調(diào)用std::terminate()結(jié)果程序)|

實例

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

void count(int id, unsigned int n)
{
    for (unsigned int i = 0; i < n; i++)
        cout << "thread " <<id<<"finish!" <<endl;
}

// 這里的線程使用了C++17
int main()
{
    thread th[10];
    for (int i = 0; i < 10; i++)
        th[i] = thread(count, i, 1000);
    for (int i = 0; i < 10; i++) 
        if (th[i].joinable())
            th[i].join();
    return 0;
}

結(jié)果

thread 6finish!
thread 6finish!
thread 6finish!
thread 6finish!
thread 6finish!
thread 6finish!
thread 6finish!
thread 6finish!
3finish!5
thread 3finish!
thread 3finish!
thread 3finish!
thread 3finish!
thread 3finish!finish!
thread 3finish!

thread 5finish!
thread thread 3finish!
thread 3finish!
thread 3finish!
thread 3finish!
thread 3finish!
thread 3finish!

并且每次結(jié)果都不一樣!??!

這是為什么呢?這就是多線程的特色!

多線程運行時是以異步方式執(zhí)行的,與我們平時寫的同步方式不同。異步方式可以同時執(zhí)行多條語句。

總結(jié): - 線程是在thread對象被定義的時候開始執(zhí)行的,而不是在調(diào)用join函數(shù)時才執(zhí)行的,調(diào)用join函數(shù)只是阻塞等待線程結(jié)束并回收資源。 - 分離的線程(執(zhí)行過detach的線程)會在調(diào)用它的線程結(jié)束或自己結(jié)束時釋放資源 - 線程會在函數(shù)運行完畢后自動釋放,不推薦利用其他方法強制結(jié)束線程,可能會因資源未釋放而導致內(nèi)存泄漏。 - 沒有執(zhí)行join或detach的線程在程序結(jié)束時會引發(fā)異常

三、join()和detach()

每一個程序至少擁有一個線程,那就是執(zhí)行main()函數(shù)的主線程。而多線程則是出現(xiàn)兩個或兩個以上的線程并行運行,即主線程和子線程在同一時間段同時運行。而在這個過程中會出現(xiàn)幾種情況:

-   主線程先運行結(jié)束
-   子線程先運行結(jié)束
-   主子線程同時結(jié)束

在一些情況下需要在子線程結(jié)束后主線程才能結(jié)束,而一些情況則不需要等待,但需注意一點,并不是主線程結(jié)束了其他子線程就立即停止,其他子線程會進入后臺運行。

1. join()

join()函數(shù)是一個等待線程完成的函數(shù),主線程需要等待子線程結(jié)束后才可以結(jié)束。

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

void count()
{
    for (unsigned int i = 0; i < 10; i++)
        cout << "count: " << i <<endl;
}

// 這里的線程使用了C++17
//main()主線程
int main()
{
    cout << "main()"<<endl;
    cout << "main()"<<endl;
    cout << "main()"<<endl;

    thread th;
    th = thread(count);
    th.join();
    return 0;
}

結(jié)果

main()
main()
main()
count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
#include <iostream>
#include <thread>
using namespace std;

void count()
{
    for (unsigned int i = 0; i < 10; i++)
        cout << "count: " << i <<endl;
}

// 這里的線程使用了C++17
//main()主線程
int main()
{
    thread th;
    th = thread(count);
    cout << "main()"<<endl;
    cout << "main()"<<endl;
    cout << "main()"<<endl;

    th.join();
    return 0;
}

結(jié)果

main()count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9

main()
main()
#include <iostream>
#include <thread>
using namespace std;

void count()
{
    for (unsigned int i = 0; i < 10; i++)
        cout << "count: " << i <<endl;
}

// 這里的線程使用了C++17
//main()主線程
int main()
{
    thread th;
    th = thread(count);
    th.join();
    cout << "main()"<<endl;
    cout << "main()"<<endl;
    cout << "main()"<<endl;

    return 0;
}

結(jié)果

count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
main()
main()
main()

2. detach()

稱為分離線程函數(shù),使用detach()函數(shù)會讓線程在后臺運行,即說明主線程不會等待子線程運行結(jié)束才結(jié)束

通常稱分離線程為守護線程(daemon threads),UNIX中守護線程是指,沒有任何顯式的用戶接口,并在后臺運行的線程。這種線程的特點就是長時間運行;線程的生命周期可能會從某一個應用起始到結(jié)束,可能會在后臺監(jiān)視文件系統(tǒng),還有可能對緩存進行清理,亦或?qū)?shù)據(jù)結(jié)構(gòu)進行優(yōu)化。

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

void count()
{
    for (unsigned int i = 0; i < 10; i++)
        cout << "count: " << i <<endl;
}

// 這里的線程使用了C++17
//main()主線程
int main()
{
    cout << "main()"<<endl;
    cout << "main()"<<endl;
    cout << "main()"<<endl;
    thread th;
    th = thread(count);
    th.detach();

    return 0;
}

1705548646607.png

可以看到,子線程還沒有執(zhí)行完,主線程就結(jié)束了。

總結(jié)

join()函數(shù):是一個等待線程的函數(shù),主線程需等待子線程運行結(jié)束后才可以結(jié)束(注意不是才可以運行,運行是并行的)。如果打算等待對應線程,則需要細心挑選調(diào)用join()的位置。
detach()函數(shù):子線程的分離函數(shù),當調(diào)用該函數(shù)后,線程就被分離到后臺運行,主線程不需要等待該線程結(jié)束才結(jié)束。

四、std::atomic和std::mutex

我們現(xiàn)在已經(jīng)知道如何在c++11中創(chuàng)建線程,那么如果多個線程需要操作同一個變量呢?

1. 為什么要有atomic和mutex

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

int n = 0;
void count()
{
    for (unsigned int i = 0; i < 1000; i++)
        n++;
}

// 這里的線程使用了C++17
//main()主線程
int main()
{
    thread th[1000];
    for (int i = 0; i < 1000; i++)
        th[i] = thread(count);
    for (int i = 0; i < 1000; i++)
        th[i].join();

    cout << n <<endl;

    return 0;
}

幾次的輸出結(jié)果:

1705555056474.png

我們的輸出結(jié)果應該是1000000,可是為什么實際輸出結(jié)果比1000000小呢?

在上文我們分析過多線程的執(zhí)行順序——同時進行、無次序,所以這樣就會導致一個問題:多個線程進行時,如果它們同時操作同一個變量,那么肯定會出錯。

為了應對這種情況,c++11中出現(xiàn)了std::atomic和std::mutex。

2. std::mutex

std::mutex是 C++11 中最基本的互斥量,一個線程將mutex鎖住時,其它的線程就不能操作mutex,直到這個線程將mutex解鎖。

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

int n = 0;
mutex mtx;
void count()
{
    for (unsigned int i = 0; i < 1000; i++) {
        mtx.lock();
        n++;
        mtx.unlock();
    }
}

// 這里的線程使用了C++17
//main()主線程
int main()
{
    thread th[1000];
    for (int i = 0; i < 1000; i++)
        th[i] = thread(count);
    for (int i = 0; i < 1000; i++)
        th[i].join();

    cout << n <<endl;

    return 0;
}

1705555302811.png

| 函數(shù) | 作用 |
| void lock() | 將mutex上鎖。如果mutex已經(jīng)被其它線程上鎖, 那么會阻塞,直到解鎖;如果mutex已經(jīng)被同一個線程鎖住,那么會產(chǎn)生死鎖。 |
| void unlock() | 解鎖mutex,釋放其所有權(quán)。 |
| bool try_lock() | 嘗試將mutex上鎖。如果mutex未被上鎖,則將其上鎖并返回true;如果mutex已被鎖則返回false。 |

2. std::atomic

mutex很好地解決了多線程資源爭搶的問題,但它也有缺點:太!慢!了!

我們定義了100個thread,每個thread要循環(huán)10000次,每次循環(huán)都要加鎖、解鎖,這樣固然會浪費很多的時間,那么該怎么辦呢?接下來就是atomic大展拳腳的時間了。

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

atomic_int n = 0;
void count()
{
    for (unsigned int i = 0; i < 1000; i++) {
        n++;
    }
}

// 這里的線程使用了C++17
//main()主線程
int main()
{
    thread th[1000];
    for (int i = 0; i < 1000; i++)
        th[i] = thread(count);
    for (int i = 0; i < 1000; i++)
        th[i].join();

    cout << n <<endl;

    return 0;
}

1705555817562.png
原子操作是最小的且不可并行化的操作。

這就意味著即使是多線程,也要像同步進行一樣同步操作atomic對象,從而省去了mutex上鎖、解鎖的時間消耗。

| 函數(shù) | 作用 |
| atomic() noexcept = default | 構(gòu)造一個atomic對象(未初始化,可通過atomic_init進行初始化) |
| constexpr atomic(T val) noexcept | 構(gòu)造一個atomic對象,用val的值來初始化 |

五、std::this_thread

上面講了那么多關(guān)于創(chuàng)建、控制線程的方法,現(xiàn)在該講講關(guān)于線程控制自己的方法了。

在頭文件中,不僅有std::thread這個類,而且還有一個std::this_thread命名空間,它可以很方便地讓線程對自己進行控制。

std::this_thread常用函數(shù)

std::this_thread是個命名空間,所以你可以使用using namespace std::this_thread;這樣的語句來展開這個命名空間。

| 函數(shù) | 作用 |
| std::thread::id get_id() noexcept | 獲取當前線程id |
| template | |
| void sleep_for( const std::chrono::duration& sleep_duration ) | 等待sleep_duration(sleep_duration是一段時間) |
| void yield() noexcept | 暫時放棄線程的執(zhí)行,將主動權(quán)交給其他線程(放心,主動權(quán)還會回來) |

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

atomic_bool ready = 0;
void sleep(uintmax_t ms)
{
    this_thread::sleep_for(chrono::microseconds(ms));
}

void count()
{
    while(!ready)
        this_thread::yield();

    for (int i = 0; i <= 20'0000'0000; i++);

    cout << "thread " << this_thread::get_id() <<" finished!" <<endl;
    return;
}
// 這里的線程使用了C++17
//main()主線程
int main()
{
    thread th[10];
    for (int i = 0; i < 10; i++)
        th[i] = thread(::count);
    sleep(5000);
    ready = true;
    cout << "Start!" <<endl;
    for (int i = 0; i < 10; i++)
        th[i].join();
    return 0;
}

1705574217257.png

你的輸出幾乎不可能和我一樣,不僅是多線程并行的問題,而且每個線程的id也可能不同。

六、未完待續(xù)

下章將繼續(xù)介紹核心的基本概念:內(nèi)核態(tài)的線程/進程技術(shù)。

歡迎關(guān)注知乎:北京不北,+vbeijing_bubei

歡迎關(guān)注douyin:near.X (北京不北)

歡迎+V:beijing_bubei

獲得免費答疑,長期技術(shù)交流。

七、參考文獻

https://zhuanlan.zhihu.com/p/613630658

https://blog.csdn.net/sjc_0910/article/details/118861539

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

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

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