【多线程】阻塞等待(Blocking Wait)(以C++为例)

【多线程】阻塞等待(Blocking Wait)(以C++为例)

本文来自于我关于多线程的系列文章。欢迎阅读、点评与交流

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.【多线程】阻塞等待(Blocking Wait)(以Java为例)

18.【多线程】阻塞等待(Blocking Wait)(以C++为例)

19.【多线程】屏障(Barrier)

20.【多线程硬件机制】总线锁(Bus Lock)是什么?

21.【多线程硬件机制】缓存锁(Cache Lock)是什么?

阻塞等待(Blocking Wait) 是一个并发编程中的核心概念之一。

1. 什么是阻塞等待?

阻塞等待指的是一个线程在执行过程中,由于某些条件暂时不满足(例如等待I/O操作完成、等待获取锁、等待另一个线程的结果等),而主动或被动地暂停自己的执行,让出CPU资源,进入一种"休眠"状态。直到它所等待的条件被满足后,才会被唤醒,重新进入就绪状态,等待CPU调度继续执行。

简单来说就是:线程停下来,等某个事情发生。

2. 为什么需要阻塞等待?

如果没有阻塞等待机制,线程在条件不满足时只能不停地循环检查(即"忙等待"或"自旋"),这会白白浪费宝贵的CPU时间片。

对比一下:

  • 阻塞等待:

    • 线程:"锁还没释放?那我先睡了,锁释放了记得叫醒我。"
    • 优点: 不占用CPU,节能高效。
    • 缺点: 线程切换会带来一定的上下文切换开销。
  • 忙等待:

    • 线程:"锁还没释放?我查一下...还没...我再查一下...还没..."
    • 优点: 响应及时,一旦条件满足可立即继续。
    • 缺点: 持续占用CPU,浪费资源,可能导致性能问题。

在绝大多数应用场景下,阻塞等待是更优的选择,因为CPU时间是宝贵的,应该留给真正需要计算的线程。

3. 常见的阻塞等待场景(附C++代码示例)

以下是一些在C++中典型的会导致线程阻塞等待的情况。

场景一:互斥锁(std::mutex)

当线程尝试获取已被其他线程持有的互斥锁时,会发生阻塞等待。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex g_mutex;

void worker(int id) {
    std::lock_guard<std::mutex> lock(g_mutex); // 尝试获取锁,如果被占用则阻塞等待
    std::cout << "线程 " << id << " 获取到锁,开始执行" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    std::cout << "线程 " << id << " 释放锁" << std::endl;
    // lock_guard析构时自动释放锁
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    
    t1.join();
    t2.join();
    return 0;
}

输出:

复制代码
线程 1 获取到锁,开始执行
(等待约2秒后)
线程 1 释放锁
线程 2 获取到锁,开始执行
线程 2 释放锁
场景二:条件变量(std::condition_variable)

条件变量允许线程在某个条件不满足时主动等待,直到其他线程通知条件发生变化。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

std::mutex g_mutex;
std::condition_variable g_cv;
bool g_ready = false;

void waiter() {
    std::unique_lock<std::mutex> lock(g_mutex);
    std::cout << "等待线程:检查条件,条件不满足,开始等待" << std::endl;
    
    // 等待条件满足(防止虚假唤醒)
    g_cv.wait(lock, []{ return g_ready; });
    
    std::cout << "等待线程:条件满足,继续执行" << std::endl;
}

void notifier() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lock(g_mutex);
        std::cout << "通知线程:改变条件并通知" << std::endl;
        g_ready = true;
    }
    g_cv.notify_one(); // 唤醒一个等待的线程
    std::cout << "通知线程:通知已完成" << std::endl;
}

int main() {
    std::thread t1(waiter);
    std::thread t2(notifier);
    
    t1.join();
    t2.join();
    return 0;
}
场景三:线程等待(std::thread::join)

主线程等待子线程执行完成。

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>

void worker() {
    std::cout << "工作线程开始工作..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "工作线程完成工作" << std::endl;
}

int main() {
    std::thread t(worker);
    
    std::cout << "主线程等待工作线程完成..." << std::endl;
    t.join(); // 主线程阻塞等待工作线程结束
    std::cout << "主线程继续执行" << std::endl;
    
    return 0;
}
场景四:带超时的等待

C++提供了带超时机制的等待,避免无限期阻塞。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

std::mutex g_mutex;
std::condition_variable g_cv;
bool g_ready = false;

void timed_waiter() {
    std::unique_lock<std::mutex> lock(g_mutex);
    std::cout << "定时等待线程:开始等待,最多等3秒" << std::endl;
    
    auto status = g_cv.wait_for(lock, std::chrono::seconds(3), 
                               []{ return g_ready; });
    
    if (status) {
        std::cout << "定时等待线程:条件在超时前满足" << std::endl;
    } else {
        std::cout << "定时等待线程:等待超时,条件仍未满足" << std::endl;
    }
}

void slow_notifier() {
    std::this_thread::sleep_for(std::chrono::seconds(5)); // 5秒后才通知
    {
        std::lock_guard<std::mutex> lock(g_mutex);
        g_ready = true;
    }
    g_cv.notify_one();
    std::cout << "慢速通知线程:通知已发出" << std::endl;
}

int main() {
    std::thread t1(timed_waiter);
    std::thread t2(slow_notifier);
    
    t1.join();
    t2.join();
    return 0;
}
场景五:Future和Promise(std::future, std::promise)

用于在线程间传递结果,等待异步操作完成。

cpp 复制代码
#include <iostream>
#include <thread>
#include <future>
#include <chrono>

int heavy_computation() {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "计算完成" << std::endl;
    return 42;
}

int main() {
    // 异步执行计算
    std::future<int> result = std::async(std::launch::async, heavy_computation);
    
    std::cout << "主线程可以做其他事情..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "主线程其他事情做完,等待计算结果..." << std::endl;
    
    // 阻塞等待并获取结果
    int value = result.get();
    std::cout << "计算结果: " << value << std::endl;
    
    return 0;
}

4. C++线程状态与阻塞

虽然C++标准没有像Java那样明确定义线程状态,但在实际实现中,线程在阻塞等待时会处于类似的状态:

  • 阻塞在互斥锁上:等待获取锁
  • 等待条件变量:主动暂停执行
  • 等待join:等待其他线程结束
  • 等待future:等待异步操作结果

5. 阻塞等待 vs 忙等待(自旋锁)

cpp 复制代码
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>

// 忙等待实现的自旋锁
class SpinLock {
private:
    std::atomic<bool> locked{false};
public:
    void lock() {
        while (locked.exchange(true, std::memory_order_acquire)) {
            // 忙等待:持续检查直到锁可用
        }
    }
    void unlock() {
        locked.store(false, std::memory_order_release);
    }
};

SpinLock spin_lock;
std::mutex normal_mutex;

void spin_lock_worker(int id) {
    spin_lock.lock(); // 忙等待获取锁
    std::cout << "自旋锁线程 " << id << " 开始" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "自旋锁线程 " << id << " 结束" << std::endl;
    spin_lock.unlock();
}

void normal_lock_worker(int id) {
    std::lock_guard<std::mutex> lock(normal_mutex); // 阻塞等待获取锁
    std::cout << "普通锁线程 " << id << " 开始" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "普通锁线程 " << id << " 结束" << std::endl;
}

总结

特性 C++阻塞等待 C++忙等待
CPU占用 不占用CPU 持续占用CPU
实现方式 std::mutex, std::condition_variable, future::get() 自旋锁,原子变量循环检查
响应速度 较慢(需要线程切换) 很快(立即响应)
适用场景 大多数高并发场景,I/O操作 极短等待,低竞争场景

C++提供了丰富的同步机制来实现阻塞等待,正确使用这些机制可以编写出高效、安全的并发程序。选择阻塞等待还是忙等待取决于具体的性能要求和应用场景。

相关推荐
Sunsets_Red4 小时前
差分操作正确性证明
java·c语言·c++·python·算法·c#
第七序章5 小时前
【C++】AVL树的平衡机制与实现详解(附思维导图)
c语言·c++·人工智能·机器学习
ajassi20005 小时前
开源 C++ QT QML 开发(十九)多媒体--音频录制
c++·qt·开源
晨非辰5 小时前
【面试高频数据结构(四)】--《从单链到双链的进阶,读懂“双向奔赴”的算法之美与效率权衡》
java·数据结构·c++·人工智能·算法·机器学习·面试
cookies_s_s6 小时前
LRU Cache 最近最少使用
c++
郝学胜-神的一滴7 小时前
深入解析Linux下的`lseek`函数:文件定位与操作的艺术
linux·运维·服务器·开发语言·c++·软件工程
仰泳的熊猫7 小时前
LeetCode:889. 根据前序和后序遍历构造二叉树
数据结构·c++·算法
小欣加油8 小时前
leetcode 329 矩阵中的最长递增路径
c++·算法·leetcode·矩阵·深度优先·剪枝
千码君20168 小时前
Go语言:记录一下Go语言系统学习的第一天
java·开发语言·学习·golang·gin·并发编程·编译语言