C/C++程序員最苦惱的是自己跨平臺能力不是一半弱。如果想跨平臺,有一大波函數庫等着你來深入研究。你再反觀java。。。。
一、原子操作
所謂原子操作,就是多線程中“最小的且不可並行化的操作”。通常原子操作都是互斥訪問保證的。但是互斥一般靠平臺相關彙編指令,這也是爲什麼C++11之前一直沒有做的原因。
#include <atomic> //原子操作需要的頭文件
#include <thread> //線程頭文件
#include <iostream>
using namespace std;
atomic_llong total{ 0 };//原子數據類型long long
//這樣的構詞法還有atomic_int等等
//下面的東西不是這一節的內容
void func(int)
{
for (long long i = 0; i < 100000000LL; ++i)
total = total + i;
}
int main()
{
thread t1(func, 0);
thread t2(func, 0);
t1.join();
t2.join();
cout << total << endl;
return 0;
}
可以通過<cstdatomic>中查看內置的原子操作。這就出現一個問題。非內置類型始終怎麼實現原子操作的。這就是atom模板類,std::atomic<T> t; 如:
atomic<float> ad{ 12.7f };//這種寫法是C++11推薦的
原子操作通常屬於“資源型”的數據。這意味着多個線程通常只能訪問單個原子類型的拷貝。因此C++11中,原子類型只能從其模板參數中進行構造,標準不允許原子類型進行拷貝構造,移動構造,以及使用operator=等 ,以防止出現意外。這樣無法編譯。
atomic<float> ad1{ad };/./錯誤
爲了避免線程間關於a的競爭。模板改了很多地方。。
atomic<int> a;
int b =a;//相當於b = a.load();
int a =1;//相當於a.store(1);
二、線程
1.線程對象的創建
#include <thread>
#include <iostream>
using namespace std;
void func(int)
{}
int main()
{
thread t1(func, 0);
t1.join();
return 0;
}
線程的構造函數參數,可以視爲,(要執行的函數名,該函數參數1,該函數參數2,。。。。)在這裏,join是阻塞函數。意義爲等上面跑完纔開始執行下一個操作。我們如果沒有他會怎麼樣呢?主線程繼續往下跑,跑到return 0;但此時t1線程可能沒有跑完,線程對象卻要被強制釋放。如下面代碼;
#include <thread>
#include <iostream>
using namespace std;
void func(int a)
{
for (int i = 0; i < 10; ++i)
std::cout << a << endl;
}
int main()
{
int a;//停機變量無意義
{
thread t1(func, 1);
thread t2(func, 2);
}//運行到這裏,t1,t1沒有了
std::cin >> a;
return 0;
}
如果改成這樣,可以正常執行:int main()
{
int a;
{
thread t1(func, 1);
thread t2(func, 2);
t1.join();//主線程被阻塞了,停在這裏,等t1線程對象的線程執行完再運行
t2.join();
}
std::cin >> a;
return 0;
}
如果不希望線程被阻塞嗎,將線程與線程對象分離可以用t1.detach();將線程與線程對象分離。這裏要說明下,線程與線程對象是兩碼事(這個很重要)。我們僅是依託線程對象來創建線程。
如下;
int main()
{
int a;
{
thread t1(func, 1);
thread t2(func, 2);
t1.detach();//主線程沒有被阻塞,將線程與線程對象分離
t2.detach();
}//運行到這一步,線程對象依然會析構,但是線程卻可以繼續運行。
std::cin >> a;
return 0;
}
這也從側面描述了,用detach將線程與線程對象分開後,就不能合併了。2.線程的特點
線程不能複製,但可以移動(即使用std::move())。線程移動後,線程對象t不再代表任何線程。。
另外還可以用std::bind或lambda表達式創建。
thread t1(std::bind(func, 1));
thread t2([](int, int) {},1,2);
t1.join();
t2.join();
三、互斥量(實質就是鎖的變量)
互斥量是一種同步原語,線程同步手段,用於保護多線程同時訪問共享數據。“互斥量”的翻譯十分有迷惑性。它就是“鎖類”。以至於如果不這樣理解,將會對後面的條件變量混淆。C++11提供了4種互斥量。
1.獨佔互斥量std::mutex
互斥量接口都很相似,一般用法是通過lock()方法來阻塞線程,直到獲得互斥量所有權爲止。線程獲得互斥量並完成任務之後,就必須使用unlock()來解除對互斥量的佔用,lock()和unlock()必須成對出現。try_lock()嘗試鎖定互斥量,如成功返回true如失敗返回false,他是非阻塞的,看來可以用來檢查當前互斥量的狀態。
改動上面的程序將函數變成加鎖的:
#include <thread>
#include <iostream>
#include <mutex>
using namespace std;
std::mutex uni_lock;
void func(int a)
{
uni_lock.lock();
for (int i = 0; i < 10; ++i)
std::cout << a << endl;
uni_lock.unlock();
}
int main()
{
int a;
{
thread t1(func, 1);
thread t2(func, 2);
t1.join();
t2.join();
}
std::cin >> a;
return 0;
}
這顯示這就友好了官方建議儘量使用更安全的lock_guard。因爲他在構造時自動加鎖,析構時自動解鎖,防止忘解鎖的事情發生。lock_guard是個 類模板,形如其名託管互斥量。後面的幾個互斥量基本都用這種方法。
std::mutex u_lock;
void func(int a)
{
std::lock_guard<std::mutex> locker(u_lock);
for (int i = 0; i < 10; ++i)
std::cout << a << endl;
}
2.遞歸互斥量std::recursive_mutex
需要說明的是,還是儘量不要使用遞歸互斥量的好
(1)需要用到遞歸鎖定的多線程,往往可以簡化爲迭代。允許遞歸容易放縱複雜邏輯產生。
(2)遞歸鎖效率一般低一些。
(3)遞歸超過一定數目再lock進行調用會拋出std::system錯誤
3.帶超時的互斥量std::timed_mutex與std::recursive_timed_mutex
std::timed_mutex u_lock;
void func(int a)
{
std::chrono::milliseconds timeout(100);
while (1)
{
if (u_lock.try_lock_for(timeout))
{
///...///
}
}
}
4.給互斥量上的的兩種區域鎖
上面我們介紹了lock_guard,這其實是一種區域鎖,內部用實現機制是類模板。四、條件變量
條件變量是C++11提供的另一種用於等待的同步機制,它能阻塞一個或多個線程。直到收到另一個線程發出的通知或者超時,纔會喚醒當前阻塞的線程。條件變量需要和互斥的量配合起來用。C++11提供兩種條件變量。- std::condition_variable:必須與std::unique_lock配合使用(上文提到一種區間鎖)
- std::condition_variable_any:更加通用的條件變量,可以與任意類型的鎖配合使用,相比前者使用時會有額外的開銷。
他們的成員函數相同。
成員函數 | 說明 |
---|---|
notify_one | 通知一個等待線程 |
notify_all | 通知全部等待線程 |
wait | 阻塞當前線程直到被喚醒 |
wait_for | 阻塞當前線程直到被喚醒或超過指定的等待時間(長度) |
wait_until | 阻塞當前線程直到被喚醒或到達指定的時間(點) |
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx; // 全局互斥鎖.
std::condition_variable cv; // 全局條件變量.
bool ready = false; // 全局標誌位.
//下面是重點
void do_print_id(int id)
{
std::unique_lock <std::mutex> lck(mtx); //獨佔鎖
while (!ready) // 如果標誌位不爲 true, 則等待...
cv.wait(lck); // 當前線程被阻塞, 當全局標誌位變爲 true 之後,
// 線程被喚醒, 繼續往下執行打印線程編號id.
std::cout << "thread " << id << '\n';
}
void go()
{
std::unique_lock <std::mutex> lck(mtx);
ready = true; // 設置全局標誌位爲 true.
cv.notify_all(); // 通知喚醒所有線程.與上面額wait函數有關。
}
//上面是重點。main函數就是爲了生成10個線程。每個線程先死循環,之後突然運行go()打開死循環。
int main()
{
std::thread threads[10];
//下面開10個線程:
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(do_print_id, i);
go(); // go!
for (auto & th : threads) //直接誒是
th.join();
return 0;
}