主页 > 互联网  > 

C++-第十八章:线程相关内容

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引入的另一种概念——异常。

        它可以帮助程序员更快的定位错误。

标签:

C++-第十八章:线程相关内容由讯客互联互联网栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“C++-第十八章:线程相关内容