C++多线程-线程调度API

前言

C++多线程编程时,通常是直接创建线程开始执行任务,并不会设置优先级,但是在一些特殊场景下,可能需要为不能的线程任务设置不同的优先级,以使线程优先处理优先级高的任务,对于pthread库, 提供了一些API来设置线程的优先级。

线程调度

在pthread中,定义了以下线程调度机制

#define SCHED_OTHER		0
#define SCHED_FIFO		1
#define SCHED_RR		2
#ifdef __USE_GNU
# define SCHED_BATCH		3
# define SCHED_ISO		4
# define SCHED_IDLE		5
# define SCHED_DEADLINE		6

# define SCHED_RESET_ON_FORK	0x40000000
#endif

以上调度机制可以被划分为普通调度机制实时调度机制,普通调度机制中,不存在优先级的概念,其优先级都为0,所以具有实时调度机制的线程会直接抢占普通调度机制线程。所以我们今天主要探讨实时调度机制。

对于实时调度策略(SCHED_FIFO, SCHED_RR),优先级需要设置为1(最小)到99(最大)中的某个值;
调度器为每个优先级维护了一个待调度线程的列表,当需要进行调度时,调度器访问最高优先级的非空的列表,然后从列表头选择一个线程调度运行;

线程的调度策略决定了一个可调度线程应该放在哪个列表的哪个位置;

所有的调度都是支持抢占的,如果有高优先级的线程准备好运行了,那么它将抢占当前运行的线程,这使得当前线程被重新加入到等待调度的链表中;调度策略决定了在同一个优先级列表中的可调度线程的顺序;

SCHED_FIFO

SCHED_FIFO即先进先出调度,是一种实时调度策略,具有优先级的概念,一定会抢占普通调度策略线程,其内部没有时间片算法的概念。
先进先出调度具有以下规则:

  1. 一个SCHED_FIFO线程被高优先级线程抢占后,会被添加到其优先级等待列表的首部。
  2. 一个阻塞的SCHED_FIFO线程由阻塞状态编程可运行状态时,它将会被添加到其优先级列表的首部。
  3. 当线程一个优先级发生改变时,会出现以下几种对应情况:
  • 优先级提高:它将被添加到新的优先级等待列表的尾部
  • 优先级没变:它在优先级等待列表中的位置不表
  • 优先级变低:它将被添加到新的优先级等待列表的首部

根据POSIX规定,除使用pthread_setschedprio(3)以外,其他改变调度策略或优先级的方式会将对应线程添加到对应优先级等待列表的尾部。


4. 线程如果使用sched_yield(2),它将不会被添加到当前等待列表的尾部。
SCHED_FIFO会一直运行,直到它被IO请求阻塞,或者被更高优先级的线程抢占,或者自己调用了sched_yield.


测试案例
int attach_cpu(int cpu_index) {
  int cpu_num = sysconf(_SC_NPROCESSORS_CONF);
  if (cpu_index < 0 || cpu_index >= cpu_num) {
    printf("cpu index ERROR!\n");
    return -1;
  }

  cpu_set_t mask;
  CPU_ZERO(&mask);
  CPU_SET(cpu_index, &mask);

  if (pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) < 0) {
    printf("set affinity np ERROR!\n");
    return -1;
  }

  return 0;
}

long long a = 0;
long long b = 0;

void *th1(void *para) {
  attach_cpu(0);
  for (long long int i = 0; i < 10000000000; i++) {
    a++;
    printf("a=%lld\n", a);
  }
  return nullptr;
}

void *th2(void *para) {
  attach_cpu(0);
  for (long long int i = 0; i < 10000000000; i++) {
    b++;
    printf("b=%lld\n", b);
  }
  return nullptr;
}

int main() {
  int minlevel = sched_get_priority_min(SCHED_FIFO);
  int maxlevel = sched_get_priority_max(SCHED_FIFO);
  if (minlevel == -1 || maxlevel == -1) {
    printf("error\n");
    return -1;
  }
  printf("minlevel = %d, maxlevel = %d\n", minlevel, maxlevel);
  pthread_t t1;
  pthread_create(&t1, nullptr, th1, nullptr);
  struct sched_param para;
  para.sched_priority = minlevel;
  pthread_setschedparam(t1, SCHED_FIFO, &para);

  pthread_t t2;
  pthread_create(&t2, nullptr, th2, nullptr);
  struct sched_param para2;
  para.sched_priority = maxlevel;
  pthread_setschedparam(t2, SCHED_FIFO, &para2);

  while (1) {
    // printf("a=%lld, b=%lld\n", a, b);
    sleep(1);
  }

  pthread_join(t1, nullptr);
  pthread_join(t2, nullptr);
  return 0;
}

运行该代码,当th2被启动后,将会抢占th1所在的cpu并一直占有,直到自己执行结束。

SCHED_RR

SCHED_RR即轮转调度策略,和SCHED_FIFO大致相同,不同的是SCHED_RR中限制了线程占据cpu的最长时间,当当前线程占用cpu的时间大于时间片,会将当前线程加入到当前等待队列的尾部,然后运行下一个线程。如果当前线程在还没有用完时间片时,被其他高优先级线程抢占了,则下次该线程继续运行时,将只会运行剩余的cpu时间。

可以通过函数sched_rr_get_interval获取线程的时间片,

测试案例

int main(){

int minlevel = sched_get_priority_min(SCHED_RR);
  int maxlevel = sched_get_priority_max(SCHED_RR);
  if (minlevel == -1 || maxlevel == -1) {
    printf("error\n");
    return;
  }
  printf("minlevel = %d, maxlevel = %d\n", minlevel, maxlevel);
  pthread_t t1;
  pthread_create(&t1, nullptr, th1, nullptr);
  struct sched_param para;
  para.sched_priority = minlevel;
  pthread_setschedparam(t1, SCHED_RR, &para);

  //   struct timespec t;

  //   if (sched_rr_get_interval(getpid(), &t) == -1) {
  //     printf("get interval failed\n");
  //     return;
  //   }
  //   printf("t.sec = %ld.%ld\n", t.tv_sec, t.tv_nsec / 1000);

  pthread_t t2;
  pthread_create(&t2, nullptr, th2, nullptr);
  struct sched_param para2;
  para.sched_priority = maxlevel;
  pthread_setschedparam(t2, SCHED_RR, &para2);

  while (1) {
    // printf("a=%lld, b=%lld\n", a, b);
    sleep(1);
  }

  pthread_join(t1, nullptr);
  pthread_join(t2, nullptr);
}

在输出时,a先被输出,当b启动时,由于b的优先级高于a,所以cpu被b占据,但是b在运行了一个cpu时间片后,由被系统将cpu分配给了a,然后a开始输出。

SCHED_OTHER

SCHED_OTHER只能用于优先级为0的线程,SCHED_OTHER策略是所有不需要实时调度线程的统一标准策略;调度器通过动态优先级来决定调用哪个SCHED_OTHER线程,动态优先级是基于nice值的,nice值随着等待运行但是未被调度执行的时间总量的增长而增加;这样的机制保证了所有SCHED_OTHER线程调度的公平性;

总结

响应时间:SCHED_RR对于I/O密集型的进程,可以确保系统对于每个请求的响应时间都是相等的;而SCHED_FIFO对于CPU密集型的进程,可以使得它们运行较长时间,减少进程上下文切换的开销。

因此,在选择使用SCHED_RR或SCHED_FIFO时,需要根据具体的情况进行考虑。如果要求对响应时间有严格的要求,并且进程的执行时间比较短,适合使用SCHED_RR策略;如果对于进程的响应时间要求不那么严格,更注重进程的执行顺序,则适合使用SCHED_FIFO策略。


《C++ Primer》《Effective C++》是C++开发者必不可少的书籍,如果你想入门C++,以及想要精进C++开发技术,这两本书可以说必须要有。此外,《Linux高性能服务器编程》以及《Linux多线程服务端编程:使用muduo C++网络库》.(陈硕)》是快速提高你的linux开发能力的秘籍。《大话设计模式》可以增强我们的模型提取及设计能力,写出更优雅的代码。同时,《操作系统导论》更是开发必读书目,在网上搜索相关资源也要花费一些力气,需要的同学可以关注gzh【程序员DeRozan】,回复【1207】快速免费领取~

你可能感兴趣的:(c++,算法,开发语言)