C++中的线程
以前的学习笔记,有一点乱
#include<thread>
#include<iostream>
void myThread(void){
std::cout << "I Love China!" << std::endl;
}
int main() {
std::thread(myThread).join(); //主线程阻塞于此,子线程执行完毕后返回
std::cout << "main thread" << std::endl;
std::thread(myThread).detach();
}
/*
join将阻塞子线程,让主线程等待子线程执行完毕,然后两线程汇合。
若不使用join,可能导致主线程在子线程执行完之前接受而导致异常。
主线程应当等待所有子线程执行完毕后再返回
detach:主线程和子线程分离,主线程先执行结束不会影响子线程的执行,主线程并不阻塞等待
子线程会失去与主线程的联系,线程结束后由运行时库清除资源
主线程return后进程结束,被分离的子线程将无法打印信息到标准输出
*/
-
若有以下代码:
void aThread() { for (;;){ /*do something*/ } } void anotherThread() { for (;;){ /*do something*/ } } int main(){ std::thread(aTread).join();//因为线程函数是无限循环,主线程阻塞于此 std::thread(anotherThread).join();//该线程将没有机会执行 } //使用临时变量(即不指定对象名)创建thread并执行且调用join()将使得主线程阻塞等待子线程结束,若为无限循环,将永远等待,后面的代码将没有机会执行,即后面一个线程将不会执行 //正确的做法: std::thread t1(aThread); //开始执行 std::thread t2(anotherThread);//开始执行 t1.join();//主线程阻塞于此,若t1先于t2结束,主线程将继续等待t2,若反,继续等待t1,实际上也达到了等待两个线程结束的目的 t2.join(); //然而若后面还有语句,将不会被执行,因为主线程等待两个不可能结束的线程
-
joinable()函数:判断是否可以使用join或者detach,返回true或false;
-
其他创建线程的方法
1.用类,以及一个问题范例
#include<thread> #include<iostream> class TA{ public: TA(int& i):i(i){} void operator()(){ //必须有仿函数,不能带参数 std::cout << "thread operator() excute" << std::endl; std::cout << "i: " << i << std::endl; std::cout << "thread operator() exit" << std::endl; } int& i; }; int main(){ int i = 4; TA ta(i); //类为可调用对象 std::thread(ta).join(); } /* 若使用detach,主线程可能先执行完毕,而子线程将使用主线程中创建的变量i的引用,产生不可预料的后果。 另外,主线程结束后,ta对象将被销毁,但是子线程使用的是该对象的副本(可以创建拷贝构造函数验证),所以子线程依然正常执行。(类中不要使用指针、引用等) 子线程执行完成后,对象将执行析构函数,主函数return后也将执行对象的析构函数 */
2.用lambda表达式
#include<thread> #include<iostream> auto myLambdaThread = []{ std::cout << "thread excute" << std::endl; std::cout << "thread exit" << std::endl; }; //分号 int main() { std::thread(myLambdaThread).join(); }
-
要避免的陷阱
-
在使用detach情形下,若线程函数使用了按引用传递的main函数中的变量,其仍是安全的,但并不推荐
-
在使用detach情形下,若线程函数使用了main函数中的指针,主线程结束后各变量将被销毁从而产生问题,故不能给线程函数传递指针
void myThread(const int& i, const char* str){ std::cout << "i:" << i << " string:" << str << std::endl; } int main(){ char* str = "something"; int i = 4; std::thread(myThread,i,str).detach(); //i实际上是按值传递(即使是按引用传递,其实质仍是按值传递,若有copy ctor,会调用之) } //若将const char*形参替换为const string&,存在的问题是系统在隐式地将char*转换为string时,main线程可能已经结束,非法操作 //安全的做法 void myThread(const int i, const string& str){...} std::thread(myThread,i,string(str)); //既然还是要创建临时对象,为什么不放弃按引用传递而使用按值传递?
在创建线程的同时构造临时对象的方法传递参数是可行的
-
-
总结
- 若传递int这种简单的类型参数,建议使用值传递
- 如果传递类对象,避免隐式类型转换,全部在创建线程时构建临时对象,然后在 线程函数里用引用作为形参,否则(即按值)会多进行一次copy ctor。(若不使用临时对象,类实际上是在子线程中使用copy ctor构造的)
- 建议不使用detach,这样就不存在局部变量失效导致内存对内存的非法引用问题
-
class T {
public:
T(std::string& str):str(str){}
std::string str;
};
void mythread(const T& t) {//由于thread的参数皆为右值引用,故此处需要const关键字,另外,因为传入的十一个临时对象,对其的更改是无意义的,故声明为const
std::cout << t.str << std::endl << std::this_thread::get_id;
}
//实质上还是按值传递
int main()
{
std::string str("fsdafas");
T t(str);
std::thread(mythread,t).join();
std::cout << "Hello World!\n";
}
/*
可以看到。无论是按引用还是按值传递,我们都无法在线程中对main函数中的变量做出改变
*/
关于右值引用的补充
- 只能绑定到临时对象,多引用的对象将要销毁或没有其他用户使用该对象
- 使用右值引用的代码可以自由的接管所引用对象的内容
- 右值引用也是左值,用常量表达式初始化之后(必须的),可以用左值修改右值引用所引用临时对象的值,即可以用一个右值引用赋值给另一个右值引用
若需要在线程中修改局部变量的值
-
使用std::ref()
-
传递指针(智能指针)
-
#include <iostream> #include<thread> class T { public: int i; T(int i) :i(i) {} }; void mythread(T& t) { ++t.i; } int main() { std::string str("fsdafas"); T t(0); std::cout << "before thread, i = " << t.i << std::endl; std::thread(mythread, std::ref(t)).join();//std::ref() std::cout << "After thread, i = " << t.i << std::endl; }
-
#include <iostream> #include<thread> class T { public: int i; T(int i) :i(i) {} }; void mythread(std::shared_ptr<T> pt) { ++pt->i; } int main() { auto ptr = std::make_shared<T>(0); std::cout << "before thread, i = " << ptr->i << std::endl; std::thread(mythread, ptr).join(); std::cout << "After thread, i = " << ptr->i << std::endl; } /* 若为unique_ptr,传参数时需要用std::move(ptr) 但是后面对ptr的使用是非法的 */
线程id
- std::this_thread::get_id();
创建等待多个线程
#include<iostream>
#include<thread>
#include<vector>
void mythread() {
std::cout << "thred id: " << std::this_thread::get_id << std::endl;
}
int main() {
std::vector<std::thread> vthrd;
for (int i = 0; i < 10; ++i)
vthrd.push_back(std::thread(mythread));
for (auto& i : vthrd)
i.join();
}
数据共享问题
-
只读的数据是安全稳定的,不需要什么特别的手段
-
有读有写的数据
-
std::mutex mtx; std::mutex mtx1; //声明一把锁 mtx.lock(); mtx.unlock(); std::lock(mtx,mtx1); //同时获得两(多)把锁,保证不会出现死锁问题:要么获得两把锁,要么都不获得,但是要自己调用unlock() std::lock_guard<std::mutex> guard(mtx, std::adopt_lock); std::lock_guard<std::mutex> guard(mtx1, std::adopt_lock); //配合第六行使用,在离开作用域时自动解锁不用手动调用unlock(),可以用大括号指明作用域,在大括号内声明lock_guard类(千万不要忘记对象的名字,否则将是立马被销毁的临时对象,锁将立马解开),然后对共享数据进行读写。若不指定adopt_lock,则guard会尝试加锁
单例设计模式:(使用频率高)
在整个项目中,有某个/些特殊的类只能创建一个
实例:
class MyCAS{
private:
MyCAS(){}
static MyCAS *instance;//静态成员变量
public:
class GabCollector{
public: //不要忘记public关键字,否则无法调用其析构函数
~GabCollector(){
if (instance){
delete(instance);
instance = nullptr;
}
}
};
//MyCAS(const MyCAS&) = delete;
static MyCAS* getInstance(){
if (instance == nullptr){//初始化之后每次调用该函数都直接返回指向对象的指针
std::lock_guard<std::mutex> guard(mtx);//保证只被某一个线程初始化一次
if (instance == nullptr){
instance = new MyCAS(); //多个线程可能同时执行这一语句,会new出多块空间
static class GabCollector gc;//静态的,程序结束时释放且调用其析构函数
}
return instance;//虽然最终静态的instance只能指向一块内存,但两个线程执行该函数而返回的指针可能不一致
}
void test(){
std::cout << "testing" << std::endl;
}
};
MyCAS::instance = nullptr ;//在类外初始化静态变量
//main函数中
MyCAS* p = MyCAS::getInstance();//获得该对象
//由于instance是堆上分配的内存,必须手动释放,不能在mycas类析构函数释放内存
//在多线程中,
std::call_once():
-
其第二个参数是一个函数名,保证函数a()只被调用一次,具备互斥量的能力,而且效率上比互斥量消耗更少
-
call_once()需要配合标记std::once_flag的对象使用
-
call_once就是根据std::once_flag来判断是否调用,一旦成功调用,其将被置为已调用状态,后续再调用a()将不再执行(改写单例模式中的代码)
//单例设计模式 std::once_flag flag; //可以定义为成员变量 class Cas { public: static Cas* getInstance() { //为什么定义为static?:因为构造函数被设为私有,我们不能用正常的方式创建实例,而static成员函数是属于类而非具体的对象的,我们可以通过作用域运算符执行Cas::getInstance()函数从而创建一个单例对象 std::call_once(flag, createInstance); return instance; } private: Cas() {} static Cas* instance; /*其他成员*/ class Gc { public: //不要忘记写public,否则无法创建全局类Gc ~Gc() { if (instance) { delete(instance); instance = nullptr; } } }; static void createInstance() { instance = new Cas;//可能还要初始化其他变量 static Gc gc; //程序结束时将调用其析构函数从而释放instance指针 } }; Cas* Cas::instance = nullptr; //不要忘记在类外初始化静态成员变量以及初始化的格式 int main() { Cas* p = Cas::getInstance(); Cas* p1 = Cas::getInstance(); std::cout << "p = " << p << std::endl << "p1= " << p1 << std::endl; } //建议在主线程中创建单例对象,而在子线程中使用
条件变量 std::condition_variable类、wait()、notify_one
是一个和条件相关的类(非模板类),说白了就是等待一个条件达成,需要和互斥量配合使用
先定义一个unique_lock(并lock),再定义std::condition_variable类的对象之后,调用wait(),其第一个参数是unique_lock,第二个参数是可调用对象(函数,仿函数,lambda表达式),**如果第二个参数返回值是false,那么wait函数将解锁互斥量并阻塞等待,直到其他线程调用notify_one并且判断第二个参数返回true,将再次尝试加锁并返回。**如果wait()没有第二个参数,那么就把第二个参数当成false(即直接解锁互斥量并阻塞),当被notify唤醒时直接尝试加锁并返回:(请注意有第二个参数和没有第二个参数的区别,前者相当于有两个判断条件)
std::mutex mtx;
std::condition_variable cond;
std::unique_lock<std::mutex> guard(mtx);//先锁定互斥量
cond.wait(guard,[this]{ //若无此lambda表达式,视为false
if (!msgQueue.empty())
return false; //返回false,解锁互斥量并阻塞直到notify_one的调用,将再次尝试加锁(可能阻塞等待)
return true;//返回true,wait()返回(并不解锁互斥量)
});
具体实例:
std::vector<int> vec;
std::mutex mtx;
std::condition_variable cond; //全局
void test1() {
int temp;
for (;;) {
{//大括号定义unique_lock的作用域
std::unique_lock<std::mutex> guard(mtx);//先加锁
cond.wait(guard, [] { //若被通知唤醒然而第二个参数依然返回false,则继续等待
return !vec.empty();//vec为空(返回false)时将阻塞
}); //执行完此句将尝试获得锁(可能阻塞),获得后返回,往下执行
temp = vec.back();
vec.pop_back();
}//释放锁
std::cout << "Data " << temp << " popped" << std::endl;
} //end for
}
void test2() {
srand(time(NULL));
int temp;
for (;;) {
temp = 1+rand() % 10;
{//guard作用域
std::unique_lock<std::mutex> guard(mtx);//加锁
vec.push_back(temp);
}//解锁
std::cout << "Data " << temp << " pushed" << std::endl;
cond.notify_one(); //唤醒线程,此句可以放在guard作用域里面
Sleep(800); //如果对端线程并未阻塞于wait(),此次notify可能无效
}
}
int main() {
std::thread a(test1);
std::thread b(test2);
a.join();
b.join();
}
//vec中的数据可能越来越多,而来不及取出
//当wait函数没有第二个参数时,只有在有线程发出notify信号才会唤醒
//当第二个参数返回true时,不需要notify即可唤醒
//当第二个参数返回false时,即使其他线程调用notify,本线程仍然阻塞
//故不阻塞的条件是:线程被notify时,第二个参数即谓词返回true
使用unique_lock取代lock_guard(皆是类模板):
-
前者比后者灵活很多,但是效率低,资源消耗大
-
同lock_guard一样,构造时加锁,析构时解锁
-
第一个参数是互斥锁,可选的第二个参数:
-
前者可以临时解锁和加锁,减少了粒度,而后者必须离开作用域析构而解锁
-
前者可以有lock,unlock,try_lock,release等成员函数,后者没有
-
前者可以移动, 不可复制,后者不可以移动,不能复制
std::unique_lock<std::mutex> guard(mtx); auto guard2 = guard; //error auto guard2(guard); auto guard3 = std::move(guard); //ok
//示例 std::mutex mtx,mtx1; std::lock(mtx,mtx1); std::unique_lock<std::mutex> guard(mtx,std::adopt_lock); //不要忘记对象名 //(1) std::adopt_lock //表示传入的互斥量已经被lock(使用mtx.lock(),或lock(mtx1,mtx2)),不需要在unique_lock构造时加锁 //(2) std::try_to_lock //尝试用锁定互斥量,如果没能锁定成功,会立即返回,并不会阻塞。注意:调用之前不得锁住互斥量,否则会造成死锁,判断是否拿锁成功: if (guard.owns_lock()){ /*操作共享数据*/ } else { /*其他操作*/ } //(3) std::defer_lock //表示并没有给传入的mtx加锁,后面在进入临界区需要自行加锁,在离开unique_lock的作用域时,(如果未解锁)它仍然析构而释放锁。 std::unique_lock<std::mutex> guard(mtx,std::defer_lock); //此时并未给mtx上锁 defer意味延后 guard.lock(); //注意此处执行的是unique_lock的成员函数 /*处理共享数据*/ guard.unlock(); //手动unlock是为了临时解锁 /*处理非共享数据*/ guard.lock(); //注意此处执行的是unique_lock的成员函数 /*处理共享数据*/ //离开作用域自动解锁(如果没有手动解锁) //另一个成员函数 guard.try_lock() //返回true表示拿到了锁,反之则反 //另一个成员函数 guard.release();//返回unique_lock对象管理的mutex指针并释放所有权,两者不再有关系,如果释放之前已经加锁,则我们有责任为之解锁 std::unique<std::mutex> guard(mtx); std::mutex* pmtx = guard.release(); /*处理共享数据*/ pmtx->unlock();
示例(2)
std::mutex mtx; std::vector<int> v; void in() { int temp; for (;;) { std::unique_lock<std::mutex> guard(mtx, std::try_to_lock); if (guard.owns_lock()) { if (!v.empty()) { temp = v.back(); v.pop_back(); std::cout << "RRRRR " << temp << " poped" << std::endl; } } else { std::cout << "RRRRR waiting" << std::endl; Sleep(1000); } } } void out() { for (;;) { { std::unique_lock<std::mutex> guard(mtx); v.push_back(4); std::cout << "WWWWW data 4 pushed" << std::endl; } Sleep(3000); } } int main() { std::thread t1(out); std::thread t2(in); t1.join(); t2.join(); }
-
unique_lock所有权的传递 (指的是mutex的所有权)
unique_lock对象可以把自己对mutex的所有权转移给其他的unique_lock(不能复制,同unique_ptr一样,可以移动,不能赋值)
std::unique_lock<std::mutex> guard(mtx); auto guard3 = std::move(guard); //or auto guard3(std::move(guard)); //guard解除了与mtx互斥量的绑定,后者现在属于guard3 (继承了lock或unlock状态)
可以返回一个unique_lock:
std::unique_lock<std::mutex> rtnUniqueLock(std::mutex mtx) { unique_lock<std::mutex> temp(mtx); return temp; } //类比unique_ptr,从函数返回一个局部对象会导致系统生成一个临时的unique_lock对象,并调用其移动构造函数
std::async、std::future创建后台并返回值
-
std::async是一个函数模板,用来启动一个异步任务,启动之后,它返回一个std::future对象(类模板)
-
异步任务:就是自动创建一个线程并开始执行对应线程入口函数并返回一个std::future对象,此对象中保存有线程入口函数的返回值,我们通过future对象的get函数来获取该返回值
-
提供了访问异步操作结果的机制,此结果可能没有办法马上拿到,但是线程执行完毕时就能拿到
示例:
#include<future>//包含该头文件 int returnInt(int i) { std::cout << "thread ID: " << std::this_thread::get_id() << std::endl; std::chrono::milliseconds dura(i*1000); //定义i秒时间 std::this_thread::sleep_for(dura);//休息该时长 return 42; } int main() { std::cout << "main thread ID: " << std::this_thread::get_id() << std::endl; std::future<int> threadResult = std::async(returnInt,5);//将启动线程并返回线程指向结果,线程不会阻塞于此 std::cout << "main thread get " << threadResult.get() << std::endl; //但会阻塞于get函数直至返回 } //future与线程绑定 //threadResult.wait() 将等待线程返回,但并不返回值 //get函数只能调用一次 //async的线程入口是可调用对象:函数、函数指针、重载了调用运算符的类、lambda表达式: class thrd{ public: int operator()(int i) { std::chrono::milliseconds dura(i*1000); std::this_thread::sleep_for(dura); return 42; } }; thrd t; //创建一个类对象,不能直接传入类 auto res = std::async(t,3); //std::future<int> res std::cout << res.get();
我们可以额外向std::async()传递一个参数(枚举类型)来达到一些特殊的目的:
-
std::launch::async,在调用函数时就开始创建线程,默认用此标记,可以省略
-
std::launch::defered :不立即开始线程,而是延迟到std::future的wait()或者get()时才开始(如果不调用这两个函数,则线程不会被创建),然而,即使是调用 了此二函数,也是在主线程中执行的线程入口函数:
auto result = std::async(std::launch::deferred,a,3); //不会立刻调用线程入口函数 result.get()//result.wait(); 实际上在主线程中调用
-
std::packaged_task:把任务包装起来
-
是个类模板,它的模板参数是各种可调用对象。通过其把各种可调用对象(返回值类型和参数类型皆相同的)包装起来,方便未来作为线程入口函数;
-
示例:
int thrd(int i) { std::chrono::milliseconds dura(i * 1000); std::this_thread::sleep_for(dura); std::cout << std::this_thread::get_id << std::endl; return 42; } int main() { std::cout << std::this_thread::get_id << std::endl; std::packaged_task<int(int)> mypt(thrd);//int(int)表示返回值和参数皆是int std::thread t(std::ref(mypt), 1); t.join(); std::future<int> res = mypt.get_future();//得到与mypt绑定的future std::cout << "get result " << res.get();//通过future得到线程的返回结果 } //包装lambda表达式 int main() { std::cout << std::this_thread::get_id << std::endl; std::packaged_task<int(int)> mypt([](int i) { std::chrono::milliseconds dura(i * 1000); std::this_thread::sleep_for(dura); std::cout << std::this_thread::get_id << std::endl; return 42; });//int(int)表示返回值和参数皆是int std::thread t(std::ref(mypt), 1); t.join(); std::future<int> res = mypt.get_future();//得到与mypt绑定的future std::cout << "get result " << res.get();//通过future得到线程的返回结果 } //mypt可以直接调用 mypt(1); //不创建新线程,相当于程序调用 std::future<int> res = mypt.get_future(); res.get(); //packaged_task对象可以装入容器 std::vector<std::packaged_task<int(int)>> tasks; tasks.push_back(std::move(mypt));// mypt现在为空 auto iter = tasks.begin(); auto pt = std::move(*iter); tasks.erase(iter); pt(1);
std::promise
-
能够在某个线程中给他赋值,然后在其他线程中拿出来用(利用future类模板)
void thrd(std::promise<int>& temp, int calc) { //参数一定要设置为引用 ++calc; std::chrono::milliseconds dura(5000); //模拟一些耗时的过程 std::this_thread::sleep_for(dura); temp.set_value(calc);//结果保存到了promise的temp对象中 } int main() { std::promise<int> prs; //声明一个std::promise对象,保存的值的类型是int std::thread t1(thrd, std::ref(prs), 180);//似乎一定要使用std::ref t1.join(); std::future<int> res = prs.get_future(); //promise和future绑定 std::cout << "get result " << res.get() << std::endl; } //由于线程间的按引用传值实质上还是值传递,不能直接传递引用来达到相同的效果
future的其他参数
-
std::future_status :枚举类型(由future类的wait_for和wait_until成员函数返回)
std::future<int> res = std::async(myThread,42); //启动线程,绑定future类 std::future_status status = res.wait_for(std::chrono::seconds(1));//等待此线程一秒钟 if (status == std::future_status::timeout) { /*表示线程超时*/ } else if (status == std::future_status::ready) { /*表示线程执行完毕*/ } else { std::cout << "thread deferred" << std::endl; } //future的成员函数future_status wait_for(const chrono::duration<_Rep, _Per>& _Rel_time) 表示等待线程一定时间,返回future_status枚举类型:std::future_status::ready,std::future_status::timeout,std::future_status::deferred(当async指定了std::launch::deferred参数)
-
future的get()成员函数使用的是移动语义,其结果只能被获取一次,若希望多次(或多个线程都要获取)结果,可以使用std::shared_future类模板
//转换future std::future<int> result = std::async(myThread,42); std::shared_future<int> res(result.shared()); //或... res(std::move(result)); //future 的成员函数valid可以判断future里是否含有有效值(即有没有被获取) if (result.valid()) result.get(); //也可以直接使用shared_future std::shared_future<int> res = mypt.get_future();// = std::async(myThread,42);
原子操作
- 可以把原子操作理解为不需要互斥量的多线程并发编程方式,实在多线程中不会被打断的程序执行片段,原子操作比互斥量更节省资源(互斥量针对对行代码,原子操作针对一个变量)
- 原子操作一般是不可分割的操作,要么是完成的,要么是并未开始的
- 一般atomic原子操作,针对++,–,+=,&=,|=,~=是支持的。其他的可能不支持
std::atomic<int> count = 0; void test() { for (int i = 0; i < 100000; ++i) ++count; //若改为count = count+1,则结果不正确 } int main() { std::thread t1(test); std::thread t2(test); t1.join(); t2.join(); std::cout << count <<std::endl; } //output 200000
std::async()深入
- 第一个可选的参数std::launch::deferred(延迟到返回的future类调用get函数或者wait函数时调用入口函数,并不创建新线程)和std::launch::async(强制异步任务在新线程中执行);若指定参数 std::launch::async | std::launch::deferred ,意味着由系统选择两种方式,若不指定第一个参数,直接传入入口函数及参数,则以此为默认值
- std::thread()如果系统资源紧张,那么可能创建线程失败,程序可能因此崩溃,而std::async是创建一个异步任务,可以根据系统资源选择异步进行或者同步进行
- std::thread创建线程的方式,想获取其返回值并不容易。async返回一个future类以获得执行结果
-
补充
-
虚假唤醒
- condition_variable在wait的过程中,有线程可能多次调用notify_one或notify_all唤醒本线程,然而数据被(其他线程)处理而使得没有数据可供处理,使得本线程被虚假唤醒
- 为防止虚假唤醒,wait()中要有第二个参数,并且该参数应当正确判断数据是否存在
-
原子操作
std::atomic<int> count = 0; std::cout << count << std::endl; //对count的读操作是原子操作然而整个语句不是原子操作 auto atomic2(count); //error 原子模板类的赋值/拷贝操作被删除 auto atm3(count.load()); //但可以使用原子操作进行复制 atm3.store(12); //原子操作,赋值
-
线程池(待续)
- C/S模型:创建线程时的消耗太大,先行创建一个线程池,有任务时分配给空闲的线程,完成后线程挂起
- 线程的数量极限:2000个线程是极限
-