目录
引言
thread类的简单介绍
接口解读
使用范例
move的作用--将资源“夺舍”
原子性操作库(atomic)
lock_guard与unique_lock
前置知识:mutex锁(类似linux下的ptrhead_mutex_t数据)
mutex的种类
1. std::mutex
2. std::recursive_mutex
3. std::timed_mutex
4. std::recursive_timed_mutex
RAII
lock_guard
unique_lock
条件变量
引言
C++11的多线程特性,是对C++语言的一次重大补充,它使得C++在保持高性能和低级操作能力的同时,具备了更加强大的并发编程能力。在这个引言中,我们将探索C++11多线程的核心概念,包括线程的创建与管理、同步机制、数据共享与保护,以及异步操作等。这些特性不仅极大地简化了多线程程序的编写,也为C++开发者打开了一扇通往高效并发编程的大门。本文重点介绍:C++11的线程库部分,已经相对应的线程安全。
thread类的简单介绍
接口解读
thread类的第一个参数是一个可调用对象,后续的参数是可调用对象的参数
1.线程库禁止赋值拷贝、与赋值重载;但是允许移动构造!
两种构造,第二种是一种可变参数模板
在C++11中,thread
和 this_thread
是 <thread>
头文件中提供的两个不同的命名空间,它们各自包含了一系列用于多线程编程的函数和类。
以下是 thread
和 this_thread
的主要区别:
-
std::thread
:std::thread
是一个类,用于表示一个执行线程。- 它可以用来创建新的线程,并与之交互。
- 通过构造函数,可以传递一个函数或者函数对象,以及可选的参数来启动线程。
std::thread
对象可以调用join()
或detach()
方法来同步或分离线程。std::thread
类还提供了其他成员函数,如get_id()
来获取线程ID,joinable()
来检查线程是否可以加入,以及native_handle()
来获取底层实现特定的线程句柄。
-
std::this_thread
:std::this_thread
是一个命名空间,它包含了一系列函数,这些函数作用于当前执行的线程。- 它提供了如
get_id()
来获取当前线程的ID,yield()
来提示调度器当前线程愿意放弃处理器,以及sleep_for()
和sleep_until()
来使当前线程暂停执行指定的时间。 std::this_thread
中的函数通常用于控制或获取有关当前线程的信息,而不是创建或管理其他线程。
简而言之,std::thread
是用来创建和管理线程的类,而 std::this_thread
是一个包含用于操作当前执行线程的函数的命名空间。
使用范例
#include<iostream>
#include<vector>
#include<string>
#include<mutex>
#include<thread>
#include<chrono>
using namespace std;
void Print1(size_t n, const string& s, mutex& m, int& rx)
{
for (size_t i = 0; i < n; i++)
{
m.lock();
cout <<this_thread::get_id()<<s<<":" << i << endl;
++rx;
m.unlock();
this_thread::sleep_for(chrono::milliseconds(300));
}
}
int main()
{
mutex mtx;
int x = 0;
thread t1(Print1, 2, "xianchen1", ref(mtx), ref(x));
thread t2(Print1, 3, "xiancheng2", ref(mtx), ref(x));
//thread t3(t1);
cout <<"线程1:" << t1.get_id() << endl;
cout <<"线程2:"<< t2.get_id() << endl;
t1.join(); //在此处阻塞
t2.join();
cout << x << endl;
return 0;
}
注意点:
1.在线程的执行方法中,如果想要引用传参,必须要加上ref。保持引用的属性。
#include <thread>
void ThreadFunc1(int& x)
{
x += 10;
}
void ThreadFunc2(int* x)
{
*x += 10;
}
int main()
{
int a = 10;
// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
引用的是线程栈中的拷贝
thread t1(ThreadFunc1, a);
t1.join();
cout << a << endl;
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t2(ThreadFunc1, std::ref(a);
t2.join();
cout << a << endl;
// 地址的拷贝
thread t3(ThreadFunc2, &a);
t3.join();
cout << a << endl;
return 0;
}
2.chrono类提供了时间管理方法。
例二:支持移动赋值。移动构造使得可以创建线程池(最开始全是空线程)。
void Print2(size_t n, const string& s)
{
for (size_t i = 0; i < n; i++)
{
cout << this_thread::get_id() << s << ":" << i << endl;
}
}
int main()
{
size_t n;
cin >> n;
//创建n个线程执行Print(空线程)
vector<thread> vthd(n);
size_t j = 0;
for (auto& thd : vthd)
{
// 移动赋值(线程启动)
thd = thread(Print2, 10, "线程" + to_string(j++));
}
for (auto& thd : vthd)
{
thd.join();
}
thread t1(Print1, 100, 1, "我是小明");
thread t2(move(t1));
t2.join();
return 0;
}
赋值时的thread()是一个匿名对象,可以理解为将亡值。由于支持移动语义。因此可以进行“夺舍”。
直接转转不过去,但是move就可以。
thread t1(Print, 100, 1, "111");
thread t2(move(t1));
t2.join();
move的作用--将资源“夺舍”
在调用 std::move(t) 的那行代码中,t 被视为一个右值,你可以将它移动到一个新的对象中。但是,一旦移动操作完成,t 就变成了一个空的 std::thread 对象,它不再管理任何线程资源。如果在移动之后继续使用 t,可能会导致未定义行为,因为 t 可能不再指向任何有效的线程。
这个函数与上述的效果一致。
原子性操作库(atomic)
#include <atomic>
int main()
{
atomic<int> a1(0);
//atomic<int> a2(a1); // 编译失败
atomic<int> a2(0);
//a2 = a1; // 编译失败
return 0;
}
这样进行a1++,就是调用的operator++。
lock_guard与unique_lock
前置知识:mutex锁(类似linux下的ptrhead_mutex_t数据)
// mutex example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex
std::mutex mtx; // mutex for critical section
void print_block (int n, char c) {
// critical section (exclusive access to std::cout signaled by locking mtx):
mtx.lock();
for (int i=0; i<n; ++i) { std::cout << c; }
std::cout << '\n';
mtx.unlock();
}
int main ()
{
std::thread th1 (print_block,50,'*');
std::thread th2 (print_block,50,'$');
th1.join();
th2.join();
return 0;
}
mutex的种类
1. std::mutex
2. std::recursive_mutex
这是递归锁。在递归函数中,如果对互斥量上锁,需要用递归锁
3. std::timed_mutex
时间锁,支持在临界区休眠。
比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
4. std::recursive_timed_mutex
RAII
当我们对数据不好控制时,特别是两把锁,容易产生死锁,因此在锁锁在的作用域中,我们期待出作用域直接销毁。这就需要RAII。
lock_guard
只有构造和析构
unique_lock
相较于lock_guard,它最大的区别就是:
1.支持手动解锁
2.支持更多的锁类型,因此出现了这些接口
条件变量
由于存在线程同步机制,因此C++库也更新了条件变量。
行为上仍然类似于POSIX条件变量。
其中notify_one为唤醒一个线程,notify_all为全部唤醒。