C++ 多线程 Thread(C++11)和线程锁的知识
参考文章:
https://blog.youkuaiyun.com/ouyangfushu/article/details/80199140
https://zhuanlan.zhihu.com/p/91062516
1.普通函数多线程调用:
无参数函数
#include <thread>
#include <iostream>
#include <mutex>
void hello_world(){
std::cout << "This is thread t1" << std::endl;
}
int main(){
std::thread t1(hello_world);
t1.join();
std::cout << "This is Main" << std::endl;
}
// 可以看到在线程的构造函数中,引入子线程的函数名
带参数函数
#include <thread>
#include <iostream>
#include <mutex>
void hello_world(int a, int b){
std::cout << "This is thread t1" << std::endl;
std::cout << " a + b = " << a+b << std::endl;
}
int main(){
std::thread t1(hello_world, a, b);
t1.join();
std::cout << "This is Main" << std::endl;
}
// 可以看到在线程的构造函数中,参数也是一并加入的
2.join() & detach() 的区别
join()
的作用前面已经提到,主线程等待子线程结束方可执行下一步(串行),detach()
是的子线程放飞自我,独立于主线程并发执行,主线程后续代码段无需等待。看看效果:
join( ):
join()
会阻塞主线程,直到子线程的结束。
detach( )
可以看到detach()
使得主线程并行运行,意味着会像图中这样,输出不对头。
这里需要特别说明一点,也是一开始我迷糊的地方:无论是join
还是detach
,线程开始的地方都是在线程的构造函数处,一旦生成了子线程的对象,子线程就已经开始了,join & detach
只是决定了主线程是否并行运行而已
3.数据同步,线程锁
无锁的尴尬
线程锁的作用是防止出现多个线程同时处理同一个共享数据,比如下面的情况:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>
int counter = 0;
void increase(int time) {
for (int i = 0; i < time; i++) {
// 当前线程休眠1毫秒
std::this_thread::sleep_for(std::chrono::milliseconds(1));
counter++;
}
}
int main(int argc, char** argv) {
std::thread t1(increase, 10000);
std::thread t2(increase, 10000);
t1.join();
t2.join();
std::cout << "counter:" << counter << std::endl;
return 0;
}
如果没有线程的相关概念,很容易理解为,increase两次,所以counter=20000,输出的结果是:
[root@2d129aac5cc5 demo]# ./mutex_demo1_no_mutex
counter:19997
[root@2d129aac5cc5 demo]# ./mutex_demo1_no_mutex
counter:19996
之所以出现这种情况是因为t1
和t2
可能在某一个时刻同时处理了counter,比如:假定counter当前值为10,线程1读取到了10,线程2也读取到了10,分别执行自增操作,线程1和线程2分别将自增的结果写回counter,不管写入的顺序如何,counter都会是11,但是线程1和线程2分别执行了一次自增操作,我们期望的结果是12!!
加锁的情况
为了避免上述尴尬的情况,我们定义一个std::mutex对象用于保护counter变量。对于任意一个线程,如果想访问counter,首先要进行"加锁"操作,如果加锁成功,则进行counter的读写,读写操作完成后释放锁(重要!!!); 如果“加锁”不成功,则线程阻塞,直到加锁成功。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>
int counter = 0;
std::mutex mtx; // 保护counter
void increase(int time) {
for (int i = 0; i < time; i++) {
mtx.lock();
// 当前线程休眠1毫秒
std::this_thread::sleep_for(std::chrono::milliseconds(1));
counter++;
mtx.unlock();
}
}
int main(int argc, char** argv) {
std::thread t1(increase, 10000);
std::thread t2(increase, 10000);
t1.join();
t2.join();
std::cout << "counter:" << counter << std::endl;
return 0;
}
这里再次说明:子线程并不是从join开始的,所以不会出现t1
处理完了再处理t2
的情况。也就是说当线程对象一建立,线程就开始了
简单总结一些std::mutex:
- 对于std::mutex对象,任意时刻最多允许一个线程对其进行上锁
- mtx.lock():调用该函数的线程尝试加锁。如果上锁不成功,即:其它线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生
- mtx.unlock():释放锁
- std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于:如果上锁不成功,当前线程不阻塞。
死锁了怎么解决
虽然std::mutex可以对多线程编程中的共享变量提供保护,但是直接使用std::mutex的情况并不多。因为仅使用std::mutex有时候会发生死锁。回到上边的例子,考虑这样一个情况:假设线程1上锁成功,线程2上锁等待。但是线程1上锁成功后,抛出异常并退出,没有来得及释放锁,导致线程2“永久的等待下去”(线程2:我的心在等待永远在等待……),此时就发生了死锁。给一个发生死锁的 :
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>
int counter = 0;
std::mutex mtx; // 保护counter
void increase_proxy(int time, int id) {
for (int i = 0; i < time; i++) {
mtx.lock();
// 线程1上锁成功后,抛出异常:未释放锁
if (id == 1) {
throw std::runtime_error("throw excption....");
}
// 当前线程休眠1毫秒
std::this_thread::sleep_for(std::chrono::milliseconds(1));
counter++;
mtx.unlock();
}
}
void increase(int time, int id) {
try {
increase_proxy(time, id);
}
catch (const std::exception& e){
std::cout << "id:" << id << ", " << e.what() << std::endl;
}
}
int main(int argc, char** argv) {
std::thread t1(increase, 10000, 1);
std::thread t2(increase, 10000, 2);
t1.join();
t2.join();
std::cout << "counter:" << counter << std::endl;
return 0;
}
运行结果是:
[root@2d129aac5cc5 demo]# ./mutex_demo3_dead_lock
id:1, throw excption....
程序并没有退出,而是永远的“卡”在那里了,也就是发生了死锁。
那么这种情况该怎么避免呢? 这个时候就需要std::lock_guard登场了。std::lock_guard只有构造函数和析构函数。简单的来说:当调用构造函数时,会自动调用传入的对象的lock()函数,而当调用析构函数时,自动调用unlock()函数(这就是所谓的RAII,读者可自行搜索)。
为了避免死锁,加入lock_guard:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>
int counter = 0;
std::mutex mtx; // 保护counter
void increase_proxy(int time, int id) {
for (int i = 0; i < time; i++) {
// std::lock_guard对象构造时,自动调用mtx.lock()进行上锁
// std::lock_guard对象析构时,自动调用mtx.unlock()释放锁
std::lock_guard<std::mutex> lk(mtx);
// 线程1上锁成功后,抛出异常:未释放锁
if (id == 1) {
throw std::runtime_error("throw excption....");
}
// 当前线程休眠1毫秒
std::this_thread::sleep_for(std::chrono::milliseconds(1));
counter++;
}
}
void increase(int time, int id) {
try {
increase_proxy(time, id);
}
catch (const std::exception& e){
std::cout << "id:" << id << ", " << e.what() << std::endl;
}
}
int main(int argc, char** argv) {
std::thread t1(increase, 10000, 1);
std::thread t2(increase, 10000, 2);
t1.join();
t2.join();
std::cout << "counter:" << counter << std::endl;
return 0;
}
巧妙之处在于,当发生异常的时候,lock_guard会自动调用析构函数,而释放锁的操作就在析构函数中,就很好解决了发生异常后不会释放锁的问题。
未完待续
这就是多线程和线程锁的基本知识,还有很多关于锁的知识,以后慢慢补充
补充1
没有真正运用过锁,很多概念都还存在于理所当然之中,其实理解是有错误的。
看下面的一个例子,猜一猜会不会输出:counter:2?
int counter_1 = 0;
std::mutex mtx; // 保护counter
void increase_1(int time) {
mtx.lock();
counter_1++;
//mtx.unlock();
}
void increase_2(int time) {
// mtx.lock();
counter_1++;
std::cout << "counter:" << counter_1 << std::endl;
}
int main(int argc, char** argv) {
std::thread t1(increase_1, 10000);
std::thread t2(increase_2, 10000);
t1.join();
t2.join();
return 0;
counter:2
Process finished with exit code 0
答案是不会,我错误的以为,当上锁之后,获得锁之后,被该线程使用的变量都不能再被其他线程获得。但其实不是,锁的作用更像是一个标志位,作用在线程之间,当一个线程获得锁A之后,当另外一个线程也想获得这把锁时候,发现获得不了于是一直阻塞着。因为上面线程t2
中并没有用注释掉的那行,所以正常可以使用counter
变量。当去掉注释之后,t2
将一直停在mtx.lock();
那行。
再看这样的一个例子:
int counter_1 = 0;
boost::recursive_mutex Mutex;
void increase_1(int time) {
boost::recursive_mutex::scoped_lock scopedLock_test(Mutex);
counter_1++;
}
void increase_2(int time) {
boost::recursive_mutex::scoped_lock scopedLock_test(Mutex);
counter_1++;
std::cout << "counter:" << counter_1 << std::endl;
}
int main(int argc, char** argv) {
std::thread t1(increase_1, 10000);
std::thread t2(increase_2, 10000);
t1.join();
t2.join();
return 0;
}
counter:2
Process finished with exit code 0
使用不同的锁机制,这里t2
明明也上锁了,t1
也没有解锁,但是还是获得了counter变量,这是因为这个boost::recursive_mutex::scoped_lock
锁机制不同与前面例子的简单的std::mutex
,scoped_lock
可以做到程序线程结束后自动释放锁。一般来说,越复杂的锁功能丰富但开销大。
如果上述increase_1()
中加入while(1)
则会发现始终无法输出counter :2
。
总结:
锁并不是对变量做了什么,而是在线程之间做了一个标志位:它说明了当其他线程使用某一把锁后,其他线程就只能乖乖等这把锁,线程的程序暂停直到这把锁被释放。