目录
线程创建与参数传递
线程的创建
C++11中开始携带标准线程库,便于跨平台程序的移植与编写。一般情况下线程由函数进入,基本的线程创建方式如下:
#include "pch.h"
#include <iostream>
#include <string>
#include <thread> //C++11线程头文件
using namespace std;
void fun()
{
cout << "In child thread: " << std::this_thread::get_id() << endl;
}
int main()
{
thread my_thread(fun);
my_thread.join();
cout << "In main thread: " << std::this_thread::get_id()<< endl;
}
程序运行结果为:
In child thread: 14768
In main thread: 10376
其中,线程由thread my_thread(fun)启动,join()函数的目的是让主线程等待子线程执行完毕,若子线程持续执行,主线程将在join()函数处阻塞直到子线程函数执行完毕。若想在主线程退出后子线程继续执行,可以将my_thread.join()函数替换为my_thread.detach()函数,此时子线程将由系统接管,在后台执行。但由于子线程有时会使用主线程资源,故在主线程结束后子线程有时无法运行或运行出错,所以一般不推荐使用detach函数。
一般来讲,线程创建代码的()内需要是可调用对象,除函数外还有lambda表达式,类内重载的()操作符等,如下:
#include "pch.h"
#include <iostream>
#include <string>
#include <thread> //C++11线程头文件
using namespace std;
auto l = []()
{
cout << "In child thread: " << std::this_thread::get_id() << endl;
};
class A{
public:
void operator() () {
cout << "In child thread: " << std::this_thread::get_id() << endl;
}
};
int main()
{
//thread my_thread(l);
A a;
thread my_thread(a);
my_thread.join();
cout << "In main thread: " << std::this_thread::get_id()<< endl;
}
注意,使用std::thread创建的线程一定要使用join()或者detach()函数,不然程序会运行异常。
线程启动函数的参数传递
本处主要介绍基本类型参数与类对象参数的传递方式。
基本对象作为参数
基本对象的函数参数传递方式如下:
void fun(int a) {
a++;
cout << "in the child thread a is: " << a << endl;
}
int main()
{
int a = 2;
thread my_thread(fun, a);
my_thread.join();
cout << "In main thread a is: " << a << endl;
}
输出结果为:
in the child thread a is: 3
In main thread a is: 2
若我们想在线程函数内更改主线程定义的变量a的值,更改fun函数的形参为int &a,即使用变量a的引言,发现程序无法编译通过。必须改为const类型,如下:
void fun(const int &a) { //形参为(int &a)无法编译通过
//a++; //由于const关键字的存在无法对a进行改变
cout << "in the child thread a is: " << a << endl;
}
这是由于线程库为了安全考虑,强制限制了无法对线程函数的外部参数进行修改。若是仍然想使用引用的形式对外部传递参数进行修改,可使用std::ref关键字强制转换为引用类型,如下:
void fun(int &a) {
a++;
cout << "in the child thread a is: " << a << endl;
}
int main()
{
int a = 2;
thread my_thread(fun, std::ref(a));
my_thread.join();
cout << "In main thread a is: " << a << endl;
}
输出结果为:
in the child thread a is: 3
In main thread a is: 3
可以看到此时在子线程内部对a值的改变同时影响了主线程中变量a的值。
类对象作为参数
相比与基本数据类型,类对象较为复杂,在参数传递时涉及到基本构造函数,拷贝构造函数的执行时间与执行方式,假设代码如下:
class A {
public:
int m_i;
A(int i) :m_i(i) { //普通构造函数
cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
}
A(const A &a) :m_i(a.m_i) { //拷贝构造函数
cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
}
};
void fun(A a)
{
cout << "子线程执行,a = " << a.m_i << " 子线程id为: " << std::this_thread::get_id() << endl;
}
int main()
{
int i = 2;
A a(i);
thread my_thread(fun, a);
my_thread.join();
cout << "主线程id为: " << std::this_thread::get_id() << endl;
}
执行结果如下:
执行标准构造函数于线程: 9432
执行拷贝构造函数于线程: 9432
执行拷贝构造函数于线程: 16912
子线程执行,a = 2 子线程id为: 16912
主线程id为: 9432
发现与基本函数不同的是,拷贝构造函数执行了两次,即在子线程与主线程分布执行了一次。而基本的函数参数为类对象时拷贝构造函数往往仅执行一次,这就说明在线程开始时,额外执行了一次拷贝构造函数,修改代码为如下:
class A {
public:
int m_i;
A(int i) :m_i(i) { //普通构造函数
cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
}
A(const A &a) :m_i(a.m_i) { //拷贝构造函数
cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
}
};
void fun(const A &a)
{
cout << "子线程执行,a = " << a.m_i << " 子线程id为: " << std::this_thread::get_id() << endl;
}
int main()
{
int i = 2;
A a(i);
thread my_thread(fun, a);
my_thread.join();
cout << "主线程id为: " << std::this_thread::get_id() << endl;
}
函数fun的参数为类对象的引用,执行结果如下:
执行标准构造函数于线程: 8304
执行拷贝构造函数于线程: 8304
子线程执行,a = 2 子线程id为: 9176
主线程id为: 8304
可以看到使用引用后,主线程内仍然执行了一次拷贝构造函数,即子线程内的对象a还是主线程内a的一个副本,且由于const的存在无法对其进行修改。若仍想要对a副本的内容进行修改,可以使用mutable关键字,如下:
class A {
public:
mutable int m_i;
A(int i) :m_i(i) { //普通构造函数
cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
}
A(const A &a) :m_i(a.m_i) { //拷贝构造函数
cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
}
void change() const
{ m_i++; }
};
另外,与基础类型类似,可以使用std::ref关键字来进行强制引用处理,不执行拷贝构造函数,子线程可直接对主线程内对象进行修改。
类内成员函数作为线程起始函数
线程起始函数可以为类内成员函数,如下:
class A {
public:
int m_i;
A(){}
A(int i) :m_i(i) { //普通构造函数
cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
}
A(const A &a) :m_i(a.m_i) { //拷贝构造函数
cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
}
void fun(int a, int b){}
};
int main()
{
int i = 2;
int p,q = 0;
A a(i);
thread my_thread(&A::fun, &a, p, q); //类对象为引用,确保线程开始不执行类的拷贝构造函数
my_thread.join();
cout << "主线程id为: " << std::this_thread::get_id() << endl;
}
detach()函数的缺点
上文提到过,detach函数会使子线程脱离主线程,由系统接管在后台执行。然而很多场合子线程会使用主线程的资源来正常运行,特别当线程起始函数传递的参数为主线程变量的引用时,主线程结束后其内部变量将会被销毁,导致子线程也无法正确执行。另外,当线程起始函数为类成员函数时,由于使用了类对象的引用,主函数注销后此类对象内存会被释放,detach会使程序发生各种错误。实际过程中detach函数很少使用,因此本处先不做介绍。
线程锁
基本互斥锁mutex
线程锁一般用在多个线程同时访问同一数据时,特别是在需要对此数据进行修改时,不对待访问数据加锁会使程序出现错误。此处主要以下面的程序为例介绍线程锁的基本工作方式。两线程同时对一个队列内的数据进行读写并打印结果:
#include <iostream>
#include <string>
#include <thread> //C++11线程头文件
#include <queue>
using namespace std;
class Task {
public:
std::queue<int> taskQue;
void getTask()
{
for (int i = 0; i < 1000; i++)
{
taskQue.push(i);
}
}
void dealTask()
{
for(int i = 0; i < 1000; i++)
{
if (!taskQue.empty())
{
int task = taskQue.front();
cout << "task is: " << task << endl;
taskQue.pop();
}
else
{
cout << " task queue is empty..." << endl;
}
}
}
};
int main()
{
Task t;
thread pushThread(&Task::getTask, &t);
thread popThread(&Task::getTask, &t);
pushThread.join();
popThread.join();
return 0;
}
运行时发现上文的程序存在问题,这是由于两线程同时对队列内数据进行读写造成的,此种情况可以使用mutex互斥锁修改程序,通过对待访问变量加锁、解锁实现双线程程序执行,使用lock与unlock函数完成:
class Task {
public:
std::queue<int> taskQue;
std::mutex myMutex;
void getTask()
{
for (int i = 0; i < 1000; i++)
{
myMutex.lock(); //获取锁
taskQue.push(i);
myMutex.unlock(); //释放锁
}
}
void dealTask()
{
for(int i = 0; i < 1000; i++)
{
myMutex.lock(); //获取锁
if (!taskQue.empty())
{
int task = taskQue.front();
cout << "task is: " << task << endl;
taskQue.pop();
myMutex.unlock(); //释放锁
}
else
{
cout << " task queue is empty..." << endl;
myMutex.unlock();
}
}
}
};
std::lock_guard类模板
std::lock_guard类模板可以自行加锁与解锁,即代替使用mutex类的lock与unlock函数,上文中获取任务的代码可以替换为:
std::lock_guard<std::mutex> myGuard(myMutex);
//std::lock_guard<std::mutex> myGuard(myMutex, std::adopt_lock); //若互斥量已经加锁
taskQue.push(i);
这是由于myGuard类会在执行构造函数时执行lock操作,执行析构函数时执行unlock操作。另外std::lock_guard构造函数提供了std::adopt_lock参数,表示若在程序上文已经对互斥量进行加锁,此时将不再进行第二次加锁操作,仅会在执行构造函数时解锁。
死锁的发生以及解决办法
死锁一般发生在存在多个互斥量的情况下,互斥量A等待B解锁才能释放A中占有的锁,互斥量B等待A解锁才能释放B中占有的锁,如下所示:
class Task {
public:
std::queue<int> taskQue;
std::mutex myMutex1;
std::mutex myMutex2;
void getTask()
{
for (int i = 0; i < 1000; i++)
{
myMutex1.lock();
myMutex2.lock();
taskQue.push(i);
myMutex2.unlock();
myMutex1.unlock();
}
}
void dealTask()
{
for(int i = 0; i < 1000; i++)
{
//std::lock(myMutex1, myMutex2) //同时对两个互斥量进行加锁
myMutex2.lock(); //myMutex1.lock(); 解除死锁状态
myMutex1.lock(); //myMutex2.lock();
if (!taskQue.empty())
{
int task = taskQue.front();
cout << "task is: " << task << endl;
taskQue.pop();
myMutex1.unlock();
myMutex2.unlock();
}
else
{
cout << " task queue is empty..." << endl;
myMutex1.unlock();
myMutex2.unlock();
}
}
}
};
解决死锁的办法很简单,仅需改变上文中两线程内加锁的顺序一致即可,或使用std::lock同时对两个互斥量进行加锁。
std::unique_lock类
std::unique_lock类与std::lock_guard类功能类似,区别在于前者使用方式更加灵活,但相对应的执行效率较慢,std::unique_lock
类的基本用法如下:
//与std::lock_guard类功能相同
std::unique_lock<std::mutex> uniqLock(myMutex);
//默认程序上文已经对互斥量加锁
std::unique_lock<std::mutex> uniqLock(myMutex, std::adopt_lock);
//对互斥量进行加锁,若加锁失败则直接返回,不会阻塞程序
std::unique_lock<std::mutex> uniqLock(myMutex, std::try_to_lock);
uniqLock.owns_lock(); //判断是否成果获取锁,是则返回true,否则返回false
//不会进行加锁,仅在析构时解锁
std::unique_lock<std::mutex> uniqLock(myMutex, std::defer_lock);
//std::unique_lock的一些成员函数
uniqLock.lock(); //加锁
uniqLock.unlock(); //解锁;
uniqLock.release(); //释放与互斥量myMutex的关系
//转换绑定的互斥量
std::unique_lock<std::mutex> uniqLockNew(std::move(uniqLock));
条件变量
对于上面描述的基本双线程代码,程序需要不断的判断队列是否为空,一定程度上会影响算法的执行效率,可使用条件变量对其进行改造,基本的条件变量用法如下:
class Task {
public:
std::queue<int> taskQue;
std::mutex myMutex;
std::condition_variable myCond;
void getTask()
{
for (int i = 0; i < 1000; i++)
{
std::unique_lock<std::mutex> myUniqLock(myMutex);
taskQue.push(i);
myCond.notify_one();
}
}
void dealTask()
{
for(int i = 0; i < 1000; i++)
{
std::unique_lock<std::mutex> myUniqLock(myMutex);
myCond.wait(myUniqLock, [this]() {
if (!taskQue.empty()) return true;
return false;
});
int task = taskQue.front();
cout << "task is: " << task << endl;
taskQue.pop();
}
}
};
std::mutex myMutex;
std::condition_variable myCond; //创建条件变量
std::unique_lock<std::mutex> myUniqLock(myMutex); //创建独占锁,仅独占锁可以使用条件变量
myCond.wait(myUniqLock, [this]() {
if (!taskQue.empty()) return true;
return false;
}); //第一个参数为独占锁,第二个参数一般为一个可调用对象,本处使用lambda表达式
//当可调用对象返回true时,wait函数直接返回,无作用
//当可调用对象返回false时,wait函数将释放互斥量,并等待notify函数将其唤醒
//唤醒后继续检查可调用对象返回值,返回true时将尝试获取对互斥量加锁,成功后wait函数返回
//若唤醒后的可调用对象返回值依旧为false,则继续等待下一次唤醒
myCond.notify_one() //唤醒一个正在wait的线程
myCond.notify_all() //唤醒多个线程
线程函数返回值的获取
上文提到的线程入口函数均为void类型,无返回值,若想得到线程入口函数的返回值,可以使用C++11提供的std::async函数模板与std::future类模板,基本作用与使用方式如下:
std::async:是一个函数模板,启动一个异步任务,任务完成后返回一个std::future类型对象。
std::future:是一个类模板,其包含了线程入口函数所返回的结果,可以通过调用其成员函数get()来获取具体返回值。
#include "pch.h"
#include <iostream>
#include <string>
#include <thread> //C++11线程头文件
#include <mutex>
#include <future> //async与future头文件
using namespace std;
int my_thread(int a, int b)
{
cout << "this thread id is: " << std::this_thread::get_id() << endl;
return a + b;
}
int main()
{
int a = 10;
int b = 10;
std::future<int> result = std::async(my_thread, a, b); //创建异步线程
int sum = result.get(); //阻塞直到子线程执行完成,get函数仅能调用一次
//result.wait() //阻塞直到子线程执行完成,不获取返回值
cout << "this thread id is: " << std::this_thread::get_id() << " result is: "<<sum<<endl;
return 0;
}
值得注意的是,当线程函数执行时间较长时,即主线程调用get()函数时my_thread函数尚未执行完成,程序会在get()函数处阻塞,直到子线程返回。另外,get()函数仅能调用一次,无法重复调用。若不调用get函数,程序也会在主线程执行完毕之前阻塞,等待子线程结束才完全退出。
std::async函数也支持std::launch类型的参数,来控制子线程的执行状况,如下:
//延迟创建线程
std::future<int> result = std::async(std::launch::deferred my_thread, a, b);
std::launch::deferred参数的功能是是线程函数延迟调用,当future类对象执行get函数时才开始调用线程函数,值得注意的是,这时此函数为主线程调用,即没有开辟新的线程,不属于多线程执行。被延迟执行的函数需要调用result.get()才开始执行。
另一个参数是std::launch::async,其与std::async的默认调用形式相同。当两参数均被使用,即为std::launch::async | std::launch::deferred时,系统将在以上两种情况内自行选择。
std::packaged_task类模板
std::packaged_task类模板的作用是将可调用对象封装,可以使用future来调用其结果,对于上文的程序,可以改写为:
std::packaged_task<int(int, int)> mypt(my_thread);
std::thread t(std::ref(mypt), a, b);
t.join(); //必须调用join或detach
//mypt(a,b); //mypt也是一个可调用对象,可以直接使用,不开辟新的线程
std::future<int> result = mypt.get_future();
int sum = result.get();
异步线程的执行状态std::future_status
对于上文提到的基本异步程序,子线程函数在很短的时间内即可执行完成,但若子线程函数计算量较大,主线程该如何获取子线程的执行状态呢,这就需要枚举类型std::future,其基本使用方式如下:
int my_thread(int a, int b)
{
cout << "my_thread start()"<<" this thread id is: " << std::this_thread::get_id() << endl;
std::chrono::seconds dur(5);
std::this_thread::sleep_for(dur); //线程等待5秒
cout << "my_thread end()" << " this thread id is: " << std::this_thread::get_id() << endl;
return a + b;
}
int main()
{
int a = 10;
int b = 10;
std::future<int> result = std::async(my_thread, a, b);
std::future_status status = result.wait_for(std::chrono::seconds(4)); //等待子线程执行4秒,并返回状态
int sum{};
if (status == std::future_status::timeout) //等待完成后,子线程尚未执行完成
{
cout << "my_thread() time out..." << endl;
}
else if(status == std::future_status::ready) //等待完成后,子线程已经执行完成,可以获取子线程结果
{
cout << "my_thread() get ready..." << endl;
sum = result.get();
}
else if (status == std::future_status::deferred) //此状况仅在std::async函数使用std::launch::deferred参数时发生,很少使用
{
cout << "can not create new thread..." << endl;
}
cout << "main thread end()" << " this thread id is: " << std::this_thread::get_id() << " result is: "<<sum<<endl;
return 0;
}
std::shared_future模板类
上文提到过,使用std::future获取的异步线程执行函数结果仅可以使用get()函数执行一次,但当多个线程或多个函数同时需要此结果时,就行需要使用std::shared_future类,此类获取的线程执行结果可以使用get()函数进行多次调用提取。
std::future<int> result = std::async(my_thread, a, b);
std::shared_future<int> result_s1(std::move(result));
std::shared_future<int> result_s2(std::async(my_thread, a, b));
std::packaged_task<int(int, int)> mypt(my_thread);
std::thread t(std::ref(mypt), a, b);
t.join(); //必须调用join或detach
std::shared_future<int> result_s3(mypt.get_future());
原子操作std::atomic
原子操作的应用与互斥量类似,都是为了保护多线程对同一变量与代码段的访问,但相比于前者,原子操作仅能应用于单一变量,且相比于频繁的使用lock与unlock,效率较高。
std::atomic其实是一个模板类,对此类变量执行的操作都是原子操作,即当一个线程对其进行访问或者修改时,其他线程无法干扰,考虑如下情况,g_mcount++就是一个原子操作,两线程同时执行时不会发生干扰。
using namespace std;
std::atomic<int> g_mcount;
std::mutex m_mutex;
void my_thread()
{
for (int i = 0; i < 100000; i++)
{
//m_mutex.lock(); //使用原子操作后不需要再进行加锁与解锁
g_mcount++;
//m_mutex.unlock();
}
}
int main()
{
std::thread t1(my_thread);
std::thread t2(my_thread);
t1.join();
t2.join();
cout << "g_mcount is: " << g_mcount << endl;
return 0;
}
其他类型锁
除基本的类型锁外,c++11还提供了其他类型的互斥量,用于处理不同的应用场合,如下:
std::timed_mutex m_time_mutex; //提供超时功能的互斥量,无法获取互斥量时不会一直等待
std::chrono::seconds dur(5); //延迟5s的时间
m_time_mutex.try_lock_for(dur); //若获取不到互斥量时,将等待5s,否则程序直接返回false,若在5s内成果获取互斥量,返回true
m_time_mutex.try_lock_until(std::chrono::steady_clock::now()+dur); //与try_lock_for功能相似,获取锁知道系统时间为参数时间
std::recursive_mutex m_re_mutex; //可以多次重复加锁,此类型互斥量也可以封装在lock_guard或unique_lock类内。