C++學習筆記--多執行緒
執行緒與程序優缺點對比:
多執行緒開銷小,但難於管理,且不能用於分散式系統;
多程序開銷大,作業系統會進行一部分管理,因此使用者管理就比較簡單,可用於分散式;
通常多執行緒和多程序結合使用。
參考資料:http://edu.csdn.net/course/detail/2303/35894?auto_start=1
程式碼例項:
1 最簡單的多執行緒
#include <iostream>
#include <thread>
void function_1()
{
std::cout <<"www.oxox.work"<<std::endl;
}
int main() //主執行緒
{
std::thread t1(function_1); //建立並初始化一個執行緒,且執行緒t1建立完之後就開始執行
//t1.detach(); //主執行緒和t1執行緒互不影響,detach可以使主執行緒不等待t1執行緒結束即可執行,這樣會使得程式的執行結果沒有內容輸出,因為主執行緒還沒有等到t1輸出內容就結束了。執行緒被deatch之後就不能再join了,如果再join,編譯不會報錯,但是執行會報錯
if(t1.joinable()) //如果執行了t1.detach(),t1.joinable()為false
{
t1.join(); //主執行緒將等待t1執行緒結束後再執行
}
return 0;
}
2 主執行緒和子執行緒交叉執行
#include <iostream>
#include <thread>
using namespace std;
void function_1()
{
cout <<"www.oxox.work"<<endl;
}
class Factor
{
public:
void operator()()
{
for(int i = 0; i > -100; --i)
{
cout << "from t1: " << i << endl;
}
}
};
int main() //主執行緒
{
//std::thread t1(function_1); //使用函式建立並初始化一個執行緒,且執行緒開始執行
Factor fct;
std::thread t1(fct); //使用函式物件建立並初始化一個執行緒
try
{
for(int i = 0; i < 100; ++i)
{
cout << "from main: " << i << endl;
}
}
catch(...) //上面的for迴圈屬於主執行緒,如果上面丟擲異常,但是沒有try catch,主執行緒終止,t1執行緒也終止了,這樣是非執行緒安全的。新增try catch之後,即使主執行緒異常,t1執行緒也能正常執行結束
{
t1.join(); //主執行緒將等待t1執行緒結束後再執行
throw;
}
return 0;
}
3 主執行緒和子執行緒之間實現記憶體共享
#include <iostream>
#include <thread>
#include <string>
using namespace std;
void function_1()
{
cout <<"www.oxox.work"<<endl;
}
class Factor
{
public:
void operator()(string &s)
{
cout << "from t1: " << s << endl;
s = "I love XuHuanDaXue";
}
};
int main() //主執行緒
{
string s("I love www.oxox.work"); //string變數s被主執行緒和t1執行緒使用,可通過s實現記憶體共享
Factor fct;
//std::thread t1(fct, s); //這種方式並不能在t1執行緒中改變s,因為s將被拷貝
std::thread t1(fct, std::ref(s)); //t1執行緒可改變s,因為引數是s的引用
t1.join();
cout << "from main: " << s.c_str() << endl; //主執行緒使用了被t1執行緒改變的s
return 0;
}
4 執行緒移動與執行緒ID
#include <iostream>
#include <thread>
#include <string>
using namespace std;
void function_1()
{
cout <<"www.oxox.work"<<endl;
}
class Factor
{
public:
void operator()(string &s)
{
cout << "from t1: " << s << endl;
s = "I love XuHuanDaXue";
}
};
int main() //主執行緒
{
string s("I love www.oxox.work");
Factor fct;
cout << std::this_thread::get_id() << endl; //獲取主執行緒ID,每個執行緒都有個ID
std::thread t1(fct, std::move(s)); //此處s被移動,移動操作比拷貝要高效,比引用要安全
std::thread t2=std::move(t1); //執行緒物件只能被移動,但不能被拷貝,所以必須使用std::move()
cout << t2.get_id() << endl; //獲取t2執行緒ID
//t1.join();
t2.join(); //t1被移動到t2,t1已經是空的了,所以得使用t2.join()
cout << "from main: " << s.c_str() << endl;
cout << std::thread::hardware_concurrency() <<endl; //檢測CPU能支援的最大執行緒數,如果使用者建立的執行緒數超過了CPU能支援的,反而會引起效能下降
return 0;
}
5 執行緒安全
下面的程式碼是非執行緒安全的,主執行緒和t1執行緒將競爭資源cout,只要競爭到資源就隨時可以將內容寫入到輸出流cout,使得輸出看起來是下面這樣的:
from t1: 0
from t1: -1
from t1: -2
from main: 0
from main: 1
from main: 2
from main: 3
from main: 4
from main: 5
from main: 6
from main: 7
from t1: -3
from t1: -4
from t1: -5
#include <iostream>
#include <thread>
#include <string>
using namespace std;
void function_1()
{
for(int i = 0; i > -100; --i)
{
cout << "from t1: " << i << endl;
}
}
int main() //主執行緒
{
std::thread t1(function_1);
for(int i = 0; i < 100; ++i)
{
cout << "from main: " << i << endl;
}
t1.join(); //主執行緒將等待t1執行緒結束後再執行
return 0;
}
可以加mutex鎖,有執行緒正在使用cout,其他執行緒就不能使用,這樣cout在當前程式中是執行緒安全的,能使得輸出是有序的,是下面這樣的:
from main: 0
from t1: 0
from main: 1
from t1: -1
from main: 2
from t1: -2
from main: 3
from t1: -3
from main: 4
from t1: -4
from main: 5
from t1: -5
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
using namespace std;
std::mutex mtx;
void shared_print(string s, int id)
{
mtx.lock();
cout << s.c_str() << id << endl;
mtx.unlock();
}
void function_1()
{
for(int i = 0; i > -100; --i)
{
shared_print("from t1: ",i);
}
}
int main() //主執行緒
{
std::thread t1(function_1);
for(int i = 0; i < 100; ++i)
{
shared_print("from main: ",i);
}
t1.join(); //主執行緒將等待t1執行緒結束後再執行
return 0;
}
上面的程式碼中,當shared_print函式中cout那一行丟擲異常時,mtx.unclock()不會被執行,mtx將被永遠地鎖住。這時可以使用std::lock_guard來保證mtx會被解鎖。shared_print函式修改如下:
void shared_print(string s, int id)
{
std::lock_guard<std::mutex> locker(mtx); //lock_guard物件建立時會自動對mtx加鎖,離開作用域被析構時,mtx會被自動解鎖,這樣即使cout這行發生異常,mtx也能被解鎖了
cout << s.c_str() << id << endl;
}
但是上面的程式碼仍然不是安全的,因為cout是個全域性變數,並沒有完全在mtx的保護下,其他執行緒仍然可以在不加鎖的情況下使用cout。為了完整地保護資源,必須使資源和互斥物件進行繫結。程式碼如下:
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
using namespace std;
class LofFile
{
public:
LofFile()
{
f.open("log.txt");
}
void shared_print(string s, int id)
{
std::lock_guard<std::mutex> locker(m_mutex);
f << s << id << endl;
}
private:
std::mutex m_mutex;
std::ofstream f;
};
void function_1(LofFile& log)
{
for(int i = 0; i > -100; --i)
{
log.shared_print("from t1: ",i);
}
}
int main() //主執行緒
{
LofFile log;
std::thread t1(function_1, std::ref(log));
for(int i = 0; i < 100; ++i)
{
log.shared_print("from main: ",i);
}
t1.join(); //主執行緒將等待t1執行緒結束後再執行
return 0;
}
上面的程式碼將資源std::ofstream f和互斥物件std::mutex m_mutex定義在LogFile類中,類外的執行緒不可訪問資源f,使用類物件的shared_print函式的執行緒也能保證資源f必定有一個互斥物件m_mutex來保護。簡單地講,就是資源和互斥物件必定成對地出現在同一個作用域中,因此資源一定會受互斥物件保護。注意,這裡的程式碼使用了資源std::ofstream f,而不是cout,是因為cout是全域性的資源。
6 避免死鎖
死鎖是指兩個執行緒互相鎖住,互相等待釋放,卻不能釋放,下面的程式碼發生了死鎖,程式的輸出可能是這樣的(程式被暫停在某處,並沒有成功執行完畢):
from t1: 0
from main: 0
from t1: -1
from main: 1
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
using namespace std;
class LofFile
{
public:
LofFile()
{
f.open("log.txt");
}
void shared_print(string s, int id) //函式被t1執行緒呼叫
{
std::lock_guard<std::mutex> locker(m_mutex);
std::lock_guard<std::mutex> locker2(m_mutex2);
cout << s << id << endl; //為了更直觀得看到結果,這裡改成cout
}
void shared_print2(string s, int id) //函式被主執行緒呼叫
{
std::lock_guard<std::mutex> locker2(m_mutex2);
std::lock_guard<std::mutex> locker(m_mutex);
cout << s << id << endl; //為了更直觀得看到結果,這裡改成cout
}
//如果t1執行緒執行到鎖住m_mutex時,主執行緒正好執行到鎖住m_mutex2,t1執行緒繼續執行下一語句發現m_mutex2被鎖住了,於是等待m_mutex2被解鎖,而主執行緒也繼續執行下一語句發現m_mutex被鎖住了,於是等待m_mutex被解鎖,兩個執行緒相互等待,這樣就發生了死鎖,程式就一直暫停在哪裡
private:
std::mutex m_mutex;
std::mutex m_mutex2;
std::ofstream f;
};
void function_1(LofFile& log)
{
for(int i = 0; i > -100; --i)
{
log.shared_print("from t1: ",i);
}
}
int main() //主執行緒
{
LofFile log;
std::thread t1(function_1, std::ref(log));
for(int i = 0; i < 100; ++i)
{
log.shared_print2("from main: ",i);
}
t1.join(); //主執行緒將等待t1執行緒結束後再執行
return 0;
}
上面的程式碼中,m_mutex和m_mutex2在兩個執行緒中加鎖的順序是相反的,如果將語句的順序改成一致就不會發生死鎖。在C++標準庫中提供了std::lock,是規範的處理死鎖問題的方法,把上面的兩個函式改成下面這樣:
void shared_print(string s, int id) //函式被t1執行緒呼叫
{
std::lock(m_mutex, m_mutex2); //std::lock可以指定鎖的順序,引數為lock1,lock2,...,lockn,它的引數個數是不固定的,有多少個鎖就可以使用多少個引數
std::lock_guard<std::mutex> locker(m_mutex, std::adopt_lock); //這裡新增std::adopt_lock是告知locker,m_mutex已經被鎖住,locker要做的只是獲得m_mutex的所有權,然後在析構時將其解鎖即可
std::lock_guard<std::mutex> locker2(m_mutex2, std::adopt_lock);
cout << s << id << endl; //為了更直觀得看到結果,這裡改成cout
}
void shared_print2(string s, int id) //函式被主執行緒呼叫
{
std::lock(m_mutex, m_mutex2);
std::lock_guard<std::mutex> locker2(m_mutex2, std::adopt_lock);
std::lock_guard<std::mutex> locker(m_mutex, std::adopt_lock);
cout << s << id << endl; //為了更直觀得看到結果,這裡改成cout
}
上面的程式碼中,即使locker和locker2的順序是相反的,但是m_mutex和m_mutex2加鎖的順序是相同的,因為std::lock指定了加鎖的順序。
為了避免程式設計中出現死鎖,可以遵循以下幾條規則:
(1)使用一個mutex即可滿足要求的場合,絕不使用兩個mutex;
(2)如果某個作用域中已經使用了一個mutex,那麼要小心該作用域中的函式呼叫,因為該函式呼叫中可能包括其他mutex;
(3)無法避免地需要使用兩個以上mutex時,儘量使用std::lock指定鎖的順序,但是在某些極端情況下std::lock無法使用,就要小心地保證加鎖的語句順序。
7 Unique Lock和call_once
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
using namespace std;
class LogFile
{
public:
LogFile()
{
f.open("log.txt");
}
void shared_print(string s, int id)
{
//std::lock_guard<std::mutex> locker(m_mutex); //lock_guard物件在構造時自動m_mutex加鎖,在析構時自動對m_mutex解鎖,使用者無法自由控制何時加鎖解鎖
//std::unique_lock<std::mutex> locker(m_mutex); //unique_lock可以起到與lock_guard一樣的功能,預設情況下是構造自動加鎖,析構自動解鎖
std::unique_lock<std::mutex> locker(m_mutex, std::defer_lock); //還可以使用defer_lock告知locker,不要在構造時自動加鎖m_mutex
locker.lock(); //然後使用者自行給m_mutex加鎖
f << s << id << endl;
locker.unlock(); //還可以在需要的時候自行解鎖,這樣m_mutex只鎖住了上面一行語句,之後的操作就沒有被m_mutex鎖住
//程式碼塊...
locker.lock(); //接下來還可以隨意地自行加鎖和解鎖
//程式碼塊...
locker.unlock();
//lock_guard和unique_lock都不能被複制,但是unique_lock可被移動。當unique_lock被移動時,m_mutex的控制權也從一個unique_lock轉移到另一個unique_lock。另外,unique_lock的代價比lock_guard高,所以使用lock_guard即可滿足要求的場合就使用lock_guard
//std::unique_lock<std::mutex> locker2 = std::move(locker);
}
private:
std::mutex m_mutex;
std::ofstream f;
};
void function_1(LogFile& log)
{
for(int i = 0; i > -100; --i)
{
log.shared_print("from t1: ",i);
}
}
int main() //主執行緒
{
LogFile log;
std::thread t1(function_1, std::ref(log));
for(int i = 0; i < 100; ++i)
{
log.shared_print("from main: ",i);
}
t1.join(); //主執行緒將等待t1執行緒結束後再執行
return 0;
}
上面使用的所有示例程式碼中,LogFile類都是在構建函式中開啟log.txt檔案,如果我們想只在呼叫shared_print函式的時候才打開檔案,可以做如下修改:
class LogFile
{
public:
LogFile()
{
//f.open("log.txt");
}
void shared_print(string s, int id)
{
//if(!f.is_open()) //這段程式碼不是執行緒安全的,因為兩個執行緒可能同時執行到f.open處,兩次開啟檔案,將會執行報錯
//{
// f.open("log.txt");
//}
//if(!f.is_open()) //這段程式碼仍然不是執行緒安全的,假設a執行緒尚未執行完f.open,b執行緒正好檢測到檔案未開啟,然後發現m_mutex_fopen被鎖住,於是等待,接著a執行緒打開了檔案,解鎖m_mutex_fopen,此時b執行緒發現解鎖了,立即執行f.open,這樣就兩次開啟檔案,將會執行報錯
//{
// std::unique_lock<std::mutex> locker(m_mutex_fopen, std::defer_lock);
// f.open("log.txt");
//}
//{ //這段程式碼是執行緒安全的,但是這存在一個性能上的問題,即每次函式呼叫都要對m_mutex_fopen進行加鎖和解鎖,這會無意義地消耗計算機資源
// std::unique_lock<std::mutex> locker(m_mutex_fopen, std::defer_lock);
// if(!f.is_open()) f.open("log.txt");
//}
std::call_once(m_flag, [&](){f.open("log.txt")}) //這行程式碼能確保後面的lambda函式只被一個執行緒呼叫一次,是C++標準庫的推薦用法
std::unique_lock<std::mutex> locker(m_mutex, std::defer_lock);
f << s << id << endl;
}
private:
std::mutex m_mutex;
std::mutex m_mutex_fopen;
std::once_flag m_flag;
std::ofstream f;
};
8 條件變數
條件變數適用於a執行緒需要等待b執行緒觸發某種條件,a執行緒才能執行的場合
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
#include <deque>
#include <functional>
#include <condition_variable>
using namespace std;
std::deque<int> q;
std::mutex mu;
std::condition_variable cond;
void function_1()
{
int count = 10;
while (count > 0)
{
std::unique_lock<mutex> locker(mu);
q.push_front(count);
locker.unlock();
cond.notify_one(); //啟用條件變數cond
cond.notify_all(); //notify_one只能啟用一個正在等待cond被啟用的執行緒
//notify_all可以啟用所有正在等待cond被啟用的執行緒
std::this_thread::sleep_for(chrono::seconds(1));
count--;
}
}
void function_2()
{
int data = 0;
while (data != 1)
{
std::unique_lock<mutex> locker(mu);
/*if (!q.empty())
{
data = q.back();
q.pop_back();
locker.unlock();
cout << "t2 got a value from t1: " << data << endl;
}
else //當q為空時,一直在執行while迴圈,不斷查詢,直到q非空,這樣是很低效的,應該使用條件變數
{
locker.unlock();
}*/
//等待執行緒1呼叫notify_one()啟用條件變數bond,只有cond被啟用,才可以
//執行後面的語句。此處locker作為引數傳遞給wait之前已經加鎖了,一個
//執行緒不會在鎖住的情況下休眠,所以wait()先將mu解鎖,使其休眠,然後
//又加鎖。由於需要重複加解鎖,所以此處得用unique_lock,而不能使用
//lock_guard
//某些情況下,執行緒可能被自己啟用,這稱為偽啟用,給條件變數新增一個
//lambda函式作為增加的一個條件,滿足條件才可以被啟用
cond.wait(locker, [](){return !q.empty(); });
data = q.back();
q.pop_back();
locker.unlock();
cout << "t2 got a value from t1: " << data << endl;
}
}
int main()
{
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
return 0;
}
9 Future, Promise和async()
std::future類可以從子執行緒獲取返回值,然後在父執行緒中使用
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
#include <future>
using namespace std;
int factorial(int N)
{
int res = 1;
for (int i = N; i > 1; --i)
{
res *= i;
}
cout << "Result is: "<< res << endl;
return res;
}
int main()
{
int x;
//std::thread t1(factorial,4);
//t1.join();
//有時希望將子執行緒的執行結果返回給父執行緒,可以修改上面的程式碼將x的引用
//傳給t1,通過記憶體共享的方式來實現,但是更簡單的方式是使用future物件,
//取名future的含義是,這個物件可以從未來獲取某個值,即等待未來子執行緒執
//行結束時返回的值。實際上async並不一定會建立子執行緒,如果明確指定第一
//個引數為std::launch::deferred,將不建立子執行緒,此時factorial()呼叫將被延期,
//等到get()執行後,就在父執行緒中呼叫factorial。如果引數為std::launch::async,
//就是明確指定建立子執行緒。async的引數預設是std::launch::async | std::launch::deferred,
//意思是是否建立子執行緒將取決於實現(沒有弄明白取決於什麼實現?)
//std::future<int> fu = std::async(factorial, 4);
std::future<int> fu = std::async(std::launch::async | std::launch::deferred, factorial, 4);
x = fu.get(); //get將會等待子執行緒結束,並取回子執行緒返回的結果,get只能被呼叫一次,呼叫兩次程式會執行報錯
return 0;
}
std::promise可以從父執行緒獲取值到子執行緒中使用
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
#include <future>
using namespace std;
int factorial(std::future<int>& f)
{
int N = f.get();
int res = 1;
for (int i = N; i > 1; --i)
{
res *= i;
}
cout << "Result is: "<< res << endl;
return res;
}
int main()
{
int x;
std::promise<int> p;
std::future<int> f = p.get_future();
std::future<int> fu = std::async(std::launch::async, factorial, std::ref(f));
p.set_value(4); //這裡必須進行set_value,否則在factorial函式的int N = f.get()這行會丟擲std::future_error::broken_promise的異常
x = fu.get();
cout << "Get from child: " << x << endl;
return 0;
}
如果需要建立多個都呼叫factorial執行緒,每個執行緒都需要一個f引數,但是std::future不能被拷貝,此時可以使用std::shared_future,它可以被拷貝,可將int factorial(std::future& f)改為int factorial(std::shared_future f),同時main函式做如下修改:
int main()
{
int x;
std::promise<int> p;
std::future<int> f = p.get_future();
std::shared_future<int> sf = f.share();
std::future<int> fu = std::async(std::launch::async, factorial, sf);
std::future<int> fu2 = std::async(std::launch::async, factorial, sf);
std::future<int> fu3 = std::async(std::launch::async, factorial, sf);
p.set_value(4);
return 0;
}
10 建立執行緒的不同方式
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
#include <future>
using namespace std;
class A
{
public:
void f(int x, char c){}
int operator()(int N){return 0;}
};
void foo(int x){}
int main()
{
A a;
std::thread t1(a, 6); //傳遞a的拷貝給子執行緒
std::thread t2(std::ref(a), 6); //傳遞a的引用給子執行緒
std::thread t3(std::move(a), 6); //從主執行緒移動a到子執行緒,a在主執行緒中不再有效
std::thread t4(A(), 6); //傳遞臨時建立的a物件給子執行緒
std::thread t5(foo, 6); //傳遞自定義的函式給子執行緒
std::thread t6([](int x){return x*x;}, 6); //傳遞lambda函式給子執行緒
std::thread t7(&A::f, a, 8, 'w'); //傳遞a的拷貝的成員函式f給子執行緒
std::thread t8(&A::f, &a, 8, 'w'); //傳遞a的地址的成員函式f給子執行緒
std::async(std::launch::async, a, 6); //上面的八種方式同樣適用於async
return 0;
}
11 Packaged_task
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
#include <future>
#include <deque>
using namespace std;
int factorial(int N)
{
int res = 1;
for (int i = N; i > 1; --i)
{
res *= i;
}
cout << "Result is: "<< res << endl;
return res;
}
std::deque<std::packaged_task<int()> > task_q;
std::mutex mu;
std::condition_variable cond;
void thread_1()
{
std::packged_task<int()> t;
{
std::unique_lock<std::mutex> locker(mu);
cond.wait(locker, []{return !task_q.empty();});
t = std::move(task_q.front());
}
t();
}
int main()
{
std::thread t1(thread_1);
std::packaged_task<int()> t(std::bind(factorial, 6)); //packaged_task只能傳遞一個引數,如果還要傳遞引數6,可以使用bind函式
std::future<int> ret = t.get_future(); //獲得與packaged_task共享狀態相關聯的future物件
{
std::unique_lock<std::mutex> locker(mu);
task_q.push_back(std::move(t));
}
cond.notify_one();
int value = ret.get(); //等待任務完成並獲取結果
t1.join()
//這兩句可以實現和packaged_task類似的功能,但是packaged_task特點是可以將一個可呼叫物件關聯到一個future變數,然後非同步獲取可呼叫物件的返回結果
//auto t = std::bind(factorial, 6);
//t();
//std::future<int> fu = std::async(factorial, 4);
x = fu.get();
std::packaged_task<int(int)> t(factorial); //以一個可呼叫物件為引數,並且可以非同步獲取該呼叫物件的返回結果
std::future<int> ret = t.get_future(); //獲得與packaged_task共享狀態相關聯的future物件
int value = ret.get(); //等待任務完成並獲取結果
}
12 時間約束
可以讓某個類物件休眠一段時間,或者到達指定的時間點才停止休眠
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
#include <future>
#include <deque>
using namespace std;
int factorial(int N)
{
int res = 1;
for (int i = N; i > 1; --i)
{
res *= i;
}
cout << "Result is: " << res << endl;
return res;
}
int main()
{
std::thread t1(factorial, 6);
std::this_thread::sleep_for(chrono::milliseconds(3)); //執行緒休眠3毫秒
chrono::steady_clock::time_point tp = chrono::steady_clock::now() + chrono::milliseconds(3); //一個靜態的時間點
std::this_thread::sleep_until(tp); //一直休眠,直到達到指定的時間點tp,才結束休眠
std::mutex mu;
std::unique_lock<std::mutex> locker(mu);
locker.try_lock_for(chrono::milliseconds(3));
locker.try_lock_until(tp);
std::condition_variable cond;
cond.wait_for(locker, chrono::milliseconds(3));
cond.wait_until(locker, tp);
std::promise<int> p;
std::future<int> f = p.get_future();
f.wait_for(chrono::milliseconds(3));
f.wait_until(tp);
}