Preemption Context Switches 和 Synchronization Context Switches

本文通过实验探讨了PreemptionContextSwitches的来源,并对比了多线程无锁与加锁情况下该指标的变化规律。

  • Preemption Context Switches度量的是操作系统任务调度器将处理器中的一个正在运行的线程切换为另一个更高优先级的线程的次数,即发生抢占的次数。
  • Synchronization context switches度量的是由于显式调用线程同步API而发生线程切换的次数,如给多线程共享的变量加锁,多线程共同去修改,有些线程要阻塞在lock,直至占用锁的线程释放lock,这个度量反映的是线程间竞争的程度。


下面的实验来自VTune,旨在探究Preemption Context Switches的来源。


实验一:多线程无锁保护


speedup-example-no-mutex.cpp

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>

#define N 4
#define M 30000

int nwait = 0;

volatile long long sum;
long loops = 6e3;

void set_affinity(int core_id) {
  cpu_set_t cpuset;
  CPU_ZERO(&cpuset);
  CPU_SET(core_id, &cpuset);
  assert(pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset) == 0);
}

void* thread_func(void *arg) {
  set_affinity((int)(long)arg);
  for (int j = 0; j < M; j++) {
	nwait++;
    for (long i = 0; i < loops; i++) 
      sum += i;
    for (long i = 0; i < loops; i++) 
      sum += i*i*i*i*i*i;
  }
}

int main(int argc, char *argv[]) {
  set_affinity(23);
  pthread_t th[N];
  int ret;

  for(unsigned i=0; i<N; ++i) {
    ret  = pthread_create(&th[i], NULL, thread_func, (void*)i);
    assert(!ret && "pthread_create() failed!");
  }

  for(unsigned i=0; i<N; ++i)
    pthread_join(th[i], NULL);

  exit(0);
}



VTune现象:


Preemption Context Switches由两部分组成:clone和Unknown stack frame(s)。

  • 后者的Preemption稳定在5:在这个程序中,共有5个线程在运行,VTune显示每个线程各占1,所以后者的Preemption才稳定在5上。为了验证,我们让N等于8,结果是每个线程各占1,Unknown stack frame(s)处的Preemption稳定在9。



  • clone处的Preemption不是一个确定的数,有可能是6、7、8等。

通过上图可以发现clone处的Preemption都分布在四个子线程中。下面再来一组:


通过比较上面三幅图,我们发现四个子线程所占的Preemption数并不总是均等。为了验证,我们让N等于8,结果如下:

果然clone处的Preemption并不是由子线程均分。不过随着线程数增加,clone处Preemption的增加幅度要大于Unknown stack frame(s)处。

通过上面的现象,我们尝试做出结论:

由于没有锁,所以线程间是独立的,我们单独分析一个线程中Preemption Context Switches的来源即可(其实这种假设是有问题的,因为我们上面提到随着线程数增加,Preemption并没有线性增加,如果各线程间相互独立,理应是线性增加的,不过我们先从简单情况入手)。我们尝试逐步减少子线程执行任务的办法:
  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
        for (long i = 0; i < loops; i++) 
          sum += i*i*i*i*i*i;
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
        for (long i = 0; i < loops; i++) 
          sum += i;
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
        nwait++;
      }
    }

    无clone处的Preemption Context Switches
通过上面我们就断定当子线程计算任务变轻时,clone处的Preemption会变少,这是武断的。因为如下:
  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
        for (long i = 0; i < loops; i++) 
          sum += i;
    	for (long i = 0; i < loops; i++) 
          sum += i;
      }
    }


这个子线程的计算任务要比上面三个中的第一个要轻,但它的Preemption数却要多,所以我初步猜想是第二层for循环的个数决定了clone处的Preemption数,于是做以下验证:
  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
        for (long i = 0; i < loops; i++) 
          sum += i;
    	for (long i = 0; i < loops; i++) 
          sum += i;
    	for (long i = 0; i < loops; i++) 
          sum += i;
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
        for (long i = 0; i < loops; i++) 
          sum += i;
    	for (long i = 0; i < loops; i++) 
          sum += i;
    	for (long i = 0; i < loops; i++) 
          sum += i;
    	for (long i = 0; i < loops; i++) 
          sum += i;
      }
    }


确实是随着for循环的增多,clone处的Preemption在增多,但以此下结论还是不妥,合理的验证还应有以下工作:
  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
        for (long i = 0; i < loops; i++) {
          sum += i;
    	
          sum += i;
    	
          sum += i;
    	
          sum += i;
    	}
      }
    }


奇怪明明这个子线程的工作量和上面验证中的第二个一样,而且它只有一个for,但clone处的Preemption却更多,于是继续做验证:
  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
        for (long i = 0; i < loops; i++) {
    		sum += i;
    		sum += i;
    		sum += i;
    		sum += i;
    		sum += i;
    		sum += i;
    		sum += i;
    	}
      }
    }



最终结论:

也就是说随着第二层for的个数增加,clone处Preemption在增加;如果第二层只有一个for,那么随着这个for中的子句(上面的实验只能说明本例中出现的子句sum+=i有这种情况)的增多,clone处的Preemption在增加。

分析:

如果说这是结论,那为什么?子线程在执行时,频繁被更高优先级的进程给抢占,可能是时间,运行时间,当子线程运行时间长时,系统中更高优先级的进程抢占它的情况更多。果然,我们重新执行上述那些验证程序,发现——clone处Preemption多的程序,它的运行时间越长。
  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
        for (long i = 0; i < loops; i++)
    		sum += i;
    	for (long i = 0; i < loops; i++)
    		sum += i;
    	for (long i = 0; i < loops; i++)
    		sum += i;
    	for (long i = 0; i < loops; i++)
    		sum += i;
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
        for (long i = 0; i < loops; i++) {
    		sum += i;
    		sum += i;
    		sum += i;
    		sum += i;
    	}
      }
    }



至于为什么,或许是因为编译器的优化。这里我们要专注于我们一开始的问题:Preemption Context Switches从何而来。从执行时间而来。当然这只是针对多线程间无锁情况,下面给它加上锁,看看是否有哪个因素也会影响到Preemption Context Switches。

实验二:多线程加锁


speedup-example-mutex-only.cpp

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>

#define N 4
#define M 30000

int nwait = 0;

volatile long long sum;
long loops = 6e3;

pthread_mutex_t mutex;

void set_affinity(int core_id) {
  cpu_set_t cpuset;
  CPU_ZERO(&cpuset);
  CPU_SET(core_id, &cpuset);
  assert(pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset) == 0);
}

void* thread_func(void *arg) {
  set_affinity((int)(long)arg);
  for (int j = 0; j < M; j++) {
	phtread_mutex_lock(&mutex);
	nwait++;
    for (long i = 0; i < loops; i++) 
      sum += i;
	phtread_mutex_unlock(&mutex);
    for (long i = 0; i < loops; i++) 
      sum += i*i*i*i*i*i;
  }
}

int main(int argc, char *argv[]) {
  set_affinity(23);
  pthread_t th[N];
  int ret;

  for(unsigned i=0; i<N; ++i) {
    ret  = pthread_create(&th[i], NULL, thread_func, (void*)i);
    assert(!ret && "pthread_create() failed!");
  }

  for(unsigned i=0; i<N; ++i)
    pthread_join(th[i], NULL);

  exit(0);
}


VTune现象:





接下来我们改变线程数,即N等于8:(我们期望Unknown处的Preemption增加类似线性,而clone处的增加幅度大,即与多线程无锁的情况类似)




Unkown stack frame(s)的对Preemption Context Switches的贡献率任然不如clone。且在同等数目线程下,加锁情况下的clone要比不加锁的制造更多的Preemption Context Switches。如果用我们上面的“时间理论”来解释——加锁的运行时间明显比不加锁要多,也能解释,不过这并不充分,让我们执行以下验证:

  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
    	phtread_mutex_lock(&mutex);
    	nwait++;
        for (long i = 0; i < loops; i++) 
          sum += i;
    	phtread_mutex_unlock(&mutex);
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
    	phtread_mutex_lock(&mutex);
    	nwait++;
    	phtread_mutex_unlock(&mutex);
        for (long i = 0; i < loops; i++) 
          sum += i*i*i*i*i*i;
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
    	phtread_mutex_lock(&mutex);
    	nwait++;
    	phtread_mutex_unlock(&mutex);
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
    	phtread_mutex_lock(&mutex);
    	nwait++;
        for (long i = 0; i < loops; i++) 
          sum += i;
    	for (long i = 0; i < loops; i++) 
          sum += i;
    	phtread_mutex_unlock(&mutex);
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
    	phtread_mutex_lock(&mutex);
    	nwait++;
        for (long i = 0; i < loops; i++) 
          sum += i;
    	for (long i = 0; i < loops; i++) 
          sum += i;
    	for (long i = 0; i < loops; i++) 
          sum += i;
    	for (long i = 0; i < loops; i++) 
          sum += i;
    	phtread_mutex_unlock(&mutex);
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
    	phtread_mutex_lock(&mutex);
    	nwait++;
        for (long i = 0; i < loops; i++) {
          sum += i;
    	  sum += i;
    	  sum += i;
    	  sum += i;
    	}
    	phtread_mutex_unlock(&mutex);
      }
    }



我们发现,基本上加锁情况与无锁情况一致,不过我们还需做以下验证:

  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
    	phtread_mutex_lock(&mutex);
    	nwait++;
    	phtread_mutex_unlock(&mutex);
        for (long i = 0; i < loops; i++) 
          sum += i*i*i*i*i*i;
    	for (long i = 0; i < loops; i++) 
          sum += i*i*i*i*i*i;
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
    	phtread_mutex_lock(&mutex);
    	nwait++;
    	phtread_mutex_unlock(&mutex);
        for (long i = 0; i < loops; i++) 
          sum += i*i*i*i*i*i;
    	for (long i = 0; i < loops; i++) 
          sum += i*i*i*i*i*i;
    	for (long i = 0; i < loops; i++) 
          sum += i*i*i*i*i*i;
    	for (long i = 0; i < loops; i++) 
          sum += i*i*i*i*i*i;
      }
    }


  • void* thread_func(void *arg) {
      set_affinity((int)(long)arg);
      for (int j = 0; j < M; j++) {
    	phtread_mutex_lock(&mutex);
    	nwait++;
    	phtread_mutex_unlock(&mutex);
        for (long i = 0; i < loops; i++) {
    		sum += i*i*i*i*i*i;
    		sum += i*i*i*i*i*i;
    		sum += i*i*i*i*i*i;
    		sum += i*i*i*i*i*i;
    	}
      }
    }



果然,在一定误差可容忍下,for循环是不区别加锁for和不加锁for,它们取得的效果基本一样——随着第二层for的数目增加,clone处的Preemption在增加;不过这里,单个for中增加子句的效果和增加for数目的效果基本一样,这与无锁是不同的;而且,有一个比较重要的差别:
无锁的情况下,
A
void* thread_func(void *arg) {
  set_affinity((int)(long)arg);
  for (int j = 0; j < M; j++) {
	nwait++;
    for (long i = 0; i < loops; i++) 
      sum += i;
    for (long i = 0; i < loops; i++) 
      sum += i*i*i*i*i*i;
  }
}

B
void* thread_func(void *arg) {
  set_affinity((int)(long)arg);
  for (int j = 0; j < M; j++) {
	nwait++;
    for (long i = 0; i < loops; i++) 
      sum += i;
    for (long i = 0; i < loops; i++) 
      sum += i;
  }
}

clone处Preemption的数目基本一致,但在加锁的情况下:
C
void* thread_func(void *arg) {
  set_affinity((int)(long)arg);
  for (int j = 0; j < M; j++) {
	phtread_mutex_lock(&mutex);
	nwait++;
    for (long i = 0; i < loops; i++) 
      sum += i;
	phtread_mutex_unlock(&mutex);
    for (long i = 0; i < loops; i++) 
      sum += i*i*i*i*i*i;
  }
}

D
void* thread_func(void *arg) {
  set_affinity((int)(long)arg);
  for (int j = 0; j < M; j++) {
	phtread_mutex_lock(&mutex);
	nwait++;
    for (long i = 0; i < loops; i++) 
		sum += i;
	 for (long i = 0; i < loops; i++) 
		sum += i;
	phtread_mutex_unlock(&mutex);
  }
}

clone处Preemption的数目不一样,前者要明显多于后者。但是如果我们将后者改为:
E
void* thread_func(void *arg) {
  set_affinity((int)(long)arg);
  for (int j = 0; j < M; j++) {
	phtread_mutex_lock(&mutex);
	nwait++;
    for (long i = 0; i < loops; i++) 
		sum += i;
	phtread_mutex_unlock(&mutex);
	for (long i = 0; i < loops; i++) 
		sum += i;
	}
}

则VTune分析有:


这就和C效果基本一样了。而解释C、D、E三者之间的差异,或许也可以用我们的“时间理论”。执行三者:
C


D


E


虽然D的运行时比C和E稍小,但我们不能直接将无锁情况下的时间理论应用到加锁情况。在说明原因之前,先看另一个程序:

F
void* thread_func(void *arg) {
  set_affinity((int)(long)arg);
  for (int j = 0; j < M; j++) {
	phtread_mutex_lock(&mutex);
	nwait++;
	phtread_mutex_unlock(&mutex);
    for (long i = 0; i < loops; i++) 
		sum += i*i*i*i*i*i;
	for (long i = 0; i < loops; i++) 
		sum += i*i*i*i*i*i;
  }
}


和D在clone处拥有基本一样的Preemption数,但二者的运行时间却大不一样。

F


所以“运行时间不一样,clone处的Preemption数不一样”,在这里就不适用了。看来无锁和加锁还是有个重要差别的。我们都知道在无锁情况下,所有子线程并行执行,VTune中有如下调度:

我们通过大量的观察发现,对于每个线程,每相隔1s就会有一次Preemption Context Switches,所以无锁情况下,随着运行时间的增加,clone处的Preemption数会增多。其实“时间理论”也适用于加锁情况,那为什么会出现上面C、D、E的情况,以及D和F的情况?我们也从调度图入手:
C

D

F

其实加锁和无锁的“时间理论”的区别在于:加锁情况中的C和D(基本串行化),并不是每一个线程中每隔1s就有一个Preemption;而加锁情况中的F(拥有并行化),每个线程中每隔1s会有一个Preemption。
这样对于C和D,由于C的运行时较D长,其中包含的Preemption比D多;而F虽然运行时比D短,但每个线程中的Preemption汇总就会和D一样多。

最终我们得出结论:
Preemption Context Switches的来源是——
对于拥有并行化的程序,运行时间越长,Preemption Context Switches越多;对于加锁导致串行化的程序,运行时间越长,Preemption Context Switches越多;对于加锁仍保留并行化的程序,运行时间越长,Preemption Context Switches越多。

<think>好的,用户现在问的是preemption prioritysub priority的区别。这个问题涉及到嵌入式系统或者实时操作系统中的中断优先级配置,尤其是在使用ARM Cortex-M系列的微控制器时常见的概念。我需要先回忆一下相关的知识点,然后组织一个清晰易懂的解释。 首先,用户之前的问题是关于C语言编译错误的,现在转向了RTOS或中断优先级,可能他们在进行嵌入式开发时遇到了这两个优先级设置的问题。可能是在配置NVIC(嵌套向量中断控制器)时遇到了疑惑,比如在STM32或者其他基于Cortex-M的芯片中设置中断优先级分组时,这两个优先级的作用不同。 接下来,我需要确认preemption prioritysub priority的定义。在Cortex-M中,每个中断的优先级被分为两个部分:抢占优先级(preemption priority)子优先级(sub priority,有时也称为响应优先级)。抢占优先级决定了中断是否可以打断当前正在执行的中断,而子优先级则决定了当多个中断同时 pending 时,哪个先被处理。 但是,用户可能对这些概念的具体应用场景区别不太清楚。比如,他们可能想知道为什么需要两种优先级,或者如何配置它们。此外,不同的优先级分组方式(比如分组0到分组4)会影响这两个优先级占用的位数,这也是需要解释的部分。 还需要考虑到用户可能的背景,他们可能刚开始接触嵌入式开发,或者对RTOS的中断管理不太熟悉。因此,需要用简单的例子来说明这两个优先级的不同作用。例如,当两个中断同时发生时,抢占优先级高的会先执行,而如果抢占优先级相同,则子优先级高的先执行。但如果一个正在处理的中断的抢占优先级较低,另一个高抢占优先级的中断可以抢占它。 另外,用户可能需要知道如何在实际代码中设置这些优先级,比如使用HAL库或者直接写寄存器时的配置方法。可能需要提到NVIC_SetPriorityGrouping函数,或者STM32CubeMX中的优先级分组设置。 还要注意可能的误区,比如子优先级并不能让中断抢占同一抢占优先级的中断,只是决定执行顺序。同时,不同厂商的芯片可能对优先级的位数分配不同,需要参考具体的芯片手册。 总结下来,回答的结构应该是先定义两者,再解释区别,接着举例说明,然后提到配置方法注意事项,最后总结应用场景。这样用户不仅能理解概念,还能知道如何应用。</think>在嵌入式系统(尤其是ARM Cortex-M系列处理器)中,**抢占优先级(Preemption Priority)****子优先级(Sub Priority)**是中断优先级分组中的关键概念,用于控制中断的嵌套响应顺序。以下是它们的区别实际意义: --- ### **1. 抢占优先级(Preemption Priority)** - **作用**: 决定**中断是否可以打断正在执行的中断**(抢占其他中断)。 - 抢占优先级高的中断可以打断抢占优先级低的中断(前提:全局中断已开启)。 - 例如:中断A的抢占优先级为1,中断B为2,则A可以抢占B。 - **特点**: - 优先级数值越小,实际优先级越高(如优先级0 > 优先级1)。 - 抢占优先级不同的中断,按数值高低决定抢占关系。 --- ### **2. 子优先级(Sub Priority,或响应优先级)** - **作用**: 当多个中断**抢占优先级相同**时,决定它们的**执行顺序**。 - 子优先级高的中断会先被响应,但**不能打断正在执行的同抢占优先级中断**。 - 例如:中断AB的抢占优先级均为2,子优先级A为1、B为0,则A先执行,但B不能抢占A。 - **特点**: - 仅在同抢占优先级的中断之间比较。 - 子优先级数值越小,实际优先级越高(与抢占优先级规则相同)。 --- ### **3. 优先级分组(Priority Grouping)** 在ARM Cortex-M中,优先级位数由芯片决定(如4位=16级),需通过**优先级分组**划分抢占优先级子优先级的位数。例如: | 分组模式 | 抢占优先级位数 | 子优先级位数 | 示例(4位总位数) | |----------|----------------|--------------|---------------------------------| | Group 0 | 0位 | 4位 | 所有中断不可抢占,仅按子优先级排序 | | Group 1 | 1位 | 3位 | 抢占优先级:2级,子优先级:8级 | | Group 4 | 4位 | 0位 | 所有中断可抢占,无子优先级 | #### **配置方法(以STM32为例)**: ```c HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2); // 选择分组2(抢占2位,子2位) ``` --- ### **4. 实际场景示例** 假设优先级分组为 `Group 2`(抢占2位,子2位): - **中断A**:抢占优先级=1,子优先级=0 - **中断B**:抢占优先级=1,子优先级=1 - **中断C**:抢占优先级=0,子优先级=3 #### **行为分析**: 1. **中断C**可以抢占AB(抢占优先级更高)。 2. **中断AB**抢占优先级相同,A先执行(子优先级更高)。 3. **中断B**无法抢占A,即使A正在执行。 --- ### **5. 总结** | | 抢占优先级(Preemption) | 子优先级(Sub Priority) | |----------------|--------------------------|--------------------------| | **核心作用** | 决定中断能否互相抢占 | 决定同组中断的执行顺序 | | **数值意义** | 数值越小优先级越高 | 数值越小优先级越高 | | **依赖条件** | 全局中断使能(如`PRIMASK`) | 仅在抢占优先级相同时生效 | | **配置关键** | 优先级分组决定位数 | 优先级分组决定位数 | 合理配置优先级分组数值,是确保实时系统响应性稳定性的关键!
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值