目录
什么是线程上下文切换
多线程的上下文切换:是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。CPU给每个线程分配CPU时间片(机会),多线程创建并切完到另一个线程的过程,就是上下文切换。
时间片:是指 CPU分配给每个线程的执行时间段。
CPU为了能够执行多个线程,需要不停的切换执行的线程,这样才能使所有线程在一段时间内都有被执行的机会。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
线程上下文切换的类型
让步式/协同式上下文切换:指某一线程执行完后主动通知系统切换到另一线程上执行,执行线程主动释放CPU,与锁竞争严重程度成正比,可通过减少锁竞争来避免;线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
抢占式上下文切换:每条线程执行的时间、线程的切换都由系统控制,线程因分配的时间片用尽而被迫放弃CPU或者被其他优先级更高的线程所抢占,(可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。)一般由于线程数大于CPU可用核心数引起,可通过调整线程数,适当减少线程数来避免。
Java中线程会按优先级分配CPU时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
线程上下文切换的原因
当前执行任务(线程)的时间片用完之后,系统CPU正常调度下一个任务中断处理,在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。中断分为硬件中断和软件中断,软件中断包括因为IO阻塞、未抢到资源或者用户代码等原因,线程被挂起。
用户态切换,对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的。
多个任务抢占锁资源,在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换。
线程上下文切换查看命令
Linux系统下可以使用vmstat命令来查看上下文切换的次数, 其中cs列就是指上下文切换的数目(一般情况下, 空闲系统的上下文切换每秒大概在1500以下)。
线程上下文切换存在的问题
上下文切换会导致额外的开销,常常表现为高并发执行时速度会慢串行,因此减少上下文切换次数便可以提高多线程程序的运行效率。
直接消耗:指的是CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉。
间接消耗:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小。
线程上下文切换优化
- 无锁并发编程:多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据。
- CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
- 使用最少线程:避免创建不必要的线程,比如,任务量很小,使用多线程处理,就容易造成线程等待。
- 协程:单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
合理设置线程数目既可以最大化利用CPU,又可以减少线程切换的开销。
- 高并发,低耗时的情况,建议少线程。
- 低并发,高耗时的情况:建议多线程。
- 高并发高耗时,要分析任务类型、增加排队、加大线程数。
思考:多线程的速度一定比单线程快吗?
这么问也就说明答案是否定的,而这道题的精髓就在于你能不能说出上下文切换这几个字。
代码实战:
package com.angyan.tool.flink.demo;
/**
* @author Evan Walker
* @version 1.0 昂焱数据 https://www.ayshuju.com
* @desc 功能描述:串行和并行执行效率测试
* @date 2023/03/29 17:13:07
*/
public class ThreadConcurrentTest {
public static void main(String[] args) throws Exception {
// 并发
concurrent(10000L);
// 串行
serial(10000L);
// 并发
concurrent(100000L);
// 串行
serial(100000L);
// 并发
concurrent(1000000L);
// 串行
serial(1000000L);
// 并发
concurrent(10000000L);
// 串行
serial(10000000L);
// 并发
concurrent(100000000L);
// 串行
serial(100000000L);
}
/**
* 并发执行
* @param count 测试数量
*/
private static void concurrent(Long count) throws Exception {
long start = System.currentTimeMillis();
Thread thread = new Thread(() -> {
int a = 0;
for (int i = 0; i < count; i++) {
a += 5;
}
});
thread.start();
Thread thread2 = new Thread(() -> {
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
});
thread2.start();
//阻塞主线程,等待thread和thread2线程执行完再往下执行
thread.join();
thread2.join();
long time = System.currentTimeMillis() - start;
System.out.print("Concurrency count:" + count + " , time:" + time + "ms ");
}
/**
* 串行执行
* @param count 测试数量
*/
private static void serial(Long count) {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (int i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("Serial count:" + count + " , time: " + time + "ms");
}
}
可以看出:并不是并发执行,速度就一定快。
更多消息资讯,请访问昂焱数据(https://www.ayshuju.com)