MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言线程创建的上下文切换

2024-03-033.8k 阅读

线程基础概念

线程是什么

在深入探讨 Linux C 语言线程创建的上下文切换之前,我们首先要理解线程的基本概念。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的大部分资源,如地址空间、打开的文件描述符等。与进程相比,线程的创建和销毁开销更小,线程间的切换也更为高效,这使得多线程编程成为提高程序并发性能的重要手段。

线程与进程的区别

  1. 资源占用:进程拥有独立的地址空间,进程间的地址空间相互隔离,一个进程崩溃不会影响其他进程。而线程共享所属进程的地址空间,多个线程可以访问相同的变量和数据结构,这虽然提高了数据共享的便利性,但也增加了数据竞争和同步的复杂性。
  2. 调度和切换:进程的调度和切换涉及到地址空间的切换等一系列开销较大的操作。线程的调度和切换主要是 CPU 上下文的切换,由于线程共享地址空间,所以线程间的切换开销相对较小。

为什么使用线程

  1. 提高并发性能:在多核处理器环境下,多线程程序可以充分利用多核的优势,将不同的任务分配到不同的线程上并行执行,从而提高程序的整体运行效率。例如,在一个网络服务器程序中,可以使用一个线程处理网络连接的监听,其他线程处理具体的请求响应,这样可以同时处理多个客户端的请求,提高服务器的并发处理能力。
  2. 改善用户体验:在图形用户界面(GUI)程序中,使用多线程可以避免主线程在执行耗时操作时导致界面冻结。例如,将文件下载等耗时任务放在一个单独的线程中执行,主线程可以继续响应用户的操作,如点击按钮、拖动窗口等,从而提升用户体验。

Linux C 语言线程创建

pthread 库简介

在 Linux 系统下进行 C 语言多线程编程,通常使用 pthread 库(POSIX threads library)。pthread 库提供了一套丰富的函数接口,用于创建、管理和同步线程。要使用 pthread 库,需要在程序中包含头文件 <pthread.h>,并且在编译时链接 pthread 库,例如使用 gcc -pthread 选项。

创建线程的函数

在 pthread 库中,创建线程的主要函数是 pthread_create,其函数原型如下:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • thread:指向 pthread_t 类型变量的指针,该变量用于存储新创建线程的标识符。
  • attr:指向 pthread_attr_t 类型变量的指针,用于设置线程的属性,如栈大小、调度策略等。如果为 NULL,则使用默认属性。
  • start_routine:指向线程函数的指针,新创建的线程将从该函数开始执行。
  • arg:传递给线程函数的参数。

简单的线程创建示例

下面是一个简单的示例代码,展示如何使用 pthread_create 函数创建一个线程:

#include <stdio.h>
#include <pthread.h>

// 线程函数
void* thread_function(void* arg) {
    printf("This is a thread. The argument passed is: %d\n", *(int*)arg);
    pthread_exit(NULL);
}

int main() {
    pthread_t my_thread;
    int arg = 42;

    // 创建线程
    if (pthread_create(&my_thread, NULL, thread_function, &arg) != 0) {
        perror("Failed to create thread");
        return 1;
    }

    printf("Main thread continues execution\n");

    // 等待线程结束
    if (pthread_join(my_thread, NULL) != 0) {
        perror("Failed to join thread");
        return 2;
    }

    printf("Main thread exits\n");
    return 0;
}

在上述代码中:

  1. 定义了一个线程函数 thread_function,它接收一个 void* 类型的参数,并将其转换为 int* 类型后打印出来。
  2. main 函数中,定义了一个 pthread_t 类型的变量 my_thread 用于存储线程标识符,以及一个 int 类型的变量 arg 作为传递给线程函数的参数。
  3. 使用 pthread_create 函数创建线程,并检查返回值以确保线程创建成功。
  4. 使用 pthread_join 函数等待线程结束,pthread_join 函数会阻塞调用线程,直到指定的线程结束。

上下文切换概述

什么是上下文切换

上下文切换(Context Switching)是指当操作系统需要从一个正在执行的进程或线程切换到另一个进程或线程时,所进行的一系列操作。这些操作包括保存当前进程或线程的执行状态(上下文),并恢复要切换到的进程或线程的执行状态。上下文主要包括 CPU 寄存器的值、程序计数器(PC)的值、栈指针(SP)的值以及其他一些与进程或线程执行相关的状态信息。

上下文切换的原因

  1. 时间片耗尽:在分时操作系统中,每个进程或线程被分配一个时间片(Time Slice),当时间片用完时,操作系统会进行上下文切换,将 CPU 资源分配给其他进程或线程。
  2. I/O 阻塞:当一个进程或线程发起 I/O 操作时,由于 I/O 操作通常比较耗时,该进程或线程会进入阻塞状态,此时操作系统会切换到其他可运行的进程或线程,以充分利用 CPU 资源。
  3. 优先级调度:操作系统根据进程或线程的优先级进行调度,如果有更高优先级的进程或线程进入可运行状态,当前正在执行的进程或线程可能会被抢占,从而引发上下文切换。

上下文切换的开销

上下文切换虽然是操作系统实现多任务处理的重要机制,但它也带来了一定的开销:

  1. 保存和恢复上下文的开销:保存和恢复 CPU 寄存器的值、程序计数器的值等上下文信息需要执行一系列的指令,这会消耗 CPU 时间。
  2. 高速缓存(Cache)失效:上下文切换可能导致 CPU 高速缓存中的数据失效,因为不同的进程或线程可能使用不同的内存地址空间。当切换到新的进程或线程后,需要重新从内存中加载数据到高速缓存,这会增加内存访问的延迟。
  3. 调度算法的开销:操作系统需要运行调度算法来决定下一个要执行的进程或线程,这也会消耗一定的 CPU 资源。

Linux C 语言线程创建中的上下文切换

线程创建时上下文切换的过程

当使用 pthread_create 函数创建一个新线程时,操作系统会执行以下主要步骤来完成上下文切换:

  1. 分配资源:操作系统为新线程分配必要的资源,如栈空间、线程控制块(TCB,Thread Control Block)等。线程控制块用于存储线程的相关信息,包括线程的状态、优先级、上下文等。
  2. 初始化上下文:操作系统初始化新线程的上下文,将线程函数的起始地址加载到程序计数器(PC)中,并设置栈指针(SP)等寄存器的值,使其指向新分配的栈空间。同时,将传递给线程函数的参数压入栈中。
  3. 调度决策:操作系统根据调度算法决定是否立即调度新创建的线程执行。如果新线程的优先级足够高,或者当前正在执行的线程时间片耗尽等原因,操作系统可能会立即进行上下文切换,将 CPU 资源分配给新创建的线程。
  4. 上下文切换:如果决定调度新线程执行,操作系统会保存当前线程的上下文,包括 CPU 寄存器的值、程序计数器的值等,并恢复新线程的上下文。这样,新线程就可以从线程函数的起始地址开始执行。

代码示例分析上下文切换

下面我们通过一个更复杂的示例代码来分析线程创建过程中的上下文切换:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 线程函数
void* thread_function(void* arg) {
    printf("Thread started. Argument: %d\n", *(int*)arg);
    for (int i = 0; i < 5; i++) {
        printf("Thread is running, iteration %d\n", i);
        sleep(1);
    }
    printf("Thread finished\n");
    pthread_exit(NULL);
}

int main() {
    pthread_t my_thread;
    int arg = 10;

    // 创建线程
    if (pthread_create(&my_thread, NULL, thread_function, &arg) != 0) {
        perror("Failed to create thread");
        return 1;
    }

    printf("Main thread continues execution\n");
    for (int i = 0; i < 3; i++) {
        printf("Main thread is running, iteration %d\n", i);
        sleep(1);
    }

    // 等待线程结束
    if (pthread_join(my_thread, NULL) != 0) {
        perror("Failed to join thread");
        return 2;
    }

    printf("Main thread exits\n");
    return 0;
}

在这个示例中:

  1. 线程函数 thread_function 接收一个整数参数,并在一个循环中打印当前迭代次数,每次迭代间隔 1 秒。
  2. main 函数创建一个新线程,并在主线程中也有一个循环,同样打印当前迭代次数,每次迭代间隔 1 秒。
  3. pthread_create 函数创建新线程后,操作系统可能会根据调度算法决定何时调度新线程执行。在新线程开始执行前,主线程会继续执行其 for 循环。当新线程被调度执行时,操作系统会保存主线程的上下文,并恢复新线程的上下文,新线程开始从 thread_function 的起始地址执行。在新线程执行过程中,如果主线程的时间片到来,操作系统又会保存新线程的上下文,恢复主线程的上下文,主线程继续执行。这种切换过程在程序运行过程中会不断发生,直到新线程和主线程都完成各自的任务。

影响线程上下文切换的因素

  1. 线程优先级:优先级较高的线程更容易被调度执行,可能导致优先级较低的线程频繁被上下文切换出去。可以通过 pthread_setschedparam 函数来设置线程的优先级。例如:
#include <pthread.h>
#include <sched.h>
#include <stdio.h>

void* high_priority_thread(void* arg) {
    struct sched_param param;
    param.sched_priority = sched_get_priority_max(SCHED_FIFO);
    pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);
    printf("High priority thread started\n");
    // 线程执行代码
    pthread_exit(NULL);
}

void* low_priority_thread(void* arg) {
    struct sched_param param;
    param.sched_priority = sched_get_priority_min(SCHED_FIFO);
    pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);
    printf("Low priority thread started\n");
    // 线程执行代码
    pthread_exit(NULL);
}

int main() {
    pthread_t high_thread, low_thread;
    if (pthread_create(&high_thread, NULL, high_priority_thread, NULL) != 0) {
        perror("Failed to create high priority thread");
        return 1;
    }
    if (pthread_create(&low_thread, NULL, low_priority_thread, NULL) != 0) {
        perror("Failed to create low priority thread");
        return 1;
    }

    if (pthread_join(high_thread, NULL) != 0) {
        perror("Failed to join high priority thread");
        return 2;
    }
    if (pthread_join(low_thread, NULL) != 0) {
        perror("Failed to join low priority thread");
        return 2;
    }

    return 0;
}

在上述代码中,通过 pthread_setschedparam 函数分别设置了两个线程的优先级,高优先级线程会更优先获得 CPU 资源,从而影响上下文切换的频率。 2. 线程数量:系统中线程数量越多,竞争 CPU 资源的情况就越激烈,上下文切换的频率也就越高。过多的线程可能会导致上下文切换开销过大,反而降低程序的整体性能。 3. 线程执行时间:如果线程执行的任务时间较长,在其执行过程中可能会发生多次上下文切换。例如,一个长时间运行的计算密集型线程可能会占用较多的 CPU 时间,导致其他线程等待调度,增加上下文切换的次数。

上下文切换的性能优化

减少不必要的上下文切换

  1. 优化线程设计:合理设计线程的任务,避免创建过多不必要的线程。例如,在一个网络服务器中,可以使用线程池来管理线程,复用已有的线程处理新的请求,而不是为每个请求都创建一个新线程,这样可以减少线程创建和上下文切换的开销。
  2. 合并任务:将一些小的任务合并成一个较大的任务,在一个线程中执行,减少线程间的切换。例如,在数据处理程序中,如果有多个对数据的简单计算操作,可以将这些操作合并在一个线程中依次执行,而不是为每个计算操作创建一个单独的线程。

优化调度策略

  1. 设置合适的优先级:根据任务的重要性和紧急程度,合理设置线程的优先级。对于关键任务的线程,设置较高的优先级,使其能够优先获得 CPU 资源,减少上下文切换对其执行的影响。但要注意避免优先级反转等问题,即高优先级线程被低优先级线程阻塞的情况。
  2. 选择合适的调度算法:Linux 系统支持多种调度算法,如 SCHED_OTHER(分时调度算法)、SCHED_FIFO(先来先服务调度算法)、SCHED_RR(时间片轮转调度算法)等。根据应用场景的不同,选择合适的调度算法可以优化上下文切换的性能。例如,对于实时性要求较高的应用,如音频和视频处理,SCHED_FIFO 或 SCHED_RR 调度算法可能更合适。

利用多核处理器

  1. 线程亲和性(Thread Affinity):通过设置线程亲和性,可以将线程绑定到特定的 CPU 核心上执行,减少线程在不同 CPU 核心间切换带来的开销。在 Linux 系统中,可以使用 sched_setaffinity 函数来设置线程亲和性。例如:
#include <pthread.h>
#include <stdio.h>
#include <sched.h>

void* thread_function(void* arg) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(0, &cpuset); // 将线程绑定到 CPU 核心 0
    if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) == -1) {
        perror("sched_setaffinity");
    }
    printf("Thread is running on CPU core %d\n", sched_getcpu());
    // 线程执行代码
    pthread_exit(NULL);
}

int main() {
    pthread_t my_thread;
    if (pthread_create(&my_thread, NULL, thread_function, NULL) != 0) {
        perror("Failed to create thread");
        return 1;
    }

    if (pthread_join(my_thread, NULL) != 0) {
        perror("Failed to join thread");
        return 2;
    }

    return 0;
}

在上述代码中,通过 sched_setaffinity 函数将线程绑定到 CPU 核心 0 上执行,这样可以减少线程在不同 CPU 核心间上下文切换的开销。 2. 负载均衡:在多核处理器环境下,合理分配任务到不同的 CPU 核心上,实现负载均衡。操作系统通常会有一些内置的负载均衡机制,但在应用程序层面,也可以通过一些策略来手动分配任务。例如,将计算密集型任务分配到不同的 CPU 核心上并行执行,避免某个 CPU 核心负载过高,而其他核心闲置的情况。

总结上下文切换与线程创建的关系

上下文切换是 Linux 系统实现多线程编程的关键机制之一,它在保证多个线程并发执行的同时,也带来了一定的开销。在使用 C 语言进行 Linux 线程编程时,了解线程创建过程中的上下文切换原理和影响因素,对于编写高效、稳定的多线程程序至关重要。通过合理设计线程、优化调度策略以及利用多核处理器等方法,可以有效减少上下文切换的开销,提高程序的性能。同时,在实际开发中,还需要注意线程同步和数据竞争等问题,以确保多线程程序的正确性。总之,深入理解上下文切换与线程创建的关系,并在实践中加以优化,是成为一名优秀的 Linux C 语言多线程开发者的重要一步。