并发编程的挑战-上下文切换的问题

并发编程的挑战

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 等参数来优化调度行为。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值