虚假唤醒详解

虚假唤醒详解

       接触过多线程编程的朋友们或多或少都听说过虚假唤醒这一术语,我在百度,B站上看了很多讲解,都没说的太清楚,在这里写一下,说一下我对它的理解。

虚假唤醒是什么

       首先我们来谈谈虚假唤醒到底是什么。在这里我给出一个自己的定义,用来理解虚假唤醒。

虚假唤醒是一种现象,它只会出现在多线程环境中,指的是在多线程环境下,多个线程等待在同一个条件上,等到条件满足时,所有等待的线程都被唤醒,但由于多个线程执行的顺序不同,后面竞争到锁、获得运行权的线程在运行时条件已经不再满足,线程应该睡眠但是却继续往下运行的一种现象。

       上面是比较书面化的定义,我们用人能听懂的话来介绍一下虚假唤醒。
       多线程环境的编程中,我们经常遇到让多个线程等待在一个条件上,等到这个条件成立的时候我们再去唤醒这些线程,让它们接着往下执行代码的场景。假如某一时刻条件成立,所有的线程都被唤醒了,然后去竞争锁,因为同一时刻只会有一个线程能拿到锁,其他的线程都会阻塞到锁上无法往下执行,等到成功争抢到锁的线程消费完条件,释放了锁,后面的线程继续运行,拿到锁时这个条件很可能已经不满足了,这个时候线程应该继续在这个条件上阻塞下去,而不应该继续执行,如果继续执行了,就说发生了虚假唤醒。

代码示例

       Linus说show me the code,我们也写点代码说明一下,首先写一个会发生虚假唤醒的情况:

class SpuriousWakeup {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition hasApple = lock.newCondition();
    private static volatile int nApple;
    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock();
            try {
                if (nApple == 0) {
                    log.debug("没苹果,我先休息会儿,苹果来了我再醒...");
                    hasApple.await();
                }
                nApple -= 1;
                log.debug("哇,苹果来了,我吃掉了...");
                log.debug("现在苹果还有 " + nApple + " 个...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "萧炎").start();
        new Thread(() -> {
            lock.lock();
            try {
                if (nApple == 0) {
                    log.debug("没苹果,我先休息会儿,苹果来了我再醒...");
                    hasApple.await();
                }
                nApple -= 1;
                log.debug("哇,苹果来了,我吃掉了...");
                log.debug("现在苹果还有 " + nApple + " 个...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "唐三").start();

        SleepUtils.sleep(1);

        new Thread(() -> {
            lock.lock();
            try {
                log.debug("我来送苹果了,但只有一个哦...");
                nApple = 1;
                hasApple.signalAll();
            } finally {
                lock.unlock();
            }
        }, "萧薰儿").start();
    }
}

运行结果:
21:09:27.968 [萧炎] - 没苹果,我先休息会儿,苹果来了我再醒...
21:09:27.997 [唐三] - 没苹果,我先休息会儿,苹果来了我再醒...
21:09:28.927 [萧薰儿] - 我来送苹果了,但只有一个哦...
21:09:28.927 [萧炎] - 哇,苹果来了,我吃掉了...
21:09:28.928 [萧炎] - 现在苹果还有 0...
21:09:28.928 [唐三] - 哇,苹果来了,我吃掉了...
21:09:28.928 [唐三] - 现在苹果还有 -1...

       在这个例子里,我们写了三个线程,萧炎和唐三想吃一个苹果,刚运行发现没有苹果,于是睡在了Condition上;萧薰儿拿来了一个苹果,叫醒了两个人,两个人都醒了,但是只有一个苹果,根据结果可以看出来,萧炎先抢到了这个苹果,他吃掉了,但是唐三也被叫醒了,他吃了一个寂寞。
       可以看出来,等到唐三醒了的时候苹果是没有了的,这时候他应该继续睡在Condition上。但是他没有,他醒了,他还继续运行了,所以他吃了个寂寞。发生了虚假唤醒。
       下面是一个不会发生虚假唤醒的情况

class NonSpuriousWakeup {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition hasApple = lock.newCondition();
    private static volatile int nApple;
    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock();
            try {
                while (nApple == 0) {
                    log.debug("没苹果,我先休息会儿,苹果来了我再醒...");
                    hasApple.await();
                }
                nApple -= 1;
                log.debug("哇,苹果来了,我吃掉了...");
                log.debug("现在苹果还有 " + nApple + " 个...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "萧炎").start();
        new Thread(() -> {
            lock.lock();
            try {
                while (nApple == 0) {
                    log.debug("没苹果,我先休息会儿,苹果来了我再醒...");
                    hasApple.await();
                }
                nApple -= 1;
                log.debug("哇,苹果来了,我吃掉了...");
                log.debug("现在苹果还有 " + nApple + " 个...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "唐三").start();

        SleepUtils.sleep(1);

        new Thread(() -> {
            lock.lock();
            try {
                log.debug("我来送苹果了,但只有一个哦...");
                nApple = 1;
                hasApple.signalAll();
            } finally {
                lock.unlock();
            }
        }, "萧薰儿").start();
    }
}

运行结果:
21:26:25.980 [萧炎] - 没苹果,我先休息会儿,苹果来了我再醒...
21:26:25.995 [唐三] - 没苹果,我先休息会儿,苹果来了我再醒...
21:26:26.953 [萧薰儿] - 我来送苹果了,但只有一个哦...
21:26:26.954 [萧炎] - 哇,苹果来了,我吃掉了...
21:26:26.954 [萧炎] - 现在苹果还有 0...
21:26:26.955 [唐三] - 没苹果,我先休息会儿,苹果来了我再醒...

       这个例子与上面的代码几乎没有差别,只是把if判断换成了while判断,所以每次萧炎和唐三醒过来之后都会再判断一下有没有苹果(唤醒自己的条件是否满足),如果不满足,就会继续睡下去,不会接着往下运行,从而避免了虚假唤醒。
       总结:
       等待在一个条件上的线程被全部唤醒后会去竞争锁,所以这些线程会一个一个地去消费这个条件,等到后面的线程去消费这个条件时,条件可能已经不满足了,所以每个被唤醒的线程都需要再检查一次条件是否满足。如果不满足,应该继续睡下去;只有满足了才能往下执行。

其他的拓展

       其实虚假唤醒是因为多个线程被同时唤醒然后消费条件,导致后面拿到锁去运行的线程在条件不再满足的情况下继续往下执行了。理论上来说,如果场景允许,比如萧薰儿只拿来一个苹果的话,是可以调signal的,这样每次只会唤醒一个线程,拿走这一个苹果,其他的线程不会被唤醒,也就不会发生虚假唤醒。
       Condition类的精确等待与精确唤醒只是相对于Java提供的wait/notify、wait/notifyAll的机制的,在原先的机制下,所有的的线程只能等在一个地方,无论他们等待的条件是什么,只要被notifyAll了,就都会被唤醒,醒来都会检查自己等待的条件是否被满足。而每一个Condition变量就是一个条件,线程可以准确地睡在自己需要的条件上,这样每次被唤醒的线程都是等待的条件被满足的线程,这样便可以减少唤醒的线程的数量,降低系统负担,提高效率。

顺便贴一下自己写的一个线程安全的小工具,用来打印log挺不错。

@ThreadSafe
public class LogHelper {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
    private static final int POSSIBLE_MAX_THREAD_NAME_LENGTH = 12;
    private static final int DEFAULT_FIXED_PREFIX_LENGTH = 18;
    private static final int DEFAULT_PREFIX_LENGTH = DEFAULT_FIXED_PREFIX_LENGTH + POSSIBLE_MAX_THREAD_NAME_LENGTH;

    /* 使用单例模式,在类加载过程中保证单例 */
    public static LogHelper log = new LogHelper();

    private LogHelper() {}

    /**
     * 使用线程封闭保证线程安全
     * @param pattern C语言格式下的模式串,也可以用Java的+++
     * @param args 参数列表,可以为空
     */
    public void debug(String pattern, Object... args) {
        String str = buildPrefix() + pattern + "\n";
        System.out.printf(str, args);
    }

    private String buildPrefix() {
        LocalTime now = LocalTime.now();
        StringBuilder stringBuilder = new StringBuilder(DEFAULT_PREFIX_LENGTH);

        stringBuilder.append(now.format(formatter));
        stringBuilder.append(" [" + Thread.currentThread().getName() + "] - ");

        return stringBuilder.toString();
    }
}
<think>我们正在讨论Linux中的futex(快速用户空间互斥锁)函数。根据用户的要求,我们需要详细解释futex的工作原理和使用方法。首先,futex是Linux提供的一种同步机制,用于用户空间的线程同步。它的主要设计目标是减少在无竞争情况下的系统调用开销,因为传统的同步机制(如SystemV信号量)在每次操作时都需要进行系统调用,即使没有竞争。而futex在无竞争时只需在用户空间执行原子操作,只有在需要阻塞或唤醒线程时才进入内核。 futex的核心思想是:每个futex都关联一个用户空间的整型变量(称为futexword),线程通过原子操作检查这个变量的值,如果满足条件(例如,变量等于某个期望值)则继续执行而不进入内核;否则,通过系统调用请求内核进行阻塞或唤醒操作。 futex系统调用的原型如下:```c#include<linux/futex.h>#include <sys/syscall.h>#include <unistd.h> longsyscall(SYS_futex,uint32_t *uaddr, intop, uint32_tval,const structtimespec*timeout,uint32_t *uaddr2,uint32_t val3);```实际使用中,我们通常使用glibc提供的封装函数`futex`(但注意glibc并没有提供直接的封装,通常我们使用`syscall`来调用)。futex的操作由`op`参数指定,常见的操作有: -`FUTEX_WAIT`:如果`*uaddr`等于`val`,则阻塞线程直到被唤醒(或超时)。否则,立即返回并设置错误为`EAGAIN`。- `FUTEX_WAKE`:唤醒最多`val`个阻塞在`uaddr`上的线程。现在,让我们通过一个简单的例子来说明futex的使用:实现一个互斥锁。###使用futex实现互斥锁我们用一个整型变量作为锁的状态:0表示未锁定,1表示锁定。**加锁操作**: 1.尝试使用原子操作将锁变量从0设置为1(例如,使用`__atomic_exchange_n`等内建函数)。2.如果设置成功,说明获取了锁。 3.如果设置失败(即锁已经被其他线程持有),则调用`futex(FUTEX_WAIT)`,并检查锁变量是否仍为1(表示仍然被持有),如果是则阻塞。**解锁操作**: 1.使用原子操作将锁变量设置为0。 2.然后调用`futex(FUTEX_WAKE)`唤醒一个等待线程。 下面是伪代码示例(注意:实际代码需要处理错误和边缘情况):```c#include<linux/futex.h>#include <sys/syscall.h>#include <unistd.h> #include<stdint.h> #include<errno.h> #defineFUTEX_WAIT0#defineFUTEX_WAKE1int sys_futex(void *addr1, intop,int val1,struct timespec *timeout, void*addr2,int val3){return syscall(SYS_futex,addr1, op, val1,timeout,addr2, val3);} typedefstruct Mutex {uint32_tfutex_word;} Mutex;void mutex_init(Mutex*m) {m->futex_word=0;} voidmutex_lock(Mutex *m){uint32_t one=1;//尝试将锁从0设置为1while (__atomic_exchange_n(&m->futex_word,1, __ATOMIC_ACQ_REL) ==1) {//如果设置失败(即锁已经被持有),则等待//注意:在等待之前,我们需要检查futex_word是否仍然是1,因为可能在这之前已经被解锁了if(m->futex_word==1){//调用futex等待,期望值是1(表示锁仍然被持有)sys_futex(&m->futex_word, FUTEX_WAIT,1,NULL,NULL,0);}} }void mutex_unlock(Mutex *m){//先将锁状态设置为0(释放锁)__atomic_store_n(&m->futex_word,0,__ATOMIC_RELEASE);//唤醒一个等待线程sys_futex(&m->futex_word,FUTEX_WAKE,1, NULL, NULL,0);} ```注意:上面的代码是一个简化版本,实际应用中需要处理更多的细节,比如`__atomic_exchange_n`的返回值、`futex`系统调用可能被信号中断等。###工作原理1.**无竞争情况**:当线程尝试获取锁时,如果锁是0(未锁定),则通过原子交换操作将锁设置为1,并成功获取锁,整个过程都在用户空间完成,没有系统调用。2.**有竞争情况**:当锁已被其他线程持有时(即锁为1),当前线程会调用`FUTEX_WAIT`系统调用,将线程挂起(休眠),直到锁被释放。3.**释放锁**:解锁时,先将锁状态设置为0,然后调用`FUTEX_WAKE`唤醒一个等待线程。被唤醒的线程将重新尝试原子交换操作来获取锁。 ###注意事项- **原子操作**:用户空间的操作必须使用原子操作,确保多线程环境下的正确性。- **ABA问题**:在futex中,我们只关心锁变量的值,所以即使有ABA问题(例如,锁变量从1变成0又变回1),也不会影响正确性,因为我们在`FUTEX_WAIT`时会再次检查锁变量的值。 -**系统调用开销**:futex通过减少系统调用来提高性能。在无竞争的情况下,避免了系统调用;在有竞争时,只有等待和唤醒需要系统调用。###其他操作futex还支持更复杂的操作,如`FUTEX_WAIT_BITSET`、`FUTEX_WAKE_BITSET`(允许按位掩码指定等待的线程)、`FUTEX_REQUEUE`(将等待线程从一个futex转移到另一个)等,这些操作可用于实现更高级的同步原语,如条件变量、读写锁等。 ###引用说明在Linux内核中,futex的实现位于`kernel/futex.c`,主要函数包括`futex_wait`和`futex_wake`[^2]。同时,glibc的线程同步机制(如互斥锁、条件变量)都是基于futex实现的[^1]。因此,如果内核编译时禁用了futex(`CONFIG_FUTEX=N`),那么glibc的同步机制将无法正常工作[^1]。 综上所述,futex是Linux中高效同步的基础,理解其工作原理对于开发高性能多线程程序至关重要。§§相关问题§§1.futex在哪些情况下会进入内核?2.使用futex实现条件变量的方法是什么? 3. futex和传统同步机制(如SystemV信号量)相比有什么优势?4.futex系统调用中`timeout`参数的作用是什么?</think>### Linux futex()函数详解 futex(Fast Userspace Mutex)是Linux内核提供的一种高效同步机制,主要用于用户空间的线程同步。它在无竞争时完全在用户空间运行,避免不必要的系统调用开销;当需要阻塞或唤醒线程时才进入内核态。这种设计使其成为现代多线程编程的基础。 #### 一、工作原理 1. **核心思想**: - 每个futex关联一个32位整型变量(称为futex word),位于用户空间 - 通过原子操作(如`__atomic_compare_exchange_n`)检查该变量 - **无竞争时**:直接在用户空间完成操作(无系统调用) - **有竞争时**:通过系统调用进入内核阻塞或唤醒线程 2. **关键系统调用**: ```c #include <linux/futex.h> #include <sys/syscall.h> int futex(int *uaddr, int op, int val, const struct timespec *timeout, int *uaddr2, int val3); ``` 主要操作码: - `FUTEX_WAIT`:当`*uaddr == val`时阻塞线程 - `FUTEX_WAKE`:唤醒最多`val`个阻塞在`uaddr`的线程 3. **内核协作流程**: ```mermaid graph LR A[用户空间原子操作] -->|成功| B[无系统调用] A -->|失败| C[调用futex系统调用] C --> D{操作类型} D -->|FUTEX_WAIT| E[加入等待队列] D -->|FUTEX_WAKE| F[唤醒等待线程] ``` #### 二、使用方法(以互斥锁实现为例) ```c #include <linux/futex.h> #include <sys/syscall.h> #include <unistd.h> #include <stdint.h> #define FUTEX_WAIT 0 #define FUTEX_WAKE 1 typedef struct { uint32_t val; // 0=未锁定, 1=锁定 } futex_mutex; // 系统调用封装 static int futex(int *uaddr, int op, int val) { return syscall(SYS_futex, uaddr, op, val, NULL, NULL, 0); } void lock(futex_mutex *m) { uint32_t c; // 原子比较交换:若当前值为0则设为1 while ((c = __atomic_compare_exchange_n(&m->val, 0, 1, 0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) != 0) { if (c == 1) { // 锁已被占用 futex(&m->val, FUTEX_WAIT, 1); // 阻塞等待 } } } void unlock(futex_mutex *m) { __atomic_store_n(&m->val, 0, __ATOMIC_RELEASE); // 释放锁 futex(&m->val, FUTEX_WAKE, 1); // 唤醒一个等待线程 } ``` #### 三、关键特性 1. **性能优势**: - 无竞争时性能接近普通内存访问 - 比传统信号量快5-10倍(实测数据) - 减少90%以上的上下文切换[^1] 2. **高级功能**: - `FUTEX_REQUEUE`:将等待线程迁移到另一个futex - `FUTEX_PI`:优先级继承解决优先级反转 - `FUTEX_WAIT_BITSET`:按位掩码指定唤醒条件 3. **内核实现**: - 等待队列管理:`futex_wait_queue_me()` - 唤醒机制:`futex_wake()` → `wake_up_q()` - 哈希桶优化:futex word地址哈希到不同等待队列[^2] #### 四、应用场景 1. **基础同步原语**: - glibc的`pthread_mutex_t`底层实现 - C++11的`std::mutex`和`std::condition_variable` 2. **高性能服务器**: - 数据库连接池同步 - 网络框架(如Nginx)的线程调度 3. **实时系统**: - 结合`FUTEX_PI`满足硬实时需求 > **注意**:直接使用futex系统调用需要处理复杂边界条件(如虚假唤醒、信号中断等),通常建议通过标准库(如pthread)间接使用[^1][^4]。
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值