当前位置: 首页 > news >正文

【多线程】忙等待/自旋(Busy Waiting/Spinning)

【多线程】忙等待/自旋(Busy Waiting/Spinning)

本文来自于我关于多线程系列文章。欢迎阅读、点评与交流
1.【多线程】互斥锁(Mutex)是什么?
2.【多线程】临界区(Critical Section)是什么?
3.【多线程】计算机领域中的各种锁
4.【多线程】信号量(Semaphore)是什么?
5.【多线程】信号量(Semaphore)常见的应用场景
6.【多线程】条件变量(Condition Variable)是什么?
7.【多线程】监视器(Monitor)是什么?
8.【多线程】什么是原子操作(Atomic Operation)?
9.【多线程】竞态条件(race condition)是什么?
10.【多线程】无锁数据结构(Lock-Free Data Structures)是什么?
11.【多线程】线程休眠(Thread Sleep)的底层实现
12.【多线程】多线程的底层实现
13.【多线程】读写锁(Read-Write Lock)是什么?
14.【多线程】死锁(deadlock)
15.【多线程】线程池(Thread Pool)
16.【多线程】忙等待/自旋(Busy Waiting/Spinning)
17.【多线程】屏障(Barrier)
18.【多线程硬件机制】总线锁(Bus Lock)是什么?
19.【多线程硬件机制】缓存锁(Cache Lock)是什么?

1. 什么是忙等待?

忙等待(Busy Waiting),也称为自旋(Spinning),是指一个线程在等待某个条件(如锁被释放、共享资源可用)时,不释放CPU,而是通过不断地执行循环并检查条件是否满足的一种策略。

通俗地说,就像你在等一个朋友,你不停地看表、不停地念叨“怎么还不来”,而不是先找个地方坐下来看看手机。在这个过程中,你(线程)一直处于“忙碌”的状态,消耗着精力(CPU资源)。

2. 忙等待的代码示例

下面是一个简单的C++示例,展示了忙等待的概念:

#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>std::atomic<bool> ready(false); // 一个原子布尔变量,作为条件标志void worker() {// 模拟一些准备工作std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Worker: Work done, notifying main thread.\n";ready = true; // 设置条件为真
}int main() {std::cout << "Main: Starting worker thread.\n";std::thread t(worker);// 忙等待:循环检查 ready 是否为 truestd::cout << "Main: Busy waiting...\n";while (!ready) { // 只要 ready 为 false,就空转// 循环体可以是空的,也可以有一些轻量级操作// 但线程不会进入阻塞状态}std::cout << "Main: Condition met, proceeding.\n";t.join();return 0;
}

在上面的代码中,主线程创建了一个工作线程,然后进入一个 while (!ready) 循环。这个循环会一直运行,直到工作线程将 ready 设置为 true。在此期间,主线程持续占用一个CPU核心。

3. 忙等待的优缺点

优点:
  1. 低延迟响应:这是忙等待最大的优点。一旦条件满足,等待线程可以立即检测到并继续执行,因为它一直在运行,没有发生线程上下文切换的开销。对于等待时间极短(比如几个时钟周期或几条指令的时间)的场景,这比让操作系统重新调度它要快得多。
  2. 避免上下文切换:线程不会被操作系统挂起,因此省去了保存和恢复线程上下文(寄存器、栈等)的开销。
缺点:
  1. 浪费CPU资源:这是忙等待最致命的缺点。等待的线程会持续消耗整个CPU核心的算力来做无意义的循环检查。如果一个CPU核心上只有一个这样的线程,那么该核心的利用率将接近100%,但实际工作进度为零。
  2. 可能导致优先级反转:在一些有优先级调度机制的系统中,如果低优先级的线程持有一个锁,并在CPU上自旋,而高优先级的线程正在忙等待这个锁,那么低优先级线程可能永远得不到CPU时间来释放锁,从而导致系统死锁。
  3. 影响系统整体性能:如果系统中有多个忙等待的线程,它们会疯狂争抢CPU时间,导致其他真正需要工作的线程(如处理I/O、计算等)得不到足够的资源,拖慢整个系统。

4. 忙等待的适用场景

正因为有上述严重的缺点,忙等待的使用场景非常有限,必须同时满足以下条件:

  • 等待时间极短:预期的等待时间最好小于完成两次线程上下文切换所需的时间。通常这意味着是纳秒或微秒级别。
  • 多核处理器:等待线程运行在一个独立的CPU核心上,这样它不会阻塞持有锁的那个线程(持有锁的线程需要在另一个核心上运行来释放锁)。

典型应用:自旋锁

忙等待最常见的应用就是实现自旋锁。自旋锁假设锁被占用的时间非常短,因此它通过忙等待来获取锁。

#include <atomic>class SpinLock {
private:std::atomic_flag flag = ATOMIC_FLAG_INIT;public:void lock() {// 忙等待,直到成功将 flag 从 false 设置为 truewhile (flag.test_and_set(std::memory_order_acquire)) {// 在等待时,可以插入一些提示来优化性能// 如 yield() 或 pause 指令// __builtin_ia32_pause(); // x86 的 PAUSE 指令,减少功耗和缓解超线程竞争}}void unlock() {flag.clear(std::memory_order_release);}
};

代码简析:

  • std::atomic_flag: 是 C++ 标准库中最简单的原子类型,保证是无锁的。它只有两个状态:set(true)和 clear(false)。

  • ATOMIC_FLAG_INIT: 用于初始化 atomic_flagclear 状态(即 false),表示锁最初是未被持有的。

  • flag.test_and_set(std::memory_order_acquire):

    • 操作: 这是一个原子操作。它同时完成两件事
      1. 读取 flag 的当前值。
      2. 设置 flag 的值为 true(set 状态)。
  • 内存序 std::memory_order_acquire:,确保在这个操作之后的所有读写操作都不会被重排到它之前执行。这保证了在成功获取锁之后,一定能看到之前持有锁的线程在临界区中所做的所有修改。

  • while 循环(忙等待):

    • 如果 test_and_set 返回 false:说明在调用时锁是空闲的,并且当前线程已经成功地将其设置为了 true(即获取了锁)。循环条件不成立,线程跳出循环,进入临界区。
    • 如果 test_and_set 返回 true:说明在调用时锁已经被其他线程持有(flag 已经是 true)。循环条件成立,线程会一直在这里空转,反复调用 test_and_set,直到锁被释放。
  • flag.clear(std::memory_order_release):

    • 操作: 原子地将 flag 设置回 false(clear 状态)。

    • 内存序 std::memory_order_release: 这是与 acquire 配对的另一个关键内存序。它建立了 “释放”语义,确保在这个操作之前的所有读写操作都不会被重排到它之后执行。这保证了当前线程在临界区中所做的所有修改,在释放锁之前都已经完成并对下一个获取锁的线程可见。

  • acquire 和 release 共同作用,形成了一个“同步点”,确保了临界区数据的安全传递。

  • PAUSE 指令

    • 减少功耗: 告诉 CPU 这是一个自旋循环,CPU 可以降低功耗或进入一个更优化的等待状态,而不是全力执行无意义的循环。
    • 避免内存顺序冲突: 在超线程技术中,它有助于减少处理器核心的资源争用,让另一个逻辑核心能更有效地工作。
    • 提升性能: 在某些架构下,使用 PAUSE 可以显著提高自旋锁在竞争激烈时的性能。
  • 在 C++20 中,可以使用 std::this_thread::yield() 或在编译器内置指令中使用 _mm_pause()(MSVC)/ __builtin_ia32_pause()(GCC/Clang)。

现代操作系统和库(如C++11的 std::mutex (互斥锁库))内部通常会采用自适应锁,即先自旋一小段时间,如果还拿不到锁,再转为阻塞状态,以在低延迟和资源消耗之间取得平衡。

5. 如何避免忙等待?—— 使用阻塞等待(Blocking Wait / Blocking)

在绝大多数情况下,当线程需要等待一个较长时间的条件时,应该使用阻塞等待(Blocking Wait / Blocking)。

阻塞等待是指线程在条件不满足时,主动释放CPU,进入睡眠状态(如 WAITINGBLOCKED 状态)。当条件满足时,由操作系统或其它线程将其唤醒。

实现阻塞等待的机制包括:

  • 互斥锁与条件变量
  • 信号量
  • 操作系统提供的等待/通知API(如 futex on Linux)

使用条件变量的C++示例:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>// 全局同步变量
std::mutex mtx;                    // 互斥锁,用于保护共享数据 ready
std::condition_variable cv;        // 条件变量,用于线程间通信
bool ready = false;                // 条件标志:表示工作是否完成// 工作线程函数
void worker() {// 模拟耗时工作(比如文件读取、网络请求、复杂计算等)std::this_thread::sleep_for(std::chrono::seconds(2));{// 使用 lock_guard 自动管理锁的生命周期// 在作用域内持有锁,保护对 ready 的修改std::lock_guard<std::mutex> lock(mtx);ready = true;  // 设置完成标志std::cout << "Worker: Work done, notifying all.\n";} // 作用域结束,lock_guard 析构,自动释放锁cv.notify_all(); // 通知等待的线程。注意:通知操作在锁外执行,这是优化做法,避免不必要的竞争
}int main() {std::cout << "Main: Starting worker thread.\n";std::thread t(worker);  // 创建并启动工作线程std::cout << "Main: Going to sleep (blocking wait).\n";{// 使用 unique_lock 而不是 lock_guard,因为 wait() 需要临时释放锁的能力(独占)std::unique_lock<std::mutex> lock(mtx);// 条件等待:等待 ready 变为 true// wait() 的工作流程:// 1. 检查条件(ready == true?)// 2. 如果条件不满足,则://    - 原子地释放锁//    - 将线程置于等待状态(阻塞,不消耗CPU)//    - 当被 notify_all() 唤醒时,重新获取锁//    - 再次检查条件// 3. 如果条件满足,则继续执行// 注意:使用lambda谓词可以防止"虚假唤醒"cv.wait(lock, [] { return ready;  // 返回 true 时停止等待,false 时继续等待});} // unique_lock 析构,自动释放锁std::cout << "Main: Condition met, proceeding.\n";t.join();  // 等待工作线程结束return 0;
}

在这个例子中,主线程在调用 cv.wait() 时会被阻塞,不消耗CPU。直到工作线程调用 cv.notify_all() 时,操作系统才会唤醒主线程。

总结

特性忙等待阻塞等待
CPU消耗高,持续占用CPU低,等待时不占用CPU
响应延迟极低,条件满足立即响应较高,需要上下文切换
适用场景等待时间极短的多核系统(如自旋锁)等待时间不确定或较长
对系统影响可能浪费大量资源,影响整体性能更友好,允许CPU执行其他任务

最佳实践:除非你非常确定等待时间极短且是性能关键路径,否则应优先选择阻塞等待。现代并发编程中,应多使用高级同步原语(如 std::mutex, std::condition_variable, std::semaphore),而避免手动编写忙等待循环。

http://www.dtcms.com/a/467344.html

相关文章:

  • Google 智能体设计模式:人机协同(HITL)
  • 国家小城镇建设政策网站wordpress shortcode插件
  • 云霄县建设局网站投诉文案类的网站
  • 免费发布信息的网站平台常州建设企业网站
  • 凌哥seoseo黑帽技术工具
  • 经常修改网站的关键词好不好上海人才网站
  • Python :求解蓝桥杯2023年第十四届省赛大学A组试题F
  • 中文wordpress网站模板下载失败wordpress 换主题 打开慢
  • 零基础自学英语入门教程
  • 中国建设企业银行网站首页媒体软文发布平台
  • 个人网站 logo 版权 备案 没用西安自助建站做网站
  • 网站建站 seo网站开发模合同
  • 设计类的属性
  • 网站备案关闭工业设计最好的公司
  • 一 网站建设管理基本情况专业制作网站建设
  • 建设网站方法桂林象鼻山景区简介
  • 化工行业网站设计相册制作模板
  • python——人脸关键点检测
  • 网站流量团队微商城分销平台上线
  • 网站制作团队响应式网站首页
  • 【Kubernets进阶】Kubernetes VPA (Vertical Pod Autoscaler) 详解与配置指南
  • 织梦网站底部黑链汕头市企业网站建设教程
  • 北京免费做网站成都武侯区建设局门户网站
  • Linux 中路由表的匹配规则
  • ios移动网站开发工具网易企业邮箱密码格式要求
  • 山东网站建设空间做思维导图的网站
  • 哈尔滨网站托管网页设计分为几个部分
  • SpringBoot 集成 LangChain4j RAG PostgreSQL
  • 寻找郑州网站优化公司上海自助建站
  • 瓜果蔬菜做的好的电商网站wordpress 自定义路由