C++11多线程简单使用
开篇介绍
C++11中引入了多线程头文件<thread>
,让我们能更方便的使用多线程进行编程
void TestThread(int index)
{
std::cout << "Child Thread" << index << " id " << std::this_thread::get_id() << std::endl;
std::cout << "Child Thread" << index << "Stop" << std::endl;
}
int main()
{
std::thread newThread1(TestThread, 1);
std::thread newThread2(TestThread, 2);
std::cout << "Main Thread id " << std::this_thread::get_id() << std::endl;
std::cout << "Main Thread Stop" << std::endl;
if (newThread1.joinable())
newThread1.join();
if (newThread2.joinable())
newThread2.join();
}
众所周知,线程具有异步性,也就是说这道程序的推进的方向是不确定的,而事实也正是如此
有两句话说得好,汇总一下就是:join()
和detach()
总要调用一个,并且调用之前最好是要进行joinable()
检查
Never call join() or detach() on std::thread object with no associated executing thread
Never forget to call either join or detach on a std::thread object with associated executing thread
如果对一个子线程执行两次join()
操作,那么会抛出异常
void TestThread(int index)
{
std::cout << "Child Thread" << index << " id " << std::this_thread::get_id() << std::endl;
std::cout << "Child Thread" << index << "Stop" << std::endl;
}
int main()
{
std::thread newThread(TestThread, 1);
newThread.join();
newThread.join();
}
cppreference中提到,当joinable()
的线程被赋值或析构的时候,会调用std::terminate()
,而一般std::terminate()
意味着std::abort()
,也就是说如果对一个线程既不执行join()
操作也不执行detach()
操作,而它却被析构了,那么“it will cause the program to Terminate(终止)”
void TestThread(int index)
{
std::cout << "Child Thread" << index << " id " << std::this_thread::get_id() << std::endl;
std::cout << "Child Thread" << index << "Stop" << std::endl;
}
int main()
{
std::thread newThread(TestThread, 1);
newThread = std::thread(TestThread, 2);
std::cout << "Main Thread Sleep Begin" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "Main Thread Sleep End" << std::endl;
if (newThread.joinable())
newThread.join();
}
抛开Bug不谈,由于线程的异步性,谁也说不清是先输出Thread1还是输出Thread2
针对这个Bug,可以这么理解:线程1由对象newThread管理,当C++底层告知操作系统去创建一个新线程并给它分配一些任务后,却马上创建了一个线程2交给newThread,这样子产生了一个覆盖操作,导致线程1被析构,而它又是可结合的,所以程序被terminate
正确的做法是
int main()
{
std::thread newThread(TestThread, 1);
if (newThread.joinable())
newThread.join();
newThread = std::thread(TestThread, 2);
if (newThread.joinable())
newThread.join();
}
线程的构造
通过前文中的示范,我们了解到可以通过传入一个函数指针来给一个线程分配它的“任务”
除了函数指针外,还可以通过lambda表达式,std::function
,std::bind
,仿函数对象等来解决
struct Handle
{
void operator()(int data) { std::cout << data << std::endl; }
};
int main()
{
std::thread t(Handle(), 10);
if (t.joinable())
t.join();
}
join与detach
-
newThread.join()
操作代表当前线程将阻塞,直到newThread
完成它的任务 -
newThread.detach()
操作代表newThread
与当前线程分开(即当前线程结束销毁后并不会同时销毁newThread
),但是程序结束后newThread
仍会被终止(即使它还没运行完,也会被操作系统强行叫停,这也可能会导致资源没有正确的释放)
class TestClass
{
public:
TestClass() { cout << "create" << endl; }
TestClass(const TestClass& t) { cout << "copy" << endl; }
void print(int num)
{
for (int i = 0; i < num; i++)
cout << i << ends;
cout << endl;
}
};
void Func2()
{
cout << "start" << endl;
TestClass t;
std::thread newThread(&TestClass::print, &t, 10);
// 让当前线程等待newThread完成
newThread.join();
cout << "end" << endl;
}
int main()
{
std::thread t(Func2);
if (t.joinable())
t.join();
return 0;
}
程序的执行结果为
void repeat1000(int index, int time)
{
for (int i = 0; i < time; i++)
cout << index;
}
void detach_test()
{
thread newThread(repeat1000, 1, 100000);
newThread.detach();
newThread = thread(repeat1000, 2, 1000);
newThread.join();
}
int main()
{
{
thread t(detach_test);
t.join();
}
cout << endl << "start wait" << endl;
this_thread::sleep_for(chrono::seconds(5));
return 0;
}
我认为这是一个很好的解释join()
和detach()
的例子,下面来分析一下
- 首先创建了线程
t
,给它分配了detach_test()
任务 - 主线程阻塞 等待
t
完成它的任务 - 同时操作系统完成了对
t
的分配,开始执行detach_test()
-
t
进程再创建子线程newThread
,并给他分配(repeat1000, 1, 100000)
任务 - 操作系统在分配
newThread
的“同时”,t
线程将其和newThread
分离 - 此时
(repeat1000, 1, 100000)
仍然在运行,同时t
线程给newThread
赋了一个新线程,任务为(repeat1000, 2, 1000)
-
newThread
调用join()
操作,t
进程阻塞,等待newThread
的(repeat1000, 2, 1000)
工作完成 -
newThread
的(repeat1000, 2, 1000)
完成,也意味着t
进程的任务完成,此时主线程不再受到阻塞 -
t
线程离开作用域,遭到销毁。但此时(repeat1000, 1, 100000)
操作因为比较耗时所以仍然还在运行 - 主程序输出"start wait"后开始休眠5秒钟,此时线程
(repeat1000, 1, 100000)
仍然在运行 - 最后程序结束,各种线程被操作系统销毁(即可能
(repeat1000, 1, 100000)
执行到第八万次的时候就突然被终止了)
在一般情况下,为了避免detach
或join
使用不当造成的程序错误,可以创建一个线程类,使用析构函数执行线程的分离或合并(join
)
线程传参时应该避免的操作
应该避免传入栈上对象的指针
void newThreadCallback(int* p)
{
std::cout << "Inside Thread: " << *p << std::endl;
// 等一秒钟 让startNewThread()执行结束 使i的内存空间被回收
std::this_thread::sleep_for(std::chrono::seconds(1));
// 抛出异常
*p = 19;
}
void startNewThread()
{
int i = 10;
std::cout << "Inside Main Thread: " << i << std::endl;
std::thread t(newThreadCallback, &i);
t.detach();
std::cout << "Inside Main Thread: " << i << std::endl;
}
int main()
{
startNewThread();
// 等两秒钟 让所有线程和方法都执行完毕再结束程序
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
堆上的数据同理
因为一般堆对象需要使用delete
来销毁,所以也无法确定别的线程访问到指针的时候,它所指的内存是否有效
线程的引用传参
使用std::ref
或者std::cref
,使用方法几乎和std::bind
中使一致的,所以不再赘述
多线程中的竞争
操作系统应该学过,竞争就是多个线程同时访问一块内存区域,导致不可预估的结果
class Wallet
{
int money;
public:
Wallet() : money(0) {}
int getMoney() const { return money; }
void addMoney(int increase)
{
for (int i = 0; i < increase; ++i)
money++;
}
};
int testMultiThreadWallet()
{
Wallet walletObject;
std::vector<std::thread> threads;
// 创建五条线程异步访问Wallet "理应"得到的结果为5000
threads.reserve(5);
// reserve对应_back而resize对应[i]
for (int i = 0; i < 5; ++i)
threads.emplace_back(&Wallet::addMoney, &walletObject, 1000);
// 等所有线程执行完再结束
for (auto& thread : threads)
thread.join();
return walletObject.getMoney();
}
int main()
{
int val = 0;
for (int k = 0; k < 1000; k++)
{
if ((val = testMultiThreadWallet()) != 5000)
std::cout << "Error at count = " << k << " Money in Wallet = " << val << std::endl;
}
return 0;
}
某次程序执行的结果为,这种结果是不确定的,可能1000次实验,一次都不会出错,也可能出现多至十多次错误
这一行短短的代码其实发生了三件事
money++;
- 将
money
的值加载进寄存器中 - 在寄存器中进行计算,即++操作
- 将寄存器中的结果存回
money
所在的内存中
而当由于线程具有异步性,在不加锁的情况下我们无法控制多条线程对money
的访问顺序,那么就可能出现以下这种情况
- 假设money的初始值是43
- money的值被线程1取出放入寄存器1中
- money的值被线程2取出放入寄存器2中
- 寄存器1进行计算得到结果44
- 寄存器2进行计算得到结果44
- 寄存器1将结果放入money所在内存中,money为44
- 寄存器2将结果放入money所在内存中,money为44
解决线程中的竞争
操作系统中的PV操作与之类似,关键就是加锁
std::mutex
使用互斥锁解锁上文中的钱包问题
#include<mutex>
class Wallet
{
int money;
mutex mutexLock;
public:
Wallet() : money(0) {}
int getMoney() const { return money; }
void addMoney(int increase)
{
mutexLock.lock();
for (int i = 0; i < increase; ++i)
money++;
mutexLock.unlock();
}
};
但是如果一个线程在加锁后并没有解锁,那么所有其他线程将会一直等待,导致程序无法结束(当使用join
的情况下)
相反的,如果没上锁就解锁
因此可以在此基础上做一层简单的封装
class SmartMutex
{
private:
mutex mutexLock;
public:
void AutoLock(const function<void()>& func)
{
mutexLock.lock();
func();
mutexLock.unlock();
}
};
class Wallet
{
int money;
SmartMutex smartMutex;
public:
Wallet() : money(0) {}
int getMoney() const { return money; }
void addMoney(int increase)
{
smartMutex.AutoLock([this, &increase]()
{
for (int i = 0; i < increase; ++i)
this->money++;
});
}
};
或者可以使用std::lock_guard
std::lock_guard
std::lock_guard
有点像一个智能指针,在它的作用域结束后,会调用析构函数,完成对互斥锁的解锁操作
class Wallet
{
int money;
mutex mutexLock;
public:
Wallet() : money(0) {}
int getMoney() const { return money; }
void addMoney(int increase)
{
// 在lockGuard的构造函数中会自动上锁
lock_guard<mutex> lockGuard(mutexLock);
for (int i = 0; i < increase; ++i)
this->money++;
// 离开作用域 lockGuard析构函数调用 自动解锁
}
};