文章基于C++的std库使用C++多线程,全文使用类伪代码编写。
1 C++多线程基本用法
1.1 C++提供的std::thread类
头文件:#include< thread >
使用方法:
void func1();
void func2();
int main(){
std::thread t1(func1);
std::thread t2(func2);
}
如果把创建线程的过程写在函数中,那要在函数结束之后进行detach ,不然线程就会因为函数的生命周期结束而崩溃。例如:
void func3 () {
std::thread t1(func1);
t1.detach();
}
1.2 C++11获取当前线程ID的方法
两种方式:
- 使用
std::this_thread
类的get_id
获取当前线程ID,这是一个静态方法。
在线程内部在执行获取id
thread t([]
{cout << this_thread::get_id() << endl; }
);
除了这些std::this_thread
类还有其他用法
- 使用
std::this_thread::yield()
放弃当前线程占用时间片使CPU重新调度以便其它线程执行 - 使用
std::this_thread::sleeo_for()
阻塞当前线程一段时间 - 使用
std::this_thread::sleeo_until()
阻塞当前线程直到某个时间点
- 使用
std::thread::id
的get_id
获取指定的线程ID,但是get_id返回的是一个std::thread::id的包装类型,该类型不可以被强转换成整型,所以一般采用一下步骤去做转换:- 只用
std::cout
这样的输出流做输出,或者先转换成std::ostringstream
对象,头文件#include < sstream >
- 再转换成字符串,然后转换成我们需要的整型
- 只用
std::thread t1(func1);
//获取线程的ID
std::thread::id t1_id = t1.get_id();
// 先将std::thread::id 转化成 std::ostringstream 对象
std::ostringstream oss
oss << t1_id;
// 再将 oss 转成 string
std::string str = oss.str();
// 最后转换成整型
unsigned long long threadid = std::stoll(str);
1.3 C++ 11 等待线程结果的函数
函数:std::thread
的 join
方法用来等待线程退出的方法
注意事项:使用这个函数时必须保证该线程处于运行转台,也就是等待的线程时可以join的,如果需要等待的线程已经退出,则此时调用join方法,程序就会因此崩溃。因此C++11 的线程库提供了一个 joinable 的方法来判断。
使用方法:
int main(){
std::thread t1(func1);
// 等待线程结束再执行下面的代码
if (t1.joinable){
t1.join();
}
}
2 整型变量的原子操作
举例子:在最int a = 0
;进行变量自增时 a++
其实执行的时三条汇编指令,两个线程同时操作自增的时候,会导致编译结果不对。在linux平台下必须先定义再赋值,因为linux不支持原子类型的拷贝构造。
int main(){
std::atomic<int> value;
value = 99;
value++;
}
3 C++ 11/14/17 线程同步对象
3.1 mutex 的使用
类型:std::mutex
包含头文件< mutex >
用法:
std::mutex my_mutex
void func1(){
my_mutex.lock();
/* ------省略------*/
my_mutex.unlock();
}
为了避免死锁,std::mutex.lock 和 std::mutex.unlock() 会成对使用。
3.2 封装mutex的接口
lock_guard
----c++11----基于作用域的互斥量管理unique_lock
----c++11----更加灵活的互斥量管理shared_lock
----c++14----共享互斥量管理scoped_lock
----c++17----多互斥量避免死锁的管理
以lock_guard
为例:
void func(){
std::lock_guard<std::mutex> guard(mymutex);
// 这里放被保护的资源操作
}
mymutex
是是std::mutex
类型,guard对象的构造函数会自动调用mymutex.lock
方法进行加锁,在guard
出了作用域的时候析构函数会自动调用mymutex.unlock()
进行解锁。
注意
:
mymutex
的生命周期一定要必须长与func
函数的作用域,很多人会写成这样对导致无法在多线程调用该函数时保护指定的对象。
void func(){
std::mutex mymutex;
std::lock_guard<std::mutex> guard(mymutex);
// 这里放被保护的资源操作
}
- 不要重复对
mutex
进行加锁,会导致崩溃。
3.3 std::shared_mutex
shared_mutex
在C++17才开始有的。底层实现的是读写锁,也就是说,多个线程对共享资源读且少许线程对共享资源写的情况下,shared_mutex
比mutex
效率更高。
std::shared_mutex
用法:
- 提供
lock
和unlock
分别获取写锁或解除写锁;用lock_shared
和unlock_shared
分别获取读锁和接触读锁。 - 另外,提供了
std::unique_lock
和std::shared_lock
配合使用的两个对象,这个两个对象在构造时自动对std::shared_mutex加锁,析构时自动解锁。前者对应写锁,后者对应读锁。
3.4 std::condition_variable
提供了wait
系列方法(wait, wait_for, wait_until
方法),发送条件信号时使用notify
方法(notify_one
和 notify_all
).使用std::condition_variable对象时需要绑定std::unique_lock 或者 std::lock_guard
对象。condition_variable不需要显示的初始化和销毁。
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx; // 全局互斥锁.
std::condition_variable cv; // 全局条件变量.
bool ready = false; // 全局标志位.
void do_print_id(int id)
{
std::unique_lock <std::mutex> lck(mtx);
while (!ready)
{
/*
如果标志位不为 true, 则等待...
当前线程被阻塞, 当全局标志位变为 true 之后, 线程被唤醒, 继续往下执行打印线程编号id.
*/
cv.wait(lck);
}
std::cout << "thread " << id << '\n';
}
void go()
{
std::unique_lock <std::mutex> lck(mtx);
ready = true; // 设置全局标志位为 true.
cv.notify_all(); // 唤醒所有线程.
}
int main()
{
std::thread threads[10];
// spawn 10 threads:
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(do_print_id, i);
std::cout << "10 threads ready to race...\n";
go(); // go!
for (auto & th : threads)
th.join();
return 0;
}
4 多线程使用锁的经验总结
- 减少锁的使用次数
- 明确锁的范围
- 减少锁的使用粒度,例如lock_guard用大括号包住要操作的部分
- 尽量避免使用try_lock 请求锁,以免出现活锁
5 线程局部存储thread_local
看明白了再更新。。。。。。
6 线程池设计
https://www.cnblogs.com/zhaocs/articles/13945197.html