1.多线程的原理
同一时间内,CPU只能处理1条线程,只有1条线程在工作(执行);多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
思考:如果线程非常非常多,会发生什么情况?
CPU会在N多线程之间调度,CPU会类似,消耗大量的CPU资源;每条线程被调度执行的频次会较低(线程的执行效率减低)。
2.多线程的优缺点
优点: 能适当提高程序的执行效率;多线程代码可以最大化利用计算机性能资源,提高资源利用率(CPU、内存利用率)。
缺点:开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能;线程越多,CPU在调度线程上的开销就越大;程序设计更加复杂,例如线程之间的通信、多线程的数据共享。
首先从简单的问题入手,如何写一个多线程的C++代码?
#include<iostream>
#include<thread>
void fun(int a){
a++;
}
int main(){
int a=0;
std::thread t(fun,a); //创建一个线程t,t调用函数fun,a作为fun的参数,也要写到thread的构造函数当中;
t.join(); //启动线程t,并且阻塞主线程,等到线程t运行结束后,再继续运行主线程;
std::cout<<a<<std::endl;
}
上面这段代码是最简单的多线程代码,调用thread类,并利用了thread的构造函数创建一个线程t,thread类的构造函数重载了很多,后面会继续说到。
在这里要说一下,thread类当中的两个成员函数, join()和detach()。这两个成员的作用就像上面代码的注释那样,启动新生成的线程的,但是区别在于join()函数是启动子线程而阻塞主线程,当子线程运行结束后,才会继续运行主线程。相比之下,detach()函数的作用是启动子线程,并且让子线程和主线程分离,子线程和主线程各运行各的,虽然两个线程会因为共享内存池的原因在操作系统的层面发生发生阻塞等关系,但是在代码层次上,两个线程并不存在谁阻塞谁,很可能主线程已经运行结束了,子线程还在运行。
关于detch( )和 join()表示线程的一种状态:
在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。
一个可结合的线程 (join) 能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。
一个分离的线程 (detach) 是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放
如下:
#include <thread>
bool HelloWorld::init()
{
std::thread t1(&HelloWorld::firstThread,this);//创建一个分支线程,回调到firstThread函数里
t1.join(); //等待子线程firstThread执行完之后,主线程才可以继续执行下去,此时主线程会释放掉执行完后的子线程资源
//线程可传参
std::thread t2(&HelloWorld::secondThread,this,10,20);//创建一个分支线程,回调到secondThread函数里
t2.detach(); //将子线程从主线程里分离,子线程执行完成后会自己释放掉资源; 分离后的线程,主线程将对它没有控制权
if(t2.joinable())
t2.join();
LOG("in major thread");//在主线程
return true;
}
void HelloWorld::firstThread()
{
LOG("in first thread");//在子线程
}
void HelloWorld::secondThread(int first,int second)
{
LOG("in second thread,first = %d,second = %d",first,second);
}
#include<iostream>
#include<thread>
class A{
public:
void fun(int a,int b){
std::cout<<"this is A thread!"<<a<<std::endl;
}
};
int main(){
int k=0;
A a;
std::thread t(&A::fun,a,k,k+1);
t.join();
}
std::thread t(&A::fun,a,k,k+1);
这个地方就可以看出thread类的构造对于成员函数的重载了, std::thread t(函数(成员函数)地址,对象地址,成员函数的参数1,参数2,参数3...)。
相比非成员函数,成员函数需要给出类实例化对象的地址,如果该线程是在同一类的某一成员函数当中被构造,则直接用this关键字代替即可。
其次,我们要说一下加锁和解锁的问题。
因为我们创造的每一个线程只要在一个进程内,都是共享内存池的,这样在读写数据可能会发生混乱。
C++11提供了mutex类进行加锁和解锁。
#include<iostream>
#include<thread>
#include<mutex>
std::mutex mut;
class A{
public:
volatile int temp;
A(){
temp=0;
}
void fun(int num){
int count=10;
while(count>0){
mut.lock();
temp++;
std::cout<<"thread_"<<num<<"...temp="<<temp<<std::endl;
mut.unlock();
count--;
}
}
void thread_run(){
std::thread t1(&A::fun,this,1);
std::thread t2(&A::fun,this,2);
t1.join();
t2.join();
}
};
int main(){
A a;
a.thread_run();
}
然后,我们说一下volatile关键字。
volatile和const关键很相似,都是修饰变量的,只是二者功能不一样。
多线程下的volatile
volatile在多线程当中经常使用,当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值
如下:
volatile BOOL bStop = FALSE;
(1) 在一个线程中:
while(!bStop ) { ... }
bStop = FALSE;
return;
(2) 在另外一个线程中,要终止上面的线程循环:
bStop = TRUE;
while( bStop ); //等待上面的线程终止,如果bStop不使用volatile申明,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。
这个关键字是用来设定某个对象的存储位置在内存中,而不是寄存器中。因为一般的对象编译器可能会将其的拷贝放在寄存器中用以加快指令的执行速度
volatile 关键字发挥了它的作用。其实不只是“内嵌汇编操纵栈”这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。一般说来,volatile用在如下的几个地方:
1) 中断服务程序中修改的供其它程序检测的变量需要加volatile;
2) 多任务环境下各任务间共享的标志应该加volatile;
<atomic>(提供原子操作功能)
<thread>(线程模型封装)
<mutex>(互斥量)
<condition_variable>(条件变量)
<future>
如果需要将参数按引用传递,那要向下例所示那样,必须将参数用std::ref 或者std::ref进行封装。
#include<iostream>
#include<thread>
void function(int& a, const std::string& str) {
std::cout << a << std::endl;
a = 19;
}
int main() {
int a = 5;
std::thread p1(function, std::ref(a), "asdasd"); //可以传递任意数量的参数
p1.join();
std::cout << a << std::endl;
getchar();
return 0;
}
输出结果为
5
19
该做法涉及到内存共享 往往效率不高
//将一个线程移动成另一个线程
std::thread p2=std::move(p1);
//错误做法
//std::thread p3=p1;
过move 可以减少不必要的内存分配
在上例代码p2 = move(p1)后 p1 p2都是线程的对象
查看进程id以及查看系统硬件支持线程数
cout<< std::thread::hardware_concurrency() <<endl;
cout << std::this_thread::get_id <<endl;
cout<<t2.get_id<<endl; //t2为一个线程
同一时间内,CPU只能处理1条线程,只有1条线程在工作(执行);多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
思考:如果线程非常非常多,会发生什么情况?
CPU会在N多线程之间调度,CPU会类似,消耗大量的CPU资源;每条线程被调度执行的频次会较低(线程的执行效率减低)。
2.多线程的优缺点
优点: 能适当提高程序的执行效率;多线程代码可以最大化利用计算机性能资源,提高资源利用率(CPU、内存利用率)。
缺点:开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能;线程越多,CPU在调度线程上的开销就越大;程序设计更加复杂,例如线程之间的通信、多线程的数据共享。
首先从简单的问题入手,如何写一个多线程的C++代码?
#include<iostream>
#include<thread>
void fun(int a){
a++;
}
int main(){
int a=0;
std::thread t(fun,a); //创建一个线程t,t调用函数fun,a作为fun的参数,也要写到thread的构造函数当中;
t.join(); //启动线程t,并且阻塞主线程,等到线程t运行结束后,再继续运行主线程;
std::cout<<a<<std::endl;
}
上面这段代码是最简单的多线程代码,调用thread类,并利用了thread的构造函数创建一个线程t,thread类的构造函数重载了很多,后面会继续说到。
在这里要说一下,thread类当中的两个成员函数, join()和detach()。这两个成员的作用就像上面代码的注释那样,启动新生成的线程的,但是区别在于join()函数是启动子线程而阻塞主线程,当子线程运行结束后,才会继续运行主线程。相比之下,detach()函数的作用是启动子线程,并且让子线程和主线程分离,子线程和主线程各运行各的,虽然两个线程会因为共享内存池的原因在操作系统的层面发生发生阻塞等关系,但是在代码层次上,两个线程并不存在谁阻塞谁,很可能主线程已经运行结束了,子线程还在运行。
关于detch( )和 join()表示线程的一种状态:
在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。
一个可结合的线程 (join) 能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。
一个分离的线程 (detach) 是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放
如下:
#include <thread>
bool HelloWorld::init()
{
std::thread t1(&HelloWorld::firstThread,this);//创建一个分支线程,回调到firstThread函数里
t1.join(); //等待子线程firstThread执行完之后,主线程才可以继续执行下去,此时主线程会释放掉执行完后的子线程资源
//线程可传参
std::thread t2(&HelloWorld::secondThread,this,10,20);//创建一个分支线程,回调到secondThread函数里
t2.detach(); //将子线程从主线程里分离,子线程执行完成后会自己释放掉资源; 分离后的线程,主线程将对它没有控制权
if(t2.joinable())
t2.join();
LOG("in major thread");//在主线程
return true;
}
void HelloWorld::firstThread()
{
LOG("in first thread");//在子线程
}
void HelloWorld::secondThread(int first,int second)
{
LOG("in second thread,first = %d,second = %d",first,second);
}
#include<iostream>
#include<thread>
class A{
public:
void fun(int a,int b){
std::cout<<"this is A thread!"<<a<<std::endl;
}
};
int main(){
int k=0;
A a;
std::thread t(&A::fun,a,k,k+1);
t.join();
}
std::thread t(&A::fun,a,k,k+1);
这个地方就可以看出thread类的构造对于成员函数的重载了, std::thread t(函数(成员函数)地址,对象地址,成员函数的参数1,参数2,参数3...)。
相比非成员函数,成员函数需要给出类实例化对象的地址,如果该线程是在同一类的某一成员函数当中被构造,则直接用this关键字代替即可。
其次,我们要说一下加锁和解锁的问题。
因为我们创造的每一个线程只要在一个进程内,都是共享内存池的,这样在读写数据可能会发生混乱。
C++11提供了mutex类进行加锁和解锁。
#include<iostream>
#include<thread>
#include<mutex>
std::mutex mut;
class A{
public:
volatile int temp;
A(){
temp=0;
}
void fun(int num){
int count=10;
while(count>0){
mut.lock();
temp++;
std::cout<<"thread_"<<num<<"...temp="<<temp<<std::endl;
mut.unlock();
count--;
}
}
void thread_run(){
std::thread t1(&A::fun,this,1);
std::thread t2(&A::fun,this,2);
t1.join();
t2.join();
}
};
int main(){
A a;
a.thread_run();
}
然后,我们说一下volatile关键字。
volatile和const关键很相似,都是修饰变量的,只是二者功能不一样。
多线程下的volatile
volatile在多线程当中经常使用,当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值
如下:
volatile BOOL bStop = FALSE;
(1) 在一个线程中:
while(!bStop ) { ... }
bStop = FALSE;
return;
(2) 在另外一个线程中,要终止上面的线程循环:
bStop = TRUE;
while( bStop ); //等待上面的线程终止,如果bStop不使用volatile申明,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。
这个关键字是用来设定某个对象的存储位置在内存中,而不是寄存器中。因为一般的对象编译器可能会将其的拷贝放在寄存器中用以加快指令的执行速度
volatile 关键字发挥了它的作用。其实不只是“内嵌汇编操纵栈”这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。一般说来,volatile用在如下的几个地方:
1) 中断服务程序中修改的供其它程序检测的变量需要加volatile;
2) 多任务环境下各任务间共享的标志应该加volatile;
3) 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
<atomic>(提供原子操作功能)
<thread>(线程模型封装)
<mutex>(互斥量)
<condition_variable>(条件变量)
<future>
如果需要将参数按引用传递,那要向下例所示那样,必须将参数用std::ref 或者std::ref进行封装。
#include<iostream>
#include<thread>
void function(int& a, const std::string& str) {
std::cout << a << std::endl;
a = 19;
}
int main() {
int a = 5;
std::thread p1(function, std::ref(a), "asdasd"); //可以传递任意数量的参数
p1.join();
std::cout << a << std::endl;
getchar();
return 0;
}
输出结果为
5
19
该做法涉及到内存共享 往往效率不高
//将一个线程移动成另一个线程
std::thread p2=std::move(p1);
//错误做法
//std::thread p3=p1;
过move 可以减少不必要的内存分配
在上例代码p2 = move(p1)后 p1 p2都是线程的对象
查看进程id以及查看系统硬件支持线程数
cout<< std::thread::hardware_concurrency() <<endl;
cout << std::this_thread::get_id <<endl;
cout<<t2.get_id<<endl; //t2为一个线程