69天探索操作系统-第10天:深入多线程模型:用户线程与内核线程

219 阅读5分钟

pro3.avif

1. 介绍

多线程是现代操作系统的基本概念,它允许在单个进程中同时执行多个线程。本文探讨了线程模型的复杂细节,重点关注用户级线程和内核级线程之间的区别。

2.理解线程基础知识

线程组件

线程由以下组成:

  • 程序计数器(PC):记录下一条要执行的指令。解释:PC是保持线程执行上下文的关键组件。每次指令执行后以及上下文切换期间都会更新PC值。当线程被挂起时,PC值会被存入线程控制块(TCB)中。
  • 寄存器集:包含的工作变量。解释:寄存器能够快速访问数据,对于线程执行至关重要。它们包括通用寄存器、浮点寄存器和栈指针等特殊寄存器。
  • 栈空间:保存函数调用历史和局部变量。解释:每个线程都有自己的栈,它在函数被调用和返回时会增长和收缩。栈中包含每个函数调用时的活动记录(栈帧)。

线程状态

image.png

线程操作

以下是 C 中线程操作的基本实现:

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

// Thread function
void* thread_function(void* arg) {
    int thread_id = *((int*)arg);
    printf("Thread %d is running\n", thread_id);
    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    int id1 = 1, id2 = 2;

    // Create threads
    pthread_create(&thread1, NULL, thread_function, &id1);
    pthread_create(&thread2, NULL, thread_function, &id2);

    // Wait for threads to complete
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

3.用户线程 (ULT)

架构

用户级线程完全在用户空间实现,无需内核参与。

// Simple user-level thread implementation
typedef struct {
    void* stack;
    void* stack_pointer;
    void (*function)(void*);
    void* arg;
    int state;
} user_thread_t;

user_thread_t* create_user_thread(void (*func)(void*), void* arg) {
    user_thread_t* thread = malloc(sizeof(user_thread_t));
    thread->stack = malloc(STACK_SIZE);
    thread->stack_pointer = thread->stack + STACK_SIZE;
    thread->function = func;
    thread->arg = arg;
    thread->state = THREAD_READY;
    return thread;
}

实现细节

  • 用户级线程库管理:线程调度说明:库实现了其自身的调度算法,通常使用轮转或基于优先级的调度。
  • 上下文切换说明:上下文切换完全在用户空间进行,使其比内核级上下文切换更快。
  • 线程同步说明:同步原语在用户空间使用如自旋锁和队列等技术实现。

4.内核级线程(KLT)

架构

内核线程由操作系统直接管理。

#include <pthread.h>

void* kernel_thread_function(void* arg) {
    // Thread implementation
    return NULL;
}

int main() {
    pthread_t kthread;
    pthread_attr_t attr;
    
    pthread_attr_init(&attr);
    pthread_create(&kthread, &attr, kernel_thread_function, NULL);
    pthread_join(kthread, NULL);
    
    return 0;
}

实现细节

内核线程涉及:线程操作的系统调用 说明:每个线程操作都要求从用户模式切换到内核模式,这增加了开销,但提供了更好的系统集成。

5.线程模型

一对多模型

image.png

一对一模型

image.png

多对多模型

image.png

6.实现示例

下面是一个使用用户和内核线程实现简单线程池的完整实现:

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

#define THREAD_POOL_SIZE 4
#define MAX_QUEUE_SIZE 100

typedef struct {
    void (*function)(void*);
    void* arg;
} task_t;

typedef struct {
    task_t* task_queue;
    int queue_size;
    int front;
    int rear;
    pthread_mutex_t queue_mutex;
    pthread_cond_t queue_not_empty;
    pthread_cond_t queue_not_full;
    pthread_t threads[THREAD_POOL_SIZE];
    int shutdown;
} thread_pool_t;

thread_pool_t* create_thread_pool() {
    thread_pool_t* pool = malloc(sizeof(thread_pool_t));
    pool->task_queue = malloc(sizeof(task_t) * MAX_QUEUE_SIZE);
    pool->queue_size = 0;
    pool->front = 0;
    pool->rear = 0;
    pool->shutdown = 0;
    
    pthread_mutex_init(&pool->queue_mutex, NULL);
    pthread_cond_init(&pool->queue_not_empty, NULL);
    pthread_cond_init(&pool->queue_not_full, NULL);
    
    return pool;
}

void* worker_thread(void* arg) {
    thread_pool_t* pool = (thread_pool_t*)arg;
    
    while (1) {
        pthread_mutex_lock(&pool->queue_mutex);
        
        while (pool->queue_size == 0 && !pool->shutdown) {
            pthread_cond_wait(&pool->queue_not_empty, &pool->queue_mutex);
        }
        
        if (pool->shutdown) {
            pthread_mutex_unlock(&pool->queue_mutex);
            pthread_exit(NULL);
        }
        
        task_t task = pool->task_queue[pool->front];
        pool->front = (pool->front + 1) % MAX_QUEUE_SIZE;
        pool->queue_size--;
        
        pthread_mutex_unlock(&pool->queue_mutex);
        pthread_cond_signal(&pool->queue_not_full);
        
        (task.function)(task.arg);
    }
    
    return NULL;
}

void add_task(thread_pool_t* pool, void (*function)(void*), void* arg) {
    pthread_mutex_lock(&pool->queue_mutex);
    
    while (pool->queue_size == MAX_QUEUE_SIZE) {
        pthread_cond_wait(&pool->queue_not_full, &pool->queue_mutex);
    }
    
    task_t task = {function, arg};
    pool->task_queue[pool->rear] = task;
    pool->rear = (pool->rear + 1) % MAX_QUEUE_SIZE;
    pool->queue_size++;
    
    pthread_mutex_unlock(&pool->queue_mutex);
    pthread_cond_signal(&pool->queue_not_empty);
}

void start_thread_pool(thread_pool_t* pool) {
    for (int i = 0; i < THREAD_POOL_SIZE; i++) {
        pthread_create(&pool->threads[i], NULL, worker_thread, pool);
    }
}

void destroy_thread_pool(thread_pool_t* pool) {
    pthread_mutex_lock(&pool->queue_mutex);
    pool->shutdown = 1;
    pthread_mutex_unlock(&pool->queue_mutex);
    
    pthread_cond_broadcast(&pool->queue_not_empty);
    
    for (int i = 0; i < THREAD_POOL_SIZE; i++) {
        pthread_join(pool->threads[i], NULL);
    }
    
    pthread_mutex_destroy(&pool->queue_mutex);
    pthread_cond_destroy(&pool->queue_not_empty);
    pthread_cond_destroy(&pool->queue_not_full);
    
    free(pool->task_queue);
    free(pool);
}

7.性能分析

线程创建时间比较:

  • 用户线程:10-100 微秒
  • 内核线程:100-1000 微秒

上下文切换开销:

  • 用户线程:1-10 微秒
  • 内核线程:10-100 微秒

内存开销:

  • 用户线程:每个线程2-8 KB
  • 内核线程:每个线程4-16 KB

8.最佳实践

  1. 线程池使用说明:实现线程池以减少线程创建的开销,并有效地管理系统资源。线程池保持一组预创建的线程,可用于不同任务。
  2. 适当的线程大小说明:根据系统的硬件能力和工作负载特性选择适当的线程数量。线程过多会导致上下文切换开销。
  3. 资源管理说明:实现适当的清理机制来防止内存泄漏资源耗尽。

9.常见陷阱

  1. 线程增殖:创建太多线程会导致系统资源耗尽,并且由于频繁的上下文切换,会降低性能。
  2. 错误的线程终止:未能正确终止线程会导致资源泄漏和僵尸线程。
  3. 不充分的数据错误处理:未对线程操作实施适当的数据错误处理可能导致应用程序不稳定和资源泄露。

10.参考资料

  1. Operating System Concepts (10th Edition) - Silberschatz, Galvin, and Gagne
  2. Modern Operating Systems (4th Edition) - Andrew S. Tanenbaum
  3. POSIX Threads Programming - computing.llnl.gov/tutorials/p…
  4. Linux Kernel Development (3rd Edition) - Robert Love

11.进一步阅读

  1. Advanced Programming in the UNIX Environment (3rd Edition)
  2. The Art of Multiprocessor Programming
  3. Programming with POSIX Threads
  4. Linux System Programming (2nd Edition)

12.总结

image.png

理解用户级别和内核级别线程之间的差异对于开发高效的多线程应用程序至关重要。每个模型都有其优点和缺点,具体的应用要求决定了选择哪个模型。