C++-第十八章:线程相关内容
目录
第一节:thread的主要内容
1-1.创建子线程
1-2.回收子线程
1-3.获得子线程的id
1-4.获得当前线程id
1-5.子线程传引用
1-6.线程的先创建后使用
第二节:mutex的主要内容
2-1.mutex的作用
2-2.智能锁
第三节:condition_variable的主要内容
3-1.休眠线程
3-2.唤醒线程
下期预告:
第一节:thread的主要内容
C++11引入了<thread>库来管理线程,它将线程包装成一种类来管理。
1-1.创建子线程
std::thread t1(可调用对象,可变参数);
t1:这个线程的管理句柄,主线程通过它管理这个子线程。
可调用对象:这个线程创建时就会执行的函数,又叫任务。
可变参数:如果任务有参数,就在这里传入。
1-2.回收子线程
t1.join();
这行代码一般由主线程调用,而且它是阻塞的,即主线程等待子线程 t1 的任务完成之后再退出,如果不等待而主线程先退出,主线程的数据被回收。由于同一进程的线程之间的很多数据都是共享的,就会影响子线程的功能。
1-3.获得子线程的id
t1.get_id();
系统给每个线程都赋予了一个唯一的id,用来管理所有的线程,主线程可以使用上述代码获取某个子线程的id。
1-4.获得当前线程id
this_thread::get_id();
线程可以使用上述代码获取自己的id,主线程也可以获取自己的id。
1-5.子线程传引用
子线程执行的任务函数也可以传引用,除了形参的位置用引用接受外,传参数时必须用ref()括起来:
void task(int& a) { //... } int main() { int a = 1; std::thread t1(task,ref(a)); // 等待线程退出 t1.join(); return 0; }
1-6.线程的先创建后使用
线程在被创建时,如果不传入任何任务函数时是被阻塞的:
std::vector<std::thread> thrpool(10); // 创建10个线程,但不执行函数
线程不支持拷贝构造,但是支持移动构造和移动赋值,那么就可以使用具有右值属性的std::thread类进行赋值,让thrpool中的线程运行起来:
void task() { std::cout << "线程执行任务" << ",id:"<<std::this_thread::get_id() << std::endl; } int main() { std::vector<std::thread> thrpool; // 创建10个线程,但不执行函数 thrpool.resize(10); // 移动赋值 for (auto& thread : thrpool) { thread = std::thread(&task); Sleep(2); } // 等待所有线程结束 for (auto& thread : thrpool) { thread.join(); } return 0; }
第二节:mutex的主要内容
2-1.mutex的作用
mutex意为锁,它用来锁住某些共享资源,防止引发线程安全的问题,请看以下的例子:
#include <thread> #include <windows.h> int tickets = 1000; // 票数 void buyTicket() { while (true) { if (tickets > 0) { tickets--; std::cout << "线程: " << std::this_thread::get_id() << " 购买了一张票, 剩余票数: " << tickets << std::endl; } else { break; // 如果没有票了,退出循环 } } } int main() { std::thread t1(buyTicket); std::thread t2(buyTicket); std::thread t3(buyTicket); // 等待线程退出 t1.join(); t2.join(); t3.join(); return 0; }
我让3个线程抢票,当票为0时退出,按理来说每个线程抢到票后,剩余的票数应该是不同的,但是上述代码不够完善,可能会出现剩余票数为负数的情况。
因为CPU是以时间片轮转的形式运行线程,如果线程1进入 if 后,此时tickets为1,线程1还未执行 tickets-- 就被剥离CPU了,线程2判断 if 时,因为tickets还是1,线程2也会执行一次 tickets-- 。
然后线程1回来之后也会执行一次 tickets-- 。这就导致为1的tickets被执行了两次--,它的值就变成-1了。
为了避免这种情况,需要保证进入 if 的线程同时只有一个,这就需要用到mutex。
mutex是一种资源,同时只有一个线程能拥有它,其他线程就会在mutex的位置进行阻塞等待,直到拥有它的线程把mutex释放掉:
std::mutex mtx; // 初始化一个锁 void buyTicket() { while (true) { mtx.lock(); // 上锁:线程获取锁 // 检查和修改 tickets 没有同步 if (tickets > 0) { --tickets; std::cout << "线程: " << std::this_thread::get_id() << " 购买了一张票, 剩余票数: " << tickets << std::endl; } else { mtx.unlock(); // 解锁:线程释放锁 break; // 如果没有票了,退出循环 } mtx.unlock(); // 解锁:线程释放锁 } }
这样就正常了。
mutex的意思就是锁,它就像锁一样,锁住其他线程,不让它们继续执行代码,直到拥有锁的线程解锁。
2-2.智能锁
就像new空间的指针一样,如果出作用域后没有释放锁,那么其他线程就会一直等待锁,线程就不能正常退出了,所以C++引入了智能锁。
智能锁需要一个锁进行构造,构造成功后会自动上锁,出作用域它会析构,自动解锁:
std::mutex mtx; void buyTicket() { while (true) { std::unique_lock<std::mutex> lock(mtx); // 智能锁 if (tickets > 0) { --tickets; std::cout << "线程: " << std::this_thread::get_id() << " 购买了一张票, 剩余票数: " << tickets << std::endl; } else { // 出作用域自动解锁 break; } lock.unlock(); // 未出作用域,手动解锁 } }
第三节:condition_variable的主要内容
condition_variable提供了条件变量相关接口,它需要配合锁使用。
3-1.休眠线程
std::mutex mtx; std::condition_variable con; std::unique_lock<std::mutex> lock(mtx) con.wait(lock);
线程执行 con.wait(mtx) 时就会一直被休眠阻塞。
条件变量阻塞线程的原理是让持有对应锁的线程释放锁,并使之休眠,等待唤醒。
注意条件变量只允许传入智能锁(unique_lock),而不允许直接传入锁(mutex)。
3-2.唤醒线程
con.notify_one(); // 随机唤醒一个线程 con.notify_all(); // 唤醒所有线程
唤醒一个线程时,该线程直接就可以获得条件变量中的锁来执行后面的代码了,其他线程继续休眠。
唤醒所有线程后,这些线程仍然需要先竞争条件变量中的锁,竞争到锁的一个线程才能执行后面的代码,其他线程没有继续休眠,而是阻塞等待锁被释放,然后竞争锁。
wait的第二个参数还可以传入一个可调用对象,线程被唤醒时,还需要可调用对象的返回值为真时才能获得锁。
不传入默认为真。
std::mutex mtx; std::condition_variable con; bool ready = false; void worker(int id) { std::unique_lock<std::mutex> lock(mtx); con.wait(lock, [] {return ready; }); // 唤醒后仍需持有锁才能执行下面的代码 std::cout << "线程 " << id << " 被唤醒并执行。" << std::endl; } int main() { std::thread t1(worker, 1); std::thread t2(worker, 2); std::thread t3(worker, 3); // 确保所有线程都进入等待状态 std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 唤醒所有线程 std::cout << "唤醒所有线程" << std::endl; ready = true; // 设置为真 con.notify_all(); t1.join(); t2.join(); t3.join(); return 0; }
下期预告:
第十九章将讲述C++11引入的另一种概念——异常。
它可以帮助程序员更快的定位错误。