线程是如何调度的?

转发: https://blog.csdn.net/u011454830/article/details/49704839

线程调度

计算机通常只有一个cpu,在任意时刻只能执行一条机器指令,每个线程只有获得cpu的使用权才能执行指令.所谓多线程的并发运行,其实是从宏观上看,各个线程轮流获取cpu的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待cpu, JAVA虚拟机的一项任务就是负责线程的调度.线程调度是指按照特定机制为多个线程分配CPU的使用.

调度方式

1. 分时调度模式: 是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的cpu的时间片.

2. 抢占式调度模式: JAVA虚拟机采用抢占式调度模式,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那就随机选择一个线程,使其占用CPU.处于运行状态的线程会一直运行,直至它不得不放弃CPU.

如何调度

NTSTATUS  NtYieldExecution (VOID)

{

    KIRQL OldIrql;

    PKTHREAD NewThread;

    PRKPRCB Prcb;

    NTSTATUS Status;

    PKTHREAD Thread;

 

    if (KiGetCurrentReadySummary() ==0) {      //如果当前CPU没有就绪线程

        return STATUS_NO_YIELD_PERFORMED;

 

    } else {

        Status =STATUS_NO_YIELD_PERFORMED;

        Thread =KeGetCurrentThread(); //获取当前线程

        OldIrql =KeRaiseIrqlToSynchLevel();//将运行级别提高到DISPATCH_LEVEL级别

        Prcb = KeGetCurrentPrcb();//获取重要的PRCB机构

        if (Prcb->ReadySummary !=0) {    //如果有就绪线程  

           KiAcquireThreadLock(Thread);//锁定当前线程

           KiAcquirePrcbLock(Prcb);//锁定当前PRCB

            if (Prcb->NextThread== NULL) {

                Prcb->NextThread =KiSelectReadyThread(1, Prcb);

            }    //如果没有线程准备好运行的话就向前找下一个可以运行的线程

            if ((NewThread =Prcb->NextThread) != NULL) {

                Thread->Quantum =Thread->QuantumReset;    //前面讲了很久的时限了,看到源代码的时候还是惊喜了一些

                Thread->Priority =KiComputeNewPriority(Thread, 1);    //这里很有趣哦,下面会专门讲这个函数的。   

               KiReleaseThreadLock(Thread);//释放线程锁

               KiSetContextSwapBusy(Thread);//将SwapBusy设置成TURE

 

                //将新线程设置成正在运行

                Prcb->NextThread =NULL;

               Prcb->CurrentThread = NewThread;

                NewThread->State =Running;

                Thread->WaitReason= WrYieldExecution;

               KxQueueReadyThread(Thread, Prcb);//旧线程进入本CPU的就绪队列

                Thread->WaitIrql =APC_LEVEL;   

                ASSERT(OldIrql <=DISPATCH_LEVEL);   

                KiSwapContext(Thread,NewThread);//上下文切换

                Status =STATUS_SUCCESS;   

            } else {

               //如果没有可运行的线程,当前线程继续执行

               KiReleasePrcbLock(Prcb);

               KiReleaseThreadLock(Thread);

            }

        }       

        KeLowerIrql(OldIrql);//恢复CPU的运行级别

        return Status;

    }

}

这段代码不长,就干了一件事,让当前线程主动暂时放弃运行,但是又不进入睡眠,说白了就是为别的线程让路.看完上面的代码和注释后你会发现几个很重要的函数调用,这几个函数就是我们接下来要去研究的内容了。在WRK-v1.2\base\ntos\ke\Ki.h文件里有KiSelectReadyThread的定义:

FORCEINLINE

PKTHREAD

KiSelectReadyThread (

   IN KPRIORITY LowPriority,

   IN PKPRCB Prcb

   )

{

 

   ULONG HighPriority;

   PRLIST_ENTRY ListEntry;

   ULONG PrioritySet;

   PKTHREAD Thread;

   PrioritySet = Prcb->ReadySummary >> LowPriority;//若低于给定的优先级就不考虑了,一定要大于给定的最低优先级

   Thread = NULL;

   if (PrioritySet != 0) {

       KeFindFirstSetLeftMember(PrioritySet, &HighPriority);//找出符合条件的优先级最高的线程

 

       ASSERT((PrioritySet & PRIORITY_MASK(HighPriority)) != 0);

 

       HighPriority += LowPriority;

 

//保证最高优先级列表里不为空

       ASSERT(IsListEmpty(&Prcb->DispatcherReadyListHead[HighPriority])== FALSE);

 

       ListEntry = Prcb->DispatcherReadyListHead[HighPriority].Flink;

       Thread = CONTAINING_RECORD(ListEntry, KTHREAD, WaitListEntry);//获取最高优先级列表里面的第一个线程

 

 

     //保证这个线程是合法的

       ASSERT((KPRIORITY)HighPriority == Thread->Priority);

       ASSERT((Thread->Affinity & AFFINITY_MASK(Prcb->Number)) != 0);

       ASSERT(Thread->NextProcessor == Prcb->Number);

      

       /*从队列中摘下该线程

       if (RemoveEntryList(&Thread->WaitListEntry) != FALSE) {

            //如果队列已空

            Prcb->ReadySummary ^=PRIORITY_MASK(HighPriority);//修改就绪队列位图

       }

   }

 

   ASSERT((Thread == NULL) || (Thread->BasePriority == 0) ||(Thread->Priority != 0));

 

   return Thread;//如果找到合适的线程的话就返回其地址

}

看了代码,你会发现代码量不多吧,但是各种复杂啊,将很多信息放到我们之前的各种复杂结构里面去了,所以在这个重要的代码里面你会发现如此轻易的就进行了,多简洁啊,这时候你随便拿个原理书看就知道书上的原理到底是如何实现的了,革命尚未成功,我们还是要继续努力的,前面我们说过了一个很重要的东西就是分发器,我解释了一些我们为什么要设置这个模块,在这里的代码里你就发现了,ReadSummary是什么啊?其实它就是分发器里面为就绪队列维护的一个摘要了,PRCB里面也有它的访问字段,我们这里就是这样访问的,什么是严格按优先级调度?我要好好的解释一下了:ReadSummary是个位图,只要那个优先级的就绪队列中有线程在等待,这个优先级的标志位就是1.从高位到地位扫描这个位图,遇到的第一个为1的标志位就代表着当时有就绪线程存在最高优先级。从这个队列中取出排在最前面的线程,这就是接下来要运行的线程了。由此可见,Windows的调度原则是严格按照优先级调度的,相同的优先级是按在队列中的先后顺序的。这个部分的内容讲的很清楚了,下面我们还是要继续回到NtYieldExcution()的代码中,接下来就是看看KiComputrNewPriorty(),在WRK-v1.2\base\ntos\ke\Ki.h里代码如下:

FORCEINLINE

SCHAR

KiComputeNewPriority (

   IN PKTHREAD Thread,

    IN SCHAR Adjustment

   )

{

 

SCHAR Priority;

//合理性检查

   ASSERT((Thread->PriorityDecrement >= 0) &&(Thread->PriorityDecrement <= Thread->Priority));

   ASSERT((Thread->Priority < LOW_REALTIME_PRIORITY) ? TRUE :(Thread->PriorityDecrement == 0));

 

   Priority = Thread->Priority;//获取当前进程的优先级

if (Priority < LOW_REALTIME_PRIORITY) {//如果优先级低于低实时优先级的话

      

 Priority = Priority -Thread->PriorityDecrement - Adjustment;//降低优先级分为两部分,函数的参数提供了一部分,另外一个就是当前线程的衰减率了,这个主要是因为内核在唤醒一个线程的时候往往会调高其临时优先级,所以在这里需要降下来

       if (Priority <Thread->BasePriority) {

            Priority =Thread->BasePriority;//这一步是为了不越界

       }

   

       Thread->PriorityDecrement = 0;//这里就将其重置为0了,对应上面的注释应该能够懂了

   }

 

   ASSERT((Thread->BasePriority == 0) || (Priority != 0));//最后再进行一些合理性检查

 

   return Priority;//返回一个新的优先级

}

这函数干嘛的?我前面介绍过QuantumReset的,这个是随优先级而变的,要是一个线程的优先级变了,我们就得修改这部分的值,而上述代码就是去确定优先级的,以便对线程进行微调,如果优先级要改,那么线程就需要挂到其他的队列上去,而QuantumReset也是需要变的。这里需要注意一下,如果一个线程不是实时的话,它的优先级会逐步的降低的,代码里的注释我也写了。判断完优先级之后, KxQueueReadyThread()函数出现了,看名字就看得出大概意思了,将某个线程挂到就绪队列里面去,还是Ki.h里面,代码如下:

FORCEINLINE

VOID

KxQueueReadyThread (

   IN PKTHREAD Thread,

   IN PKPRCB Prcb

   )

{

 

   BOOLEAN Preempted;

   KPRIORITY Priority;

   //合理性检查

   ASSERT(Prcb == KeGetCurrentPrcb());

   ASSERT(Thread->State == Running);

   ASSERT(Thread->NextProcessor == Prcb->Number);

#if !defined(NT_UP)//判断该线程是否允许在本CPU上运行

   if ((Thread->Affinity & Prcb->SetMember) != 0) {

#endif

 

       Thread->State = Ready;//将线程的状态设为就绪

       Preempted = Thread->Preempted;//看一下是否有被抢占

       Thread->Preempted = FALSE;

       Thread->WaitTime = KiQueryLowTickCount();

       Priority = Thread->Priority;

       ASSERT((Priority >= 0) && (Priority <= HIGH_PRIORITY));//检查

       if (Preempted != FALSE) {

            //被抢占的情况下

           InsertHeadList(&Prcb->DispatcherReadyListHead[Priority],

                          &Thread->WaitListEntry);//补偿一下,插到头部

   

       } else {

           InsertTailList(&Prcb->DispatcherReadyListHead[Priority],

                           &Thread->WaitListEntry);//相反插到尾部

       }

 

       Prcb->ReadySummary |= PRIORITY_MASK(Priority);//更新一下ReadySummary

       ASSERT(Priority == Thread->Priority);

       KiReleasePrcbLock(Prcb);//释放PRCB锁,之前被锁定了的

 

#if !defined(NT_UP)

   } else {

       Thread->State = DeferredReady;

       Thread->DeferredProcessor = Prcb->Number;

       //释放锁,放到准备好就绪队列

       KiReleasePrcbLock(Prcb);

       KiDeferredReadyThread(Thread);

   }

#endif

   return;

}

写到这里,你已经差不多能理解书上和资料上那些原理了,因为我已经把最核心的东西都解释出来了,而 KiSwapContext(Thread, NewThread);我们在很早以前就解释,上下文切换,还记得吗?关于线程调度到这里我就差不多要结束了,最后我从宏观角度来讲一下线程调度的问题。

1.Windows内核严格按照优先级调度。

2.同一优先级按照顺序执行。

3.每个正在运行的线程都有一个时间片Quantum,没有被剥夺的情况下,就是自己一直执行下去直到时间用光,如果不是实时的线程的话,该线程的优先级会逐步的降低,但是不会低于其基本优先级。

4.如果线程在运行过程中被抢占了的话,将其挂到相应优先级就绪队列的头部,否则挂到尾部。

5.线程运行中被抢占了的话,这种情况下,线程切换不会马上切换,而要等待内核的级别降到DISPATCH_LEVEL级以下,这时候才能进行线程切换。

  • 3
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java程是Java中的一种机制,用于实现并发编程。线程可以看作是程序执行流的最小单元,能够独立运行并执行任务。Java线程是通过java.lang.Thread类来实现的,可以继承Thread类或实现Runnable接口来创建线程。 Java线程的运用可以通过以下几个步骤来实现: 1. 创建线程对象 在Java中创建线程对象有两种方式,一种是继承Thread类,另一种是实现Runnable接口。继承Thread类需要重写run()方法,实现Runnable接口需要实现run()方法。 2. 启动线程 创建线程对象后,需要调用线程对象的start()方法来启动线程。调用start()方法后,线程进入就绪状态,等待系统调度执行。 3. 线程执行任务 线程启动后,会自动执行run()方法中的任务。在任务执行过程中,可以通过sleep()方法、yield()方法等来控制线程的执行。 4. 线程结束 线程执行完任务后,会自动退出。在多线程编程中,需要注意线程的结束状态,避免出现线程泄漏或死锁等问题。 以下是一个简单的Java线程示例代码,通过继承Thread类来创建线程: ```java public class SimpleThread extends Thread { public void run() { System.out.println("线程开始执行!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程执行完毕!"); } public static void main(String[] args) { SimpleThread thread = new SimpleThread(); thread.start(); System.out.println("主线程执行完毕!"); } } ``` 在上述代码中,我们创建了一个继承自Thread类的SimpleThread类,并在run()方法中定义了线程要执行的任务。在main()方法中,我们创建了一个SimpleThread对象,并调用它的start()方法来启动线程。在start()方法被调用后,线程会自动调用run()方法来执行任务。 除了继承Thread类外,我们还可以通过实现Runnable接口来创建线程。以下是一个实现Runnable接口的示例代码: ```java public class SimpleRunnable implements Runnable { public void run() { System.out.println("线程开始执行!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程执行完毕!"); } public static void main(String[] args) { SimpleRunnable runnable = new SimpleRunnable(); Thread thread = new Thread(runnable); thread.start(); System.out.println("主线程执行完毕!"); } } ``` 在上述代码中,我们创建了一个实现了Runnable接口的SimpleRunnable类,并在run()方法中定义了线程要执行的任务。在main()方法中,我们创建了一个SimpleRunnable对象,并将它作为参数传递给Thread类的构造方法来创建一个新的线程。最后,我们调用线程的start()方法来启动线程。 以上就是Java线程的简单运用,当然Java线程的使用还涉及到线程同步、线程池等高级特性,需要进一步学习和实践。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值