C++11提供了线程库,下面我们来看一下如何使用。
线程的创建
头文件
要创建一个线程需要包一个线程头文件:#include <thread>
我们先来看看thread支持的构造方式。
支持默认构造,直接使用thread创建一个空的线程对象。
也支持带参的构造,参数就是可执行的对象,可以使函数指针、仿函数、lamdba表达式、包装器,参数是可执行对象需要传的参数,因为是可变参数,因此根据实际的可执行对象传递参数个数即可。
线程是不支持拷贝构造的,不可以用一个线程对象拷贝另一个线程对象,但是支持移动构造。
无参构造 + 移动赋值
我们创建线程的时候可以先不给该线程关联函数,创建一个空线程,等到后续有需要的时候在关联函数,比如实现线程池就可以这样做,线程池的线程是不知道要执行啥函数的。
直接使用 thread 这个类型,thread 是一个类,里面实现了线程的各种方法,使用thread然后后面跟上变量名,这样创建的线程是没有启动的线程,还没有给该线程关联要执行的函数,可以使用移动赋值来给该线程关联要执行的函数,使用匿名构造,第一个参数传递要执行的函数,后续的参数是函数要传递的参数,如果函数有一个参数就传递1个有两个参数就传递2个。线程的构造函数是一个函数对象和可变参数列表。
我们创建好线程之后需要进行等待,直接使用thread的join函数即可,该函数没有返回值和参数。
get_id()可以获取线程的id,这个函数在this_thread这个命名空间里。因此使用时需要使用这个空间域。
cpp
#include <iostream>
#include <thread>
using namespace std;
void func(int x)
{
cout << this_thread::get_id() << endl;
for(int i = 1; i <= x; i++)
cout << "music" << endl;
}
int main()
{
// 无参构造 + 移动赋值
thread t1; // 没启动的线程
t1 = thread(func, 9);
// 线程等待
t1.join();
return 0;
}
总结:
1.带参构造,创建可执行对象
2.创建空线程对象,移动构造或者移动赋值,把右值线程对象转移过去
带参构造
第二种方式是直接在创建线程的时候就给该线程关联上要执行的函数
cpp
#include <iostream>
#include <thread>
using namespace std;
void func(int x)
{
cout << this_thread::get_id() << endl;
for(int i = 1; i <= x; i++)
cout << "music" << endl;
}
int main()
{
// 2.带参构造
thread t2(func, 9);
// 线程等待
t2.join();
return 0;
}
移动构造
第三种方式是使用移动构造
cpp
#include <iostream>
#include <thread>
using namespace std;
void func(int x)
{
cout << this_thread::get_id() << endl;
for(int i = 1; i <= x; i++)
cout << "music" << endl;
}
int main()
{
// 3.移动构造
thread t3 = thread(func, 9);
// 线程等待
t3.join();
return 0;
}
锁
ref
如果我们现在要使用两个线程对一个局部变量进行++操作,那么我们应该如何写代码呢?我们肯定是创建两个线程,然后把该局部变量以引用的方式传递过去,这样线程执行的函数就会修改我们传递的局部变量,这里当然是有线程安全问题需要加锁,但我们先不考虑加锁,我们看下面的代码。
cpp
#include <iostream>
#include <thread>
using namespace std;
void add(int& x)
{
for (int i = 0; i < 100; i++)
{
x++;
}
}
int main()
{
int num = 0;
// 错误写法
thread t1(add, num);// 这样写会报错,看似是引用的方式接收,但实际上接收的是num的拷贝
// 正确写法
thread t1(add, ref(num)); // 如果是引用方式接收,需要使用ref转一下,这样写才正确
// 错误写法
thread t2(add, num);// 这样写会报错,看似是引用的方式接收,但实际上接收的是num的拷贝
// 正确写法
thread t2(add, ref(num));// 如果是引用方式接收,需要使用ref转一下,这样写才正确
this_thread::sleep_for(chrono::seconds(2));
cout << num << endl;
t1.join();
t2.join();
return 0;
}
上面的代码看上去是正确的,但是实际我们编译的时候发现编译不过去,原因在于add的参数是引用的方式接收的,但是由于num是先传给了t1的构造函数,然后在给add传递过去,相当于不是直接传过去,而是中间转了一层,所以这里看似是传递的num但实际传递的是num的拷贝,因此如果要传递num的引用,需要加上ref()。
休眠可以使用this_thread::sleep_for这个函数,chrono是时钟的意思,seconds是按秒休眠,也可以按毫秒milliseconds休眠。
mutex
这里毫无疑问是有线程安全的问题的,因此是需要加锁的。
要使用锁需要包含头文件,#include <mutex>
然后直接就可以使用mutex定义一把锁,如果要加锁 调用lock 方法,解锁 调用unlock方法即可。
此时的代码就可以这样更改。
cpp
void add(mutex& mtx, int& x)
{
for (int i = 0; i < 100; i++)
{
mtx.lock();
x++;
mtx.unlock();
}
}
int main()
{
int num = 0;
mutex mt; //定义一把锁
thread t1(add, ref(mt), ref(num));
thread t2(add, ref(mt), ref(num));
this_thread::sleep_for(chrono::seconds(2));
cout << num << endl;
t1.join();
t2.join();
return 0;
}
unique_lock和lock_guard
但是有的时候我们可能加锁的时候忘记解锁了,就会导致死锁,那么我们可以使用智能指针把锁管理起来,创建锁的时候直接加锁,出了作用域变量销毁在析构的时候直接解锁,此时我们就不需要手动的加锁解锁了,那么我们要自己实现吗?C++已经给我们提供了对锁进行管理的类,unique_lock和lock_guard,我们直接使用即可。
我们先来看unique_lock。
unique_lock是一个模板类,是专门用来管理所的类,类型直接传递锁的类型mutex即可,然后构造的时候把锁传递过去即可,一下是他的一些构造函数。
因此我们在线程函数里可以使用unique_lock,对传递过来的锁进行管理。
cpp
void add(mutex& mtx, int& x)
{
for (int i = 0; i < 100; i++)
{
unique_lock<mutex> lock(mtx);
x++;
}
}
lock_guard和unique_lock的使用方法是一样的。
cpp
void add(mutex& mtx, int& x)
{
for (int i = 0; i < 100; i++)
{
//unique_lock<mutex> lock(mtx);
lock_guard<mutex> lock(mtx);
x++;
}
}
lock_guard相比unique_lock来说更轻量一些,unique_lock则更加灵活,可以支持延迟锁定、尝试锁定等,但开销会比较大。这两个都是基于RAII的,头文件都是<mutex>,它们两个可以用来配合条件变量。
atomic
我们加锁是因为对一个数++操作不是原子的,C++给我们们提供了atomic这个类,我们可以直接使用这个类定义变量,定义的变量++和--操作是原子的操作,此时就可以不需要加锁。
使用非常简单,atomic是一个类,如果要定义一个int类型的变量,直接模版参数传递int即可,比如说atomic<int> nums, 此时对nums++和--就是原子的操作,也可以对他进行打印和赋值,使用起来和int类型是一样的。
cpp
void add(mutex& mtx, atomic<int>& x)
{
for (int i = 0; i < 100; i++)
{
x++;
}
}
int main()
{
atomic<int> num = 0;
mutex mt;
thread t1(add, ref(mt), ref(num));
thread t2(add, ref(mt), ref(num));
this_thread::sleep_for(chrono::seconds(2));
cout << num << endl;
t1.join();
t2.join();
return 0;
}
条件变量
头文件:#include <condition_variable>
要使用条件变量需要包含上面的头文件,创建条件变量直接使用condition_variable创建一个变量即可。
wait方法:将该线程加入到阻塞队列中
我们可以直接调用wait就可以将线程加入到阻塞队列,传递的参数是一个RAII的锁,也就是unique_lock管理的锁,加入到阻塞队列后锁会被释放。
唤醒线程:将在阻塞队列的函数唤醒
唤醒线程有两个,一个是唤醒阻塞队列中的全部线程,一个是唤醒阻塞队列中的一个线程。这两个唤醒函数都不需要传递参数。
接下来我们用条件变量来实现一个两个线程交替打印奇偶数
两个线程交替打印奇偶数
首先要创建两个线程,当然也需要创建锁和条件变量,也需要一个变量number,number初始化为1,我们规定,让线程1先来打印number,线程1打印完毕后对number进行+1操作,此时number就变成了偶数,然后让线程2打印number,此时打印的就是偶数,然后+1,让线程1打印,如此交替执行即可。
既然要让线程1先打印,那么线程2一定要被阻塞住,否则是无法保证打印顺序的。一开始线程1打印,然后线程2被阻塞,线程1打印完毕后要唤醒线程2去打印,然后把自己阻塞住,线程2打印完毕后唤醒线程1,然后让自己阻塞,这样就可以控制打印顺序。
那么我们如何控制谁先打印和阻塞?我们可以使用一个flag标记。
刚开始flag是假,线程1的判断条件是while(flag),因此,一开始线程1不会被阻塞会直接打印,而线程2条件是while(!flag),会直接进入阻塞队列阻塞,当线程1打印完后,把flag置为true,然后唤醒线程2,线程2的判断条件不成立不会进入阻塞队列会打印线程1,然而线程1条件成立会进入阻塞队列,线程2打印完毕后把flag置为false,自己会进入阻塞队列,而线程1条件不成立会直接打印,此时就完成了打印顺序的控制。
当然我们也可以不需要加锁,用atomic创建变量num即可。
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <condition_variable>
using namespace std;
int main()
{
int num = 1;
bool flag = false;
mutex mtx; // 锁
condition_variable cond; // 条件变量
thread t1([&]() {
for (int i = 0; i < 50; ++i)
{
unique_lock<mutex> lock(mtx);
while (flag)
cond.wait(lock);
cout << "线程1:" << num << endl;
num++;
flag = true;
cond.notify_one();
}
});
thread t2([&]() {
for (int i = 0; i < 50; i++)
{
unique_lock<mutex> lock(mtx);
while (!flag)
cond.wait(lock);
cout << "线程2:" << num << endl;
num++;
flag = false;
cond.notify_one();
}
//std::this_thread::sleep_for(std::chrono::seconds(1));
});
t1.join();
t2.join();
return 0;
}