网站首页 > 基础教程 正文
多线程挺简单
c++11提供了方便的线程管理类std::thread,位于#include <thread>头文件中,下面是个简单的示例:
void func()
{
std::cout << "hello multi-thread! " << std::endl;
}
int main ()
{
for(int i = 0 ; i < 4; i++)
{
std::thread t(func);
t.detach();
}
return 0;
}
上面的例子中,创建了4个线程用于输出“hello multi-thread”。多线程初体验 - 多线程的创建就是这么简单。
多线程的组成
在多线程编程中,每个应用程序至少有一个进程,每个进程至少有一个主线程。除了主线程之外,可以在一个进程中创建多个线程,每个线程都有入口函数,其中主线程的入口函数就是main函数。当入口函数执行结束时,线程随之退出。在c++11中,使用std::thread类可以创建并启动一个线程,该thread对象负责管理启动的线程(执行/挂起等)。下面是使用std::thread创建线程的简单示例:
void func(int tid)
{
std::cout << "cur thread \
id is [%d] !" << tid << std::endl;
}
std::thread t(func, tid);
上面的示例中,创建了一个thread对象,就会启动一个线程(线程对象创建即启动,不许额外的操作)。与第一个示例不同的一点是,这里的入口函数需要传入一个参数,即thread构造函数的第二个参数。
入口参数的类型
std::thread类的构造函数是使用可变参数模板实现的,也就是说,可以传递任意个参数,第一个参数是线程的入口函数,而后面的若干个参数是该函数的参数。其中,入口参数的类型为可调用对象(Callable Objects),一般包含以下几种类型:
- 函数指针,即传入函数名(c类型)
- 重载了operator()运算符的类对象,即函数对象
- lambda表达式(匿名函数)
- std::function,其实上述3种类型都可以用std::function表示,不算是单独的一类
函数指针的示例不再赘述,关于lambda表达式和重载运算符以及std::function作为入口函数,下面是简单的示例:
// lambda表达式作为现成的入口函数 - 打印数字
for(int i = 0; i < 4; i++)
{
std::thread t([i]{
std::cout << "cur number is: "
<< i << std::endl;
});
t.detach();
}
上面的例子中,启动4个线程,并使用lambda表达式作为入口函数,实现数字打印的功能。
// 重载运算符的实例作为入口函数
class Test
{
public:
void operator()(int i)
{
std::cout << "cur \
number is:" << i << std::endl;
}
}
int main()
{
for (int i = 0; i < 4; i++)
{
Test tmp;
std::thread t(tmp, i);
t.detach();
}
}
把 函数对象 传入std::thread的构造函数时,要注意一个C++的语法解析错误(C++'s most vexing parse)。std::thread构造函数接受的是一个临时变量,否则就会导致语法解析错误,这是因为解释器比较“笨”,将Test()解释为了函数声明,该函数返回一个Test对象。需要说明的是,如果重载运算符有参数,则不会出现编译问题。知道原因,解决起来也就容易了,代码如下:
std::thread t(Test()); // 编译出错,可以这样做:std::thread t{Test()}; 或者 std::thread t( (Test()) );
函数对象:定义了调用操作符的类对象,当用该对象调用此操作符时,其表现形式如同普通函数调用一般,因此取名叫函数对象。
// std::function作为入口参数
void add(int i, int j) { std::cout << i+j << std::endl; }
std::function<void(int, int)> func1 = add;
std::function<void(int, int)> func2 = [](int i, int j)
{ std::cout << i+j << std::endl; }
std::thread t1(func1, num1, num2);
std::thread t2(func2, num1, num2);
线程的join或者detach
一个线程启动之后,一定要在线程对象被销毁前确定以何种方式等待线程执行结束。等待的方式有两中join或detach。
- join:线程启动之后,调用join阻塞主线程,等子线程执行结束后,继续执行主线程的指令;
- detach:线程启动之后,调用detach不会影响主线程的执行,启动的子线程在后台运行;
// 使用join阻塞主线程的例子
int main()
{
auto func = [](int num){
std::cout<<"cur num is:" <<num<<std::endl;
};
for (int i = 0; i < 4; i++)
{
std::thread t(func, i);
t.join();
}
std::cout<<"thread join finish"<<std::endl;
return 0;
}
///////输出//////////////////
// cur num is: 0
// cur num is: 1
// cur num is: 2
// cur num is: 3
// thread join finish
// 使用detach等待子线程的示例
int main()
{
auto func = [](int num){
std::cout<<"cur num is:" <<num<<std::endl;
};
for (int i = 0; i < 4; i++)
{
std::thread t(func, i);
t.join();
}
std::cout<<"thread detach finish"<<std::endl;
return 0;
}
///////输出(不唯一)/////////////
// cur num is: 0
// cur num is: 2
// thread detach finish
// cur num is: 1
// cur num is: 3
设置线程等待方式的注意事项
关于线程的detach等待方式,有个疑惑:会不会出现主线程执行结束,子线程仍在执行的情况?如果主线程退出,线程对象会随之销毁,子线程还能继续吗?
// 使用detach等待子线程
int main()
{
auto func = [](int num){
// Sleep(1000);
for (int i = 0; i < num; i++)
{
std::cout<<"cur num is:"
<<i<<std::endl;
}
};
std::thread t(func, 4);
t.join();
std::cout<<"main thread finish"<<std::endl;
return 0;
}
///////输出部分后崩溃///////
// cur num is: 0
// main thread finish
// cu
上述实验代码说明,使用detach时,的确会存在主线程结束后,子线程尚未结束的情况,且会导致程序崩溃!如果真是这样,我的疑惑更大了,稍微复杂点的程序,子线程的执行时间稍微长点就可能导致上面的情况。此时貌似只能使用join,这样的话detach是否有点鸡肋,请大神不吝赐教,不胜感激!
同时也要注意到,join也不完美:使用join的难点在于,在哪里join,不合适的join可能会导致程序串行,达不到并行的效果。如果join和detach能够结合就好了,可以吗?(no idea)
既然子线程可以设置为detach模式在后台运行,需要注意:当子线程使用了局部变量,且局部变量的作用域结束,子线程尚未结束时,如果继续使用局部变量,会出现意想不到的错误,并且这种错误很难排查。但是,线程的参数传递是:“默认的会将传递的参数以拷贝的方式复制到线程空间”,不应该出问题吧?需要自行验证。
当使用join方式等待线程结束时,需要注意:当决定以detach方式让线程在后台运行时,可以在创建thread的实例后立即调用detach,这样线程就会和thread的实例分离,即使出现了异常thread的实例被销毁,仍然能保证线程在后台运行。但线程以join方式运行时,需要在主线程的合适位置调用join方法,如果调用join前出现了异常,thread被销毁,线程就会被异常所终结。为了避免异常将线程终结,或者由于某些原因,例如线程访问了局部变量,就要保证线程一定要在函数退出前完成,就要保证要在函数退出前调用join,此时一种比较好的方法是资源获取即初始化(RAII,Resource Acquisition Is Initialization),示例如下:
class thread_guard
{
thread &t;
public :
explicit thread_guard(thread& _t) :
t(_t){}
~thread_guard()
{
if (t.joinable())
t.join();
}
thread_guard(const thread_guard&) = delete;
thread_guard& operator=(const thread_guard&) = delete;
};
void func(){
thread t([]{
cout << "Hello Multi-thread" <<endl ;
});
thread_guard g(t);
}
上面的例子中,使用了std::thread类的另一个成员函数joinable(),用于判断当前线程是否已经join。注意,无论是join还是detach方式,都只能调用一次。
线程的入口函数参数 - 引用还是传值
入口函数的参数使用值传递还是引用传递?都可以,但是如果想在线程中对参数的修改传递出来(引用),你可能有失望了。因为默认的会将传递的参数以拷贝的方式复制到线程空间,即使参数的类型是引用,此时引用的也是线程空间中的对象,而不是初始希望改变的对象。(这也导致了我前面提到的疑惑:既然线程空间维护了参数变量的副本,即使参数变量对应的变量离开作用域被销毁,也不会影响到线程空间的拷贝吧。)示例如下:
// 下面的代码据说g++编译会报错
int main() {
auto func = [](std::string& ref_str){
ref_str.assign("goodbey");
std::cout << "ref:" << ref_str << std::endl;
}
std::string orig_str = "hello";
std::thread t(func, orig_str);
t.join();
std::cout << "orig:" <<orig_str << std::endl;
return 0;
}
///////////输出//////////
// ref: goodbey
// orig: hello
如果想将更新的参数传递出来,可以在调用线程类构造函数的时候,使用std::ref()。如下面修改后的代码:
std::thread t(func,std::ref(orig_str));
// 此时输出为:
// ref: goodbey
// orig: goodbey
线程对象只能移动不可复制
线程对象之间是不能复制的,只能移动,移动的意思是,将线程的所有权在std::thread实例间进行转移,使用std::move。示例如下:
void func1(int);
std::thread t1(func1, num);
std::thread t2 = t1; // 编译错误,不可复制
std::thread t3 = std::move(t1); // 正确,将对象t1负责管理的线程转移给t2
最后附上std::thread类的成员函数:
(constructor) Construct thread (public member function )
(destructor) Thread destructor (public member function )
operator= Move-assign thread (public member function )
get_id Get thread id (public member function )
joinable Check if joinable (public member function )
join Join thread (public member function )
detach Detach thread (public member function )
swap Swap threads (public member function )
native_handle Get native handle (public member function )
hardware_concurrency [static] Detect hardware concurrency (public static member function )
猜你喜欢
- 2024-11-11 Linux下的C++ socket编程实例 linux c++ tcp
- 2024-11-11 C++11原子变量:线程安全、无锁操作的实例解析
- 2024-11-11 C++11的thread_local原理和应用范例
- 2024-11-11 知识重构-c++ : Lambda 知识重构拼音
- 2024-11-11 c++ 疑难杂症(4) std:vector c++ vector subscript out of range
- 2024-11-11 深入探索C++异步编程的奥秘 c++11异步编程
- 2024-11-11 C++ 开发中使用协程需要注意的问题
- 2024-11-11 golang极速嵌入式Linux应用开发(四)-协程与并发
- 2024-11-11 在计算机编程中,线程是指一个程序内部的执行流程
- 2024-11-11 C++ std:decay、std:bind、std:packaged_task 在模版编程的实践
- 最近发表
- 标签列表
-
- gitpush (61)
- pythonif (68)
- location.href (57)
- tail-f (57)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- css3动画 (57)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- canvasfilltext (58)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- node教程 (59)
- console.table (62)
- c++time_t (58)
- phpcookie (58)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)