性能打磨手记:记一段 Futex 机制的内核优化之旅

Posted by Haibo.Chen on 2024-03-15

1. futex简介

Futex 是Fast Userspace muTexes的缩写,由 Linux 2.5.7开始引入。传统的SYSTEM V IPC机制需要系统调用进入内核态去操作某个内核对象,由内核来仲裁同步,事实上大部分情况下并没有资源竞争,此种情况下仍然进入内核态会显得很浪费,系统开销增加进而造成性能折扣,由此引入futex的概念。

futex是一种用户态和内核态混合机制,需要两个部分合作完成。在用户态,futex指的是一个原子变量,用来记录某种锁状态。需要持锁时,如果锁空闲则直接改变futex变量值,即表明持有该锁,不需要陷入内核。释放锁时,如果锁状态中没有线程等锁,则直接置锁状态为空闲,也无需陷入内核。

对于内核而言,futex是一个系统调用。当用户态出现锁竞争,线程需要休眠等待时,通过futex系统调用陷入内核并挂在wait queue中。当释放锁发现有线程在休眠等锁时,通过futex系统调用陷入内核去唤醒等锁的线程。

相比传统的SYSTEM V IPC机制,在无竞争时,用户态通过futex原子变量操作,替代内核对象操作,来提升性能。而绝大部分情况下,锁都是无竞争的,所以futex机制实现用户空间锁非常高效。

1.1 用户态futex同步机制

所有的futex同步操作都应该从用户空间开始,首先创建一个futex原子变量。其中以pthread_mutex的锁操作过程为例,state表示其持有和竞争状态,0为无人持锁,1为有人持锁但无人竞争,2为有人持锁且有竞争。

  1. 当线程A准备lock持锁,发现state\=\=0,则直接通过CAS(Compare and Swap)原子操作将state=1,表示持有该锁就结束了lock过程,整个过程并不需要内核参与,非常高效;
  2. 当线程B到来准备lock持锁,发现state\=\=1,锁已经被别人持有,于是使state=2表示有竞争,然后通过futex_wait(&state) 操作陷入内核,内核会根据用户空间地址&state创建一个等待队列,然后将B加入到&state的等待队列中,将B休眠;
  3. 当线程A退出临界区unlock释放该锁时,发现state\=\=2意识到有人竞争,会将state=0,然后通过futex_wake(&state) 陷入内核来唤醒等待&state的线程B。线程A返回用户空间继续执行,退出unlock流程;
  4. 线程B从futex等待队列中移除,并加入调度程序的就绪队列,等待调度。得到调度后返回用户空间重新判断state\=\=0,于是重复步骤1;

1.2 内核态futex系统调用

linux上提供了sys_futex系统调用,对进程竞争情况下的同步处理提供支持。其原型和系统调用号为如下,参数常用的就是三个。uaddr就是用户态下共享内存的地址,也就是用户态的futex变量地址。op存放着操作类型,最常用的是futex_wait和futex_wake。val表示要操作的线程个数。

1
2
3
4
#include <linux/futex.h>
#include <sys/time.h>
int futex (int *uaddr, int op, int val, const struct timespec *timeout,int *uaddr2, int val3);
#define __NR_futex 240

内核中futex内部采用了哈希表的数据结构,保存那些需要睡眠的任务。通过用户空间地址 uaddr,flag,以及 futex的读写状态可以计算出hash key值,将需要睡眠的任务的task_struct放到对应的哈希桶上的优先链表的节点中。

futex_wait(uaddr,val,abs_time) 流程:

  1. 计算 futex 对应的 key,获取 key 对应的哈希表链表。
  2. 获取哈希桶链表自旋锁,如果 *uaddr == val 返回错误给用户态。
  3. 否则将当前任务状态改为 TASK_INTERRUPTIBLE,并将当前任务插入到 futex 等待队列,释放哈希桶链表自旋锁,然后调度器重新调度。
  4. 当该线程从睡眠中被唤醒时,例如超时或者被wakeup,做相应处理后,返回用户态。

futex_wake(uaddr,nr_wake) 流程:

  1. 计算 futex 对应的 key,获取 key 对应的哈希桶。
  2. 获取哈希桶的自旋锁,遍历这个哈希桶上的优先链表,找到key匹配的任务,说明该任务在等待 futex,将当前任务添加到唤醒队列 wake_q 中,如果达到了 nr_wake 个,则退出循环。
  3. 释放哈希桶自旋锁,唤醒队列 wake_q 中每一个任务。

2. futex在Android的应用

下面是安卓系统中用户空间通用的同步机制框图,最终基本上都是基于 futex 系统调用来实现的。其中java部分同步机制,主要是由ART虚拟机实现的synchronize,和java.util.concurrent库实现的lock机制。C++部分主要是pthread库中实现的锁机制。

2.1 ART锁

Synchronize是java中的隐式锁,它的获取锁和释放锁都是隐式的,完全交由虚拟机实现。Synchronize生成的字节码会对应两条指令,分别是monitor-enter和monitor-exit。虚拟机锁一开始会通过CAS(Compare and swap)原子操作自旋,但是随着等锁时间增加,锁会不断升级,如果迟迟没有等到锁,最终升级为重量级锁,需要陷入内核休眠。

重量级锁最终会通过futex系统调用,由内核参与实现线程的挂起和唤醒。

1
2
3
4
5
6
7
8
void Monitor::Lock(Thread* self) 
|-->monitor_lock_.ExclusiveLock(self);
|-->futex(state_and_contenders_.Address(), FUTEX_WAIT_PRIVATE, cur_state,
enable_monitor_timeout_ ? &timeout_ts : nullptr , nullptr, 0)

bool Monitor::Deflate(Thread* self, ObjPtr<mirror::Object> obj) {
|-->monitor->monitor_lock_.ExclusiveUnlock(self);
|-->futex(state_.Address(), FUTEX_WAKE_PRIVATE, kWakeAll, nullptr, nullptr, 0);

2.2 JUC锁

java.util.concurrent包中提供的锁机制,称为JUC锁。相比Synchronize更加灵活,功能也更丰富,但是安全性也更低,需要开发者对JUC锁有足够的理解。根据功能分为下面几类

2.3 pthread锁

pthread锁是native层提供的pthread库中的一部分,提供给native层代码使用,来实现各种同步机制。

2.3.1 pthread_mutex

下面这段代码是pthread_mutex_lock核心部分,通过原子操作atomic_exchange_explicit,这个操作是将锁原子变量_Atomic(uint16_t) state替换为locked_contended,即置为处于竞争状态,并返回原本的state值。如果原本的锁状态不为unlocked即已经被人持有了,则调用__futex_wait_ex陷入内核休眠。如果原本锁状态就是unlocked,也就是没有人持有,则不休眠,也不需要内核参与。

1
2
3
4
5
6
7
8
9
594      const uint16_t unlocked           = shared | MUTEX_STATE_BITS_UNLOCKED;
595 const uint16_t locked_contended = shared | MUTEX_STATE_BITS_LOCKED_CONTENDED;
605 while (atomic_exchange_explicit(&mutex->state, locked_contended,
606 memory_order_acquire) != unlocked) {
607 if (__futex_wait_ex(&mutex->state, shared, locked_contended, use_realtime_clock,
608 abs_timeout_or_null) == -ETIMEDOUT) {
609 return ETIMEDOUT;
610 }
611 }

同样pthread_mutex_unlock核心代码,首先通过原子操作将锁状态state置为unlocked,然后判断原本的锁状态state是否为竞争状态,如果是竞争状态则调用__futex_wake_ex陷入内核来唤醒一个等待该state变量的线程

1
2
3
4
629      if (atomic_exchange_explicit(&mutex->state, unlocked,
630 memory_order_release) == locked_contended) {
658 __futex_wake_ex(&mutex->state, shared, 1);
659 }

2.3.2 pthread_cond

条件变量pthread_cond,2个典型操作wait和signal,等待和发射某个条件,对应的底层实现分别是futex_wait和futex_wake。

1
2
3
4
5
6
7
int pthread_cond_wait(pthread_cond_t* cond_interface, pthread_mutex_t* mutex) { 
int status = __futex_wait_ex(&cond->state, cond->process_shared(), old_state,
use_realtime_clock, abs_timeout_or_null);
}
int pthread_cond_signal(pthread_cond_t* cond_interface) {
__futex_wake_ex(&cond->state, cond->process_shared(), thread_count);
}

2.3.3 pthread_rwlock

读写锁,加读锁或写锁时,如果遇到互斥情况,通过futex_wait陷入内核休眠等待。释放读写锁时,通过futex_wake陷入内核唤醒等锁线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock_interface) {
futex_result = __futex_wait_ex(&rwlock->pending_writer_wakeup_serial, rwlock->pshared,
old_serial, use_realtime_clock, abs_timeout_or_null);
}


static int __pthread_rwlock_timedrdlock(pthread_rwlock_internal_t* rwlock, bool use_realtime_clock,
const timespec* abs_timeout_or_null) {
futex_result = __futex_wait_ex(&rwlock->pending_reader_wakeup_serial, rwlock->pshared,
old_serial, use_realtime_clock, abs_timeout_or_null);
}


int pthread_rwlock_unlock(pthread_rwlock_t* rwlock_interface) {
__futex_wake_ex(&rwlock->pending_writer_wakeup_serial, rwlock->pshared, 1);
}

3. 内核社区的futex优化方案

谷歌通过增加内核futex_swap接口,实现用户态任务调度框架 SwitchTo,来提高两个线程唤醒+休眠的效率。并在此基础上实现direct-thread-switch功能,使被唤醒线程直接利用即将休眠的线程时间片运行,减少调度开销。优化主要是在线程协作的场景,例如IPC通信,client线程切换到特定的server线程,或者server线程切换回到client线程。

移动终端厂商利用futex传递Ux属性,使锁的owner线程继承Ux属性一段时间。具备Ux属性的线程在调度、io、内存等资源的竞争上更优先,owner线程执行更快,这样可以减少原Ux线程等待该futex的时间。

下面分5节分别介绍futex_swap、current_cpu,direct-thread-switch、futex vip优化思路,及实际调试验证结果。

3.1 futex_swap

在谷歌内部有一套闭源的用户态任务调度框架 SwitchTo,这个框架可以为谷歌提供延迟敏感的服务,对运行的内容进行细粒度的用户空间控制/调度,它可以让内核来实现上下文的切换,同时将任务何时切换,何时恢复的工作交给了用户态的程序来做,这样既可以实现在任务间协作式切换的功能,又可以不丧失内核对于任务的控制和观察能力。这是使用 SwitchTo 和使用其他线程间切换的组件的上下文切换性能对比,可以看到在使用 SwitchTo 后切换的性能比其他组件提高了一个数量级别。

谷歌工程师Peter Oskolkov posk@google.com向 LKML(Linux Kernel Mail List) 提出的补丁中有关 futex_swap() 的实现。可以确定的是,SwitchTo 构建在这个futex_swap内核函数之上。futex_swap接口引出:[2] https://lore.kernel.org/lkml/48058b850de10f949f96b4f311adb649b1fb3ff2.camel@posk.io/

对于一些线程交互的场景,例如线程T1与线程T2协助完成一项任务,T1完成一个阶段任务后,需要移交给T2然后等待T2的结果,T2完成阶段任务后再移交给T1。常见的RPC通信一般都是这种交互模式,例如client唤醒server执行相应的任务,client自己休眠,然后server执行完成后唤醒client。用futex操作简化成下面的三个步骤。

a. T1: futex-wake T2, futex-wait
b. T2: wakes, does what it has been woken to do
c. T2: futex-wake T1, futex-wait

其中步骤a和c都有2次系统调用,如果有一个futex内核接口,能一次实现两个过程,即一个接口完成1个线程的唤醒和1个线程的休眠,将会带来一些的性能提升。

futex_swap(uaddr,val,abs_time,uaddr2) 流程:

  1. 获得等待在 uaddr2 上的预备唤醒队列,记录队列第一个任务为 next,对其他任务则执行唤醒。
  2. 对 uaddr1 执行 futex_wait(),传入 next。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int futex_swap(u32 __user *uaddr, unsigned int flags, u32 val,
ktime_t *abs_time, u32 __user *uaddr2)
{
u32 bitset = FUTEX_BITSET_MATCH_ANY;
struct task_struct *next = NULL;
DEFINE_WAKE_Q(wake_q);
int ret;

ret = prepare_wake_q(uaddr2, flags, 1, bitset, &wake_q);
if (ret < 0)
return ret;
if (!wake_q_empty(&wake_q)) {
/* At most one wakee can be present. Pull it out. */
next = container_of(wake_q.first, struct task_struct, wake_q);
next->wake_q.next = NULL;
}

return futex_wait(uaddr, flags, val, abs_time, bitset, next);
}
static void futex_wait_queue_me(struct futex_hash_bucket *hb, struct futex_q *q,
- struct hrtimer_sleeper *timeout)
+ struct hrtimer_sleeper *timeout,
+ struct task_struct *next)
{
+ if (!timeout || timeout->task) {
+ if (next) {
+ /*
+ * wake_up_process() below will be replaced
+ * in the next patch with
+ * wake_up_process_prefer_current_cpu().
+ */
+ wake_up_process(next);
+ put_task_struct(next);
+ next = NULL;
+ }
freezable_schedule();
+ }
}
}

移植到Android平台后,实际测试数据结果futex_swap较FUTEX_WAKE_WAIT相差不大,与社区的提升29.5%有一定出入,不知是不是有什么前置条件。

为了究其原因,从耗时角度对两种运行模式进行拆解,得到下面的流程图。在多核情况下,T1和T2线程会分别在不同cpu上运行。

从整体上对比分析,FUTEX_SWAP相比FUTEX_WAKE_WAIT,虽然少了2次syscall的耗时,但是这2次都是在并行的时间里完成的,并不影响测试用例链路耗时,仅仅是减少了系统的整体负载损耗。经过抽离简化,2种模式下的执行耗时其实都是下面这样,并没有多大差异,所以耗时相同也符合预期。

虽然futex_swap操作并没有能减少2个线程切换的耗时,但是成功将futex_wake+futex_wait操作融合为一个系统调用,为后续的深度优化提供了可能。

3.2 wake_up_process_prefer_current_cpu

谷歌工程师 Peter Oskolkov的优化patch[3] ,https://lore.kernel.org/lkml/d5cf58486a6a5e41581bed9183e8a831908ede0b.camel@posk.io/

在T1通过futex_swap操作唤醒T2并休眠自己时,优先将T2选核到T1的cpu上。

优化patch比较简单,在try_to_wake_up时传入自定义的WF_CURRENT_CPU标志位,在select_task_rq_fair选核时,判断如果有该标志位就将T2的cpu选到当前T1的cpu。这样T1 通过schedule()调度出去时,在next = pick_next_task(rq, prev, &rf)中,极大概率可以直接选中T2,进行无缝切换。同理T2切换到T1时也是如此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
diff --git a/kernel/futex.c b/kernel/futex.c
index f3833190886f..a426671e4bbb 100644
--- a/kernel/futex.c
+++ b/kernel/futex.c
@@ -2646,12 +2646,7 @@ static void futex_wait_queue_me(struct futex_hash_bucket *hb, struct futex_q *q,
*/
if (!timeout || timeout->task) {
if (next) {
- wake_up_process(next);
+ wake_up_process_prefer_current_cpu(next);
put_task_struct(next);
next = NULL;
}
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 9a2fbf98fd6f..f894b3e6c9ed 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -6180,6 +6180,11 @@ void sched_setnuma(struct task_struct *p, int nid)
}
#endif /* CONFIG_NUMA_BALANCING */

+int wake_up_process_prefer_current_cpu(struct task_struct *next)
+{
+ return try_to_wake_up(next, TASK_NORMAL, WF_CURRENT_CPU);
+}
+
#ifdef CONFIG_HOTPLUG_CPU

diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index 538ba5d94e99..80f927bb62eb 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -6656,6 +6656,9 @@ select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_f
int want_affine = 0;
int sync = (wake_flags & WF_SYNC) && !(current->flags & PF_EXITING);

+ if ((wake_flags & WF_CURRENT_CPU) && cpumask_test_cpu(cpu, p->cpus_ptr))
+ return cpu;
+

通过实测数据,经过current_cpu优化后的FUTEX_SWAP执行耗时降低92.69%,令人惊讶。

通过trace分析,截取2种测试模式运行时三次切换的片段,FUTEX_WAKE_WAIT时2个线程在不同cpu上来回切换,cpu实际频率只有500MHz,因为负载分配在2个cpu上,每个cpu的占用率不到50%。current_cpu优化后的FUTEX_SWAP时,2个线程跑在同一个cpu上,实际cpu频率有2.5GHz,因为负载集中在1个cpu上,cpu的占用率接近100%,所以频率自动调的也高。虽然速度提升10倍,但是其中有5倍是cpu频率影响。

FUTEX_WAKE_WAIT模式:3轮切换耗时182us

FUTEX_SWAP模式:3轮切换耗时18.4us

综上分析,执行时间降低10倍原因:

  1. cpu频率高5倍:跑在单核上,cpu利用率高,更有效触发提频
  2. 调度效率高2倍:同一个cpu,无缝切换,避免从idle退出的耗时,调度尾延更低,同时cache命中率更高。

所以从patch优化效率上来看,实际优化了2倍。在Android上,binder作为主要的IPC通信手段,运行场景跟这很类似,一个binder线程A唤醒另一个binder线程B,线程B执行完任务后反过来唤醒线程A,2个线程交替执行。如果把这里的current_cpu优化放在binder上,或许也有一定的优化效果。

3.3 direct-thread-switch

脱离 Android 的全自研鸿蒙 next 宣称支持轻量级线程并发模型,可能就是 direct-thread-switch,已合入服务器欧拉操作系统。

本节内容引自华为欧拉操作系统论坛:

1
2
在操作系统中,线程是一个独立的运行单元,一般认为每个线程是独立运行的;但是在实际应用中,经常出现一个业务功能需要多个线程协同处理,这时候线程和线程之间存在逻辑上的关联关系,经常出现线程之间的唤醒与被唤醒。线程的每次“交互”需要内核调度模块参与,一次完整的交互,内核调度模块需要参与选核,入队列,更新负载信息,选任务等流程。系统中如果线程比较多(有数千个线程),线程负载比较小的场景下,会导致内核调度开销比较大,而且线程之间交互的效率比较低(线程B需要入队列,等待CPU资源,导致runable的时间比较长)。在《The Case for Thread Migration- Predictable IPC in a Customizable and Reliable OS》这篇论文中讲述了一种新的线程切换思路:执行实体与调度实体分离。在同步IPC场景中,线程之间发生切换,只切换执行实体不切换调度实体,从而提升IPC的通信效率,降低调度开销。通过测试,这种技术可以使得同步IPC的效率提升近10倍。 引用
执行实体与调度实体分离的技术适用于微内核架构,在linux系统中并不适用,主要因为linux系统在设计之初就将执行实体与调度实体融合成一个结构体:task_struct, 经过多年的开发,task_struct结构体已经散布到linux内核的各个模块中,此时拆分执行实体和调度实体,工作量大,难度高,质量难以保障。 我们可以重新审视执行实体和调度实体分离的技术原理,在线程交互时,切换执行实体而不切换调度实体,本质上是利用了原任务的剩余时间片来运行新任务,bypass调度,降低了内核调度开销,同时原任务直接切换到新任务时,新任务不用重新入队列等待CPU资源,这时没有尾时延(runnable时间),大大提升线程交互的效率。这种往往用在同步线程模型中(原任务进入休眠,新任务执行)。

将原任务直接切换到新任务,bypass调度的技术命名为direct-thread-switch,bypass调度的前提必须是利用原任务的剩余时间片,否则会打破CFS调度的公平性。具体由briansun imbriansun@gmail.com提交到华为欧拉操作系统论坛上实现的patch

https://gitee.com/openeuler/kernel/commit/dad99a5750c4c942300c6626888a6ad52e62bf24

从patch的描述看,direct-thread-switch是在futex_swap的基础上实现的,是对futex_swap的进一步优化。futex_swap将futex_wake+futex_wait融合到一次系统调用,实现线程T2的唤醒和线程T1的休眠,涉及2次调度。而direct-thread-switch则是在此基础上,bypass调度,直接利用线程T1的剩余时间片来运行线程T2,避免了2次调度的开销。由于是基于futex_swap,所以使用场景也同样有限制,典型的场景是IPC通信,线程模型为T1->T2—-T2->T1。

direct-thread-switch代码实现很复杂,目前也只理解了很少一部分。在原有futex_swap基础上,在T1->T2线程切换时另起炉灶,替代了原有的schedule()函数,以__direct_thread_switch(next)函数直接从T1切换到T2。并提出共享调度实体shared sched_entry的概念,来保证CFS调度的公平性及调度器稳定性。

futex_swap:

1
2
+                                if (flags & FUTEX_FLAGS_DTS_MODE)
+ do_dts_switch = direct_thread_switch(next);

由于内核版本差异等因素,该patch并未移植成功,无法实际测试效果,借用openEuler内核社区测试了(futex_wake+futex_wait)、futex_swap、(futex_swap+direct-thread-switch)的切换耗时,futex_swap相比(futex_wake+futex_wait)提升了29.5%,(futex_swap+direct-thread-switch)相比(futex_wake+futex_wait)提升了37.1%

3.4 futex vip插队优化

目前已有的针对锁的优化特性都是针对内核锁,用户空间锁则不在这些优化范围内,两者有共性。

如果某个用户锁有大量线程并发竞争在等待,等锁造成的响应延迟将非常大。对于用户体验相关的前台vip线程来说非常不友好,需要适当向vip线程倾斜。线程B被加入futex等待队列,这个队列排序原则是RT线程按照其线程优先级0~99来排序,普通线程全部按照优先级100,然后FIFO先进先出原则排序。用户体验相关的vip线程很多是普通线程,所以在这里排队没有优势,需要改变规则,将vip线程排在普通线程之前,RT线程之后。

通过测试用例,创建一个持锁线程A(demo 10622),然后创建9个普通线程B和一个vip线程C(demo 10630)。线程A先持mutex锁,9个普通线程B等锁,线程C等锁,然后通过命令将线程C设置为vip线程。

当线程A释放锁时,可以看到vip线程C最先被唤醒,即使是最后一个入队也最先被唤醒,其他普通线程则是按照等锁入队FIFO顺序唤醒。所以vip线程的等锁时间也是最短的

为了防止普通线程饿死,还采取了规避措施,如果队列中第一个普通线程等待时间已经超过一定时间,则本次不做vip插队操作。可以看到,当vip线程 12446在等锁入队时,前面其他线程等锁时间超过347.2ms,所以本次不插队,vip线程按照入队顺序出队。

3.5 futex vip继承优化

在 vip_task 特性中,vip线程享有CFS优先调度、内核锁插队、内存快速分配,binder 优先与继承等优势,vip线程在等普通线程持有的用户空间锁时,普通线程没有相应的资源倾斜,可能导致其在临界区运行很慢,造成”优先级”翻转,vip线程的优势也得不到发挥。参考进程优先级继承的优化思路,vip线程在等用户锁时,临时赋予non-vip线程vip特权一段时间,让其尽快执行脱离临界区,最大程度减少vip线程的等待时延。

futex vip继承在内核实现上,有一些需要解决的问题,即vip线程在等futex锁时,需要知道持锁的线程holder是谁,才能针对持锁线程做boost。不同于内核中的mutex,现有基于futex实现的接口,并没有包含锁owner信息,所以需要对用户空间中相关接口做调整。

对于native的pthread_mutex锁,在调用futex_wait时增加一个参数用来传递futex_info信息,其中就包括owner_id。

内核实现,首先判断等锁线程是不是vip线程,如果是vip则解析其owner_id参数,找到owner task并对其赋予临时的vip特权,直到该owner通过futex_wake来唤醒等待的vip线程,解除vip特权。

4. futex vip收益测试

由于3.1-3.3节的优化依赖于上层的具体应用场景来实现,在安卓上并未找到合适的软件场景,所以没有做过多收益测试。3.4,3.5节契合安卓手机场景,我们分别模拟用户使用场景测试做了实测数据,测得对比数据。

正常使用场景,vip等futex的最大等待时间减少70%,超过100ms的次数减少25%,vip线程等futex总时间减少24%。

重载使用场景,vip等futex的最大等待时间减少67%,超过100ms的次数减少38%,超过200ms次数减少45%,vip线程等futex总时间减少21%。

5. 总结

本文首先介绍了linux的futex系统调用,及基于futex的同步机制。然后列举了futex在Android上的几种应用,包括ART锁、JUC锁、pthread锁。

介绍了几种futex性能优化相关的开源案例,其中futex_swap(包含current_cpu)优化和direct-thread-switch优化,线程切换性能分别提升了29.5%和37.1%。futex vip插队及属性继承优化,最大程度保证用户交互线程的响应速度。

最后futex vip插队及属性继承优化,实际模拟用户场景测试收益,取得不错的收益。改善优先级翻转和锁等待的性能延时问题,提高前台应用响应速度,降低偶现卡顿的机率。


This is copyright.