文章目录
并发编程的挑战
1、上下文切换的问题
1.1 多线程和单线程执行速度对比
同时执行多个线程,它的机制是 CPU 通过给每个 线程分配CPU 时间片来实现的。时间片是CPU 分给各个线程的时间,因为时间片非常短,一般是几十毫秒,所以CPU 通过不停切换线程执行,让我们感觉多个线程是同时执行的。
线程上下文切换是指 CPU 停止当前正在执行的线程,将其状态保存到内存中,然后加载另一个线程的状态并开始执行新线程的过程。线程状态包括程序计数器(PC)、寄存器内容和线程栈等。
并发和串行的对比
public class ConcurrencyTest {
private static final long count = 1000000000l;
public static void main(String[] args) throws InterruptedException {
concurrency();
serial();
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++){
a += 5;
}
int b = 0;
for (long i=0; i < count; i++){
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial:" + time + "ms, b="+b+", a="+a);
}
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
public void run() {
int a = 0;
for (int i = 0; i < count; i++) {
a += 5;
}
}
});
thread.start();
int b = 0;
for (int i = 0; i < count; i++) {
b--;
}
thread.join(); //
long time = System.currentTimeMillis() - start;
System.out.println("concurrency : " + time + "ms, b = "+ b);
}
}
我这里的测试结果是 在 循环次数在1千万的时候,并发比串行快一倍,在百万级别相同,百万以下串行比并发有时快,有时也相同。
1.2 测试上下文的切换次数和时长
使用vmstat工具查看上下文的切换次数
vmstat 1
(注意 vmstat 1,这里是阿拉伯数字一,不是字母);
vmstat
(虚拟内存统计)是一个系统监控工具,用于报告虚拟内存、进程、CPU活动等的统计信息。通过 vmstat 1
命令,你可以每秒查看一次系统的状态。下面是对 vmstat
输出结果的解释:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 555232 179124 2201508 0 0 0 36 4 5 1 1 99 0 0
0 0 0 554944 179124 2201512 0 0 0 572 1201 2066 1 1 95 3 0
1 0 0 555148 179124 2201516 0 0 0 0 913 1632 1 0 99 0 0
0 0 0 555164 179124 2201516 0 0 0 0 807 1479 0 0 99 0 0
0 0 0 555164 179124 2201516 0 0 0 0 825 1540 0 0 100 0 0
0 0 0 555492 179124 2201520 0 0 0 0 1035 1971 1 1 99 0 0
0 0 0 555036 179124 2201520 0 0 0 96 939 1665 0 1 98 0 0
每列的含义如下:
procs
(进程信息)
r
(runnable processes): 可运行的进程数(正在运行或等待运行的进程数)。b
(blocked processes): 正在等待资源(通常是 I/O)的进程数。
memory
(内存信息)
swpd
(swapped): 使用的虚拟内存大小(单位:KB)。free
(free): 空闲物理内存大小(单位:KB)。buff
(buffer): 用作缓冲的内存大小(单位:KB)。cache
(cached): 用作缓存的内存大小(单位:KB)。
swap
(交换信息)
si
(swap in): 从交换区读入内存的数据量(单位:KB/s)。so
(swap out): 从内存写入交换区的数据量(单位:KB/s)。
io
(I/O信息)
bi
(blocks in): 从块设备(如硬盘)读入的数据量(单位:块/s)。bo
(blocks out): 写入块设备的数据量(单位:块/s)。
system
(系统信息)
in
(interrupts): 每秒的中断次数。cs
(context switches): 每秒的上下文切换次数。
cpu
(CPU信息,所有值总和为100%)
us
(user time): 用户进程消耗的 CPU 时间百分比。sy
(system time): 内核进程消耗的 CPU 时间百分比。id
(idle time): CPU 空闲时间百分比。wa
(wait time): 等待 I/O 操作完成的时间百分比。st
(stolen time): 被虚拟机管理程序 “偷走” 的时间百分比(在虚拟化环境中)。
1.2.1示例输出分析
第二行 (数据行)
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 555232 179124 2201508 0 0 0 36 4 5 1 1 99 0 0
r
= 1: 有1个进程在运行或等待运行。b
= 0: 没有进程在阻塞等待资源。swpd
= 0: 没有使用交换内存。free
= 555232: 有 555232 KB 的空闲物理内存。buff
= 179124: 有 179124 KB 的缓冲内存。cache
= 2201508: 有 2201508 KB 的缓存内存。si
= 0: 没有从交换区读入的数据。so
= 0: 没有从内存写入交换区的数据。bi
= 0: 没有从块设备读入的数据。bo
= 36: 每秒 36 块数据写入块设备。in
= 4: 每秒 4 次中断。cs
= 5: 每秒 5 次上下文切换。us
= 1: 用户进程消耗了 1% 的 CPU 时间。sy
= 1: 内核进程消耗了 1% 的 CPU 时间。id
= 99: 99% 的 CPU 时间是空闲的。wa
= 0: 等待 I/O 操作完成的时间为 0%。st
= 0: 没有被虚拟机管理程序 “偷走” 的时间。
第三行 (数据行)
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 554944 179124 2201512 0 0 0 572 1201 2066 1 1 95 3 0
r
= 0: 没有进程在运行或等待运行。b
= 0: 没有进程在阻塞等待资源。swpd
= 0: 没有使用交换内存。free
= 554944: 有 554944 KB 的空闲物理内存。buff
= 179124: 有 179124 KB 的缓冲内存。cache
= 2201512: 有 2201512 KB 的缓存内存。si
= 0: 没有从交换区读入的数据。so
= 0: 没有从内存写入交换区的数据。bi
= 0: 没有从块设备读入的数据。bo
= 572: 每秒 572 块数据写入块设备。in
= 1201: 每秒 1201 次中断。cs
= 2066: 每秒 2066 次上下文切换。us
= 1: 用户进程消耗了 1% 的 CPU 时间。sy
= 1: 内核进程消耗了 1% 的 CPU 时间。id
= 95: 95% 的 CPU 时间是空闲的。wa
= 3: 等待 I/O 操作完成的时间为 3%。st
= 0: 没有被虚拟机管理程序 “偷走” 的时间。
其余数据类似。
1.2.2 总结
从这些数据可以看出:
- 系统的 CPU 负载较轻,大部分时间处于空闲状态 (
id
在 95-100%)。 - 没有使用交换内存 (
swpd
一直为 0)。 - 内存中有足够的空闲空间和缓存。
- I/O 操作较少,但有时写入块设备 (
bo
) 会增加。 - 中断和上下文切换数量适中,没有明显的瓶颈。
1.3 如何减少上下文切换
1. 优化代码和应用程序设计
- 减少线程和进程的数量:尽量减少创建不必要的线程和进程。可以考虑使用线程池等机制来重用线程。
- 使用异步 I/O:同步 I/O 操作会导致线程阻塞,增加上下文切换。使用异步 I/O 操作可以减少阻塞,提高效率。
- 优化锁的使用:避免频繁的加锁和解锁操作,因为这些操作可能导致线程争用资源和上下文切换。可以通过减少临界区的大小、使用无锁编程或减少锁的粒度来优化锁的使用。
2. 系统级优化
- 调整调度策略:根据应用程序的特性,调整操作系统的调度策略。例如,可以调整调度优先级、时间片长度等参数来减少上下文切换。
- 绑定进程或线程到特定的 CPU(CPU pinning/affinity):将进程或线程绑定到特定的 CPU 核心上,减少因迁移导致的上下文切换。
3. 使用高效的同步机制
- 减少竞争:通过减少线程之间的竞争来减少上下文切换。例如,尽量避免多个线程同时访问共享资源。
- 选择合适的同步原语:根据具体情况选择合适的同步机制。自旋锁(spinlock)在短时间内保持锁时比互斥锁(mutex)更高效,因为自旋锁避免了上下文切换。
4. 应用程序架构优化
- 减少线程间通信:线程间的通信和同步可能导致上下文切换。设计应用程序时,尽量减少线程之间的依赖和通信。
- 批处理任务:尽量批量处理任务,减少频繁的线程切换。例如,可以使用队列来批量处理任务。
5. 系统监控和调优
- 监控系统性能:使用工具(如
vmstat
,top
,htop
,perf
等)监控系统性能,找出上下文切换频繁的原因。 - 调优系统参数:根据监控结果,调优系统参数。例如,可以调整
/proc/sys/kernel/sched_latency_ns
和/proc/sys/kernel/sched_min_granularity_ns
等参数来优化调度行为。