目录
0.前言
由于是学习笔记,就不放在项目经验系列里了。
参考资料:
C++多线程详细讲解_千场托儿索的博客-CSDN博客_c++多线程
https://www.jianshu.com/p/5f0bc41249ad
1.线程与进程的概念
废话不多说。
进程就是程序的实体,比如你打开任务管理器,上面vscode,wegame等等就是应用,一个应用可能包含一个或多个进程,各司其职。一般而言,进程之间是分隔开的,一个崩了不影响其他的,除非进程之间存在通信,那就是多进程编程,但这样有诸如效率低,麻烦等缺点。
而线程就是轻量级的进程,区别在于线程不独立的拥有资源,依赖于创建它的进程而存在,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据。而一个进程至少拥有一个线程。
应用-进程-线程,就像公司-部门-员工吧。
2.C++11后的多线程
2.1基础使用
之前的pthread.h太麻烦,C++11后,就引入了#include<thread>,它在CMakeLists中的引用方式为
find_package(Threads REQUIRED)
target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT})
创建和使用多线程的方式如下
#include <iostream>
#include <thread>
int k=0;
void foo(int n)
{
for(int i=0;i<n;i++)
{k++;}
}
void foo_2(int n)
{
for(int i=0;i<n;i++)
{k++;}
}
int main()
{
std::thread Tr1(foo,100);
std::thread Tr2(foo_2,100);
Tr1.join();
Tr2.join();
std::cout<<k<<std::endl;
}
其中,当创建好std::thread对象时,函数foo和foo_2就已经在执行了,下面的join的意思是,如果当main()函数执行到这里,Tr1和Tr2还在执行,就等等,执行完了才往下走(很合理,线程阻塞,等待结果),detach不推荐使用。
然后这里相当于有两个线程在同时执行foo和foo_2函数的,这就提高了效率。
实际写程序,可能遇到如下报错,原因是函数名可能和某个类的成员函数名重合了,成员函数作为传参需加上类名。
error: no matching function for call to 'std::thread::thread ...
然后这里还有个线程安全的问题,即,上面两个线程同时在修改公共资源k,当你把n调大,比如100000,就会发现k不等于2n。
2.2小技巧
我现在有一段代码,如果我不想专门创建一个函数呢?没关系,可以用匿名函数
std::thread Tr([&](){some codes});
即便这个some codes有千行也可以。
第二个是,我不确定我要创建几个线程,能不能动态决定?
可以,用数组和匿名对象
std::vector<std::thread> TRs;
TRs.reserve(num);
for(int i=0;i<num;i++)
{
TRs.emplace_back(std::thread([&,=i](){some codes})); // 公共资源要么上锁,要么像i一样使用值传递
}
for(int i=0;i<num;i++)
{
TRs[i].join();
}
最后问一下,一个进程能创建多少线程?跟默认预留堆栈空间有关,linux下,大约几百到几千吧。
3.线程安全
3.1互斥锁
可以通过对资源上锁的方式,杜绝同时访问,高级的我还不懂,初级的手动锁和自动锁如下:
①手动上锁解锁
// 手动锁
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int k=0;
void foo(int n)
{
mtx.lock();
for(int i=0;i<n;i++)
{k++;}
mtx.unlock();
}
void foo_2(int n)
{
mtx.lock();
for(int i=0;i<n;i++)
{k++;}
mtx.unlock();
}
int main()
{
std::thread Tr1(foo,100);
std::thread Tr2(foo_2,100);
Tr1.join();
Tr2.join();
std::cout<<k<<std::endl;
}
②自动锁
// 自动锁
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int k=0;
void foo(int n)
{
std::lock_guard<std::mutex> lock(mtx);
for(int i=0;i<n;i++)
{k++;}
}
void foo_2(int n)
{
std::lock_guard<std::mutex> lock(mtx);
for(int i=0;i<n;i++)
{k++;}
}
int main()
{
std::thread Tr1(foo,100);
std::thread Tr2(foo_2,100);
Tr1.join();
Tr2.join();
std::cout<<k<<std::endl;
}
然后需要注意的是,我们说的同时访问指的是真的同一处,比如你定义一个公共资源结构体,两个线程同时访问同一结构体实例中的不同成员,其实是没有影响的(个人实验,不对请指教)。
3.2原子操作(atomic)
互斥锁虽然保证了安全,但性能开销挺大的,所以C++11引入了原子,即,直接把公共资源设为原子类型,它会自动保证同时只有一个线程访问修改,不需要加锁解锁,代码如下
// 原子
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
std::atomic<int> k=0;
void foo(int n)
{
for(int i=0;i<n;i++)
{k++;}
}
void foo_2(int n)
{
for(int i=0;i<n;i++)
{k++;}
}
int main()
{
std::thread Tr1(foo,100);
std::thread Tr2(foo_2,100);
Tr1.join();
Tr2.join();
std::cout<<k<<std::endl;
}
当然原子的玩法有很多,暂时没探究了,总而言之,它的性能开销比互斥锁加锁解锁小,也能保证安全。