L-Chat(4)——M4.2

1 阅读3分钟

Reactor 模式与工作线程池

1.1 thread_pool.h

#ifndef _THREAD_POOL_H_
#define _THREAD_POOL_H_

#include <pthread.h>
#include <stdint.h>
#include <stddef.h>
#include "../connection.h"
//线程池 = 预先创建一堆工作线程 + 任务队列分发 = 避免频繁创建/销毁线程的开销
//链表本质:数据 + 下一个节点的地址
typedef struct Task{
    Conn *conn;
    uint16_t type;  
    void *payload;          // 消息体数据(必须是堆内存):epoll 缓冲区会被覆盖,必须复制一份
    struct Task *next;      // 下一个任务(链表指针),内部必须用 struct Task
} Task;                     //结构体内部引用自己时必须用struct Task(结构体标签)
                            //结构体要「自引用」?:链表节点要指向同类型的下一个节点              
typedef struct {            //链表节点必须用 malloc(堆内存),Task *t = malloc(sizeof(Task));
    pthread_mutex_t lock;   // 互斥锁:保护任务队列(多线程同时操作队列),保护共享数据,同一时间只允许一个线程访问
    pthread_cond_t notify;  // 条件变量:让线程「等某个条件成立」再继续执行

    Task *head;             // 任务链表头
    Task *tail;             // 任务链表尾

    pthread_t *threads;     // 工作线程数组
    int thread_count;       // 线程数量
    int shutdown;           // 线程池关闭标志
} ThreadPool;

ThreadPool* thread_pool_create(int thread_count);
void thread_pool_submit(
    ThreadPool *pool,
    Conn *conn,
    uint16_t type,
    void *payload,
    size_t payload_len);
#endif

1.2 thread_pool.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "thread_pool.h"
#include "../dispatcher.h"

static void* worker_routine(void *arg){
    ThreadPool *pool = (ThreadPool *)arg;
    while (1) {
        pthread_mutex_lock(&(pool->lock)); // 1. 抢锁

        // 2. 如果队列为空且没有关停,就陷入沉睡,等待老板叫醒
        while (pool->head == NULL && !pool->shutdown) {
            pthread_cond_wait(&(pool->notify), &(pool->lock));
        }

        if (pool->shutdown) {
            pthread_mutex_unlock(&(pool->lock));
            pthread_exit(NULL);
        }

        // 3. 领任务 (从队列头部摘取)
        Task *task = pool->head;
        if (task) {
            pool->head = task->next;
            if (pool->head == NULL) pool->tail = NULL;
        }
        
        pthread_mutex_unlock(&(pool->lock)); // 4. 拿完任务,马上释放锁,让其他兄弟去抢

        // 5. 干活!调用业务分发器 (这部分耗时再长,也不会卡住主线程了)
        if (task) {
            // 真正干活的地方
            dispatch_message(task->conn, task->type, task->payload);
            
            // 干完活,释放老板给这个任务分配的内存
            free(task->payload);
            free(task);
        }
    }
    return NULL;
}

// 初始化线程池
ThreadPool* thread_pool_create(int thread_count) {
    ThreadPool *pool = (ThreadPool *)malloc(sizeof(ThreadPool));
    pool->thread_count = thread_count;
    pool->shutdown = 0;
    pool->head = NULL;
    pool->tail = NULL;

    pthread_mutex_init(&(pool->lock), NULL);
    pthread_cond_init(&(pool->notify), NULL);

    pool->threads = (pthread_t *)malloc(sizeof(pthread_t) * thread_count);
    for (int i = 0; i < thread_count; i++) {
        pthread_create(&(pool->threads[i]), NULL, worker_routine, (void *)pool);
    }

    printf(">>> [System] Thread Pool created with %d workers.\n", thread_count);
    return pool;
}

// 老板派发任务
void thread_pool_submit(ThreadPool *pool, Conn *conn, uint16_t type, void *payload, size_t payload_len) {
    Task *new_task = (Task *)malloc(sizeof(Task));
    new_task->conn = conn;
    new_task->type = type;
    new_task->next = NULL;

    // 【内存安全核心】:由于主线程的 epoll 会不断覆盖 conn->in_buf
    // 必须把 payload 拷贝到安全的堆内存中,让工作线程去慢慢处理
    if (payload_len > 0) {
        new_task->payload = malloc(payload_len);
        memcpy(new_task->payload, payload, payload_len);
    } else {
        new_task->payload = NULL;
    }

    // 加锁,将任务挂到队列尾部
    pthread_mutex_lock(&(pool->lock));
    if (pool->tail == NULL) {
        pool->head = new_task;
        pool->tail = new_task;
    } else {
        pool->tail->next = new_task;
        pool->tail = new_task;
    }
    
    // 通知(唤醒)一个正在睡觉的工作线程起来干活
    pthread_cond_signal(&(pool->notify));
    pthread_mutex_unlock(&(pool->lock));
}

关键点:payload 指针传给任务队列行不行?”

这就是 thread_pool_submit 函数里那个 mallocmemcpy 存在的意义。 如果直接传指针(指向 conn->in_buf),主线程立刻去接收下一个网络包,in_buf 里的数据就会被覆盖。等后台工作线程拿到任务准备处理时,数据早就变成了乱码(内存踩踏)。

1.3测试:

image.png

创建了包含四个线程从线程池