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

Linux C语言多线程服务器模型架构设计

2021-04-252.6k 阅读

一、多线程服务器的基本概念

在Linux环境下,使用C语言开发多线程服务器具有显著的优势。多线程服务器允许在同一进程内同时执行多个线程,这些线程共享进程的资源,如内存空间、文件描述符等,从而提高服务器的并发处理能力。

  1. 线程与进程的区别 进程是资源分配的基本单位,拥有独立的地址空间、文件描述符表等资源。而线程是CPU调度的基本单位,线程之间共享进程的资源。创建进程开销较大,因为需要分配独立的资源;而创建线程开销相对较小,因为线程共享资源。例如,在创建一个新进程时,系统需要为其分配独立的内存空间、复制文件描述符表等,而创建线程只需在进程已有的资源基础上,分配少量的栈空间和线程控制块。

  2. 多线程服务器的优势 多线程服务器能够有效利用多核CPU的性能,提高服务器的并发处理能力。在传统的单线程服务器中,一次只能处理一个客户端请求,当处理耗时操作时,其他客户端请求只能等待。而多线程服务器可以为每个客户端请求分配一个独立的线程进行处理,从而提高服务器的响应速度和吞吐量。例如,在一个文件传输服务器中,单线程服务器在传输大文件时,无法同时处理其他客户端的连接请求,而多线程服务器可以为每个文件传输任务分配一个线程,使得多个客户端可以同时进行文件传输。

二、Linux下C语言多线程编程基础

  1. 线程库的引入 在Linux下进行C语言多线程编程,通常使用POSIX线程库(pthread)。POSIX线程库提供了一系列的函数来创建、管理和同步线程。在使用这些函数之前,需要在代码中包含头文件<pthread.h>。例如:
#include <pthread.h>
  1. 线程的创建与终止
    • 线程创建:使用pthread_create函数来创建一个新线程。其函数原型为:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

其中,thread是指向新线程标识符的指针;attr用于指定线程的属性,通常设置为NULL使用默认属性;start_routine是新线程开始执行的函数指针;arg是传递给start_routine函数的参数。例如:

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

void* thread_function(void* arg) {
    printf("This is a new thread. Argument passed: %d\n", *(int*)arg);
    pthread_exit(NULL);
}

int main() {
    pthread_t new_thread;
    int arg = 42;
    int result = pthread_create(&new_thread, NULL, thread_function, &arg);
    if (result != 0) {
        perror("Thread creation failed");
        return 1;
    }
    printf("Main thread continues execution.\n");
    pthread_join(new_thread, NULL);
    return 0;
}
- **线程终止**:线程可以通过以下几种方式终止:
  - 线程函数执行完毕并返回,如上述代码中`thread_function`函数执行完`printf`语句后调用`pthread_exit(NULL)`返回。
  - 线程调用`pthread_exit`函数,该函数原型为`void pthread_exit(void *retval);`,`retval`是线程的返回值,可被`pthread_join`函数获取。
  - 其他线程调用`pthread_cancel`函数来取消该线程,被取消的线程可以通过设置取消状态和清理函数来处理取消请求。

3. 线程同步 由于多个线程共享进程资源,可能会出现资源竞争问题,例如多个线程同时访问和修改同一个变量。为了解决这些问题,需要使用线程同步机制。 - 互斥锁(Mutex):互斥锁用于保护共享资源,确保同一时间只有一个线程能够访问共享资源。使用pthread_mutex_t类型来定义互斥锁变量,通过pthread_mutex_init函数初始化,pthread_mutex_lock函数加锁,pthread_mutex_unlock函数解锁,pthread_mutex_destroy函数销毁。例如:

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_variable = 0;

void* increment_thread(void* arg) {
    pthread_mutex_lock(&mutex);
    shared_variable++;
    printf("Increment thread: shared_variable = %d\n", shared_variable);
    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
}

void* decrement_thread(void* arg) {
    pthread_mutex_lock(&mutex);
    shared_variable--;
    printf("Decrement thread: shared_variable = %d\n", shared_variable);
    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
}

int main() {
    pthread_t inc_thread, dec_thread;
    pthread_create(&inc_thread, NULL, increment_thread, NULL);
    pthread_create(&dec_thread, NULL, decrement_thread, NULL);
    pthread_join(inc_thread, NULL);
    pthread_join(dec_thread, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}
- **条件变量(Condition Variable)**:条件变量用于线程间的同步,它允许线程在某个条件满足时被唤醒。使用`pthread_cond_t`类型定义条件变量,通过`pthread_cond_init`函数初始化,`pthread_cond_wait`函数等待条件变量,`pthread_cond_signal`函数唤醒一个等待的线程,`pthread_cond_broadcast`函数唤醒所有等待的线程,`pthread_cond_destroy`函数销毁条件变量。例如:
#include <stdio.h>
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

void* waiting_thread(void* arg) {
    pthread_mutex_lock(&mutex);
    while (!ready) {
        printf("Waiting thread is waiting...\n");
        pthread_cond_wait(&cond, &mutex);
    }
    printf("Waiting thread is awakened. ready = %d\n", ready);
    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
}

void* signaling_thread(void* arg) {
    pthread_mutex_lock(&mutex);
    ready = 1;
    printf("Signaling thread sets ready to 1.\n");
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
}

int main() {
    pthread_t wait_thread, signal_thread;
    pthread_create(&wait_thread, NULL, waiting_thread, NULL);
    pthread_create(&signal_thread, NULL, signaling_thread, NULL);
    pthread_join(wait_thread, NULL);
    pthread_join(signal_thread, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

三、Linux C语言多线程服务器模型架构设计

  1. 基本架构概述 多线程服务器模型通常包含一个主线程和多个工作线程。主线程负责监听客户端连接请求,将新连接分配给工作线程进行处理。工作线程则负责与客户端进行数据交互,处理业务逻辑。

  2. 监听线程设计 监听线程使用系统调用socketbindlisten来创建一个监听套接字,并开始监听指定端口。当有新的客户端连接请求到达时,监听线程调用accept函数接受连接,并将新连接的套接字传递给工作线程池中的某个工作线程。例如:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>

#define PORT 8080
#define BACKLOG 10

void* handle_connection(void* arg);

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    pthread_t worker_thread;

    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    memset(&client_addr, 0, sizeof(client_addr));

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

    if (listen(server_socket, BACKLOG) == -1) {
        perror("Listen failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port %d...\n", PORT);

    while (1) {
        client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_socket == -1) {
            perror("Accept failed");
            continue;
        }

        if (pthread_create(&worker_thread, NULL, handle_connection, (void*)&client_socket) != 0) {
            perror("Thread creation failed");
            close(client_socket);
        }
    }

    close(server_socket);
    return 0;
}

void* handle_connection(void* arg) {
    int client_socket = *(int*)arg;
    char buffer[1024] = {0};
    ssize_t bytes_read = read(client_socket, buffer, sizeof(buffer));
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("Received from client: %s\n", buffer);
        const char* response = "Message received successfully.";
        write(client_socket, response, strlen(response));
    } else if (bytes_read == -1) {
        perror("Read failed");
    }
    close(client_socket);
    pthread_exit(NULL);
}
  1. 工作线程池设计 工作线程池是一组预先创建的工作线程,它们从任务队列中获取任务并执行。任务队列用于存储新的客户端连接请求。工作线程池的优点是减少线程创建和销毁的开销,提高服务器的性能和响应速度。
    • 任务队列实现:可以使用链表或数组来实现任务队列。这里以链表为例,定义一个任务结构体和任务队列结构体:
typedef struct Task {
    int client_socket;
    struct Task* next;
} Task;

typedef struct TaskQueue {
    Task* head;
    Task* tail;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
} TaskQueue;
- **任务队列操作函数**:包括初始化任务队列、向任务队列添加任务、从任务队列获取任务等函数。
void init_task_queue(TaskQueue* queue) {
    queue->head = NULL;
    queue->tail = NULL;
    pthread_mutex_init(&queue->mutex, NULL);
    pthread_cond_init(&queue->cond, NULL);
}

void add_task(TaskQueue* queue, int client_socket) {
    Task* new_task = (Task*)malloc(sizeof(Task));
    new_task->client_socket = client_socket;
    new_task->next = NULL;

    pthread_mutex_lock(&queue->mutex);
    if (queue->tail == NULL) {
        queue->head = new_task;
        queue->tail = new_task;
    } else {
        queue->tail->next = new_task;
        queue->tail = new_task;
    }
    pthread_cond_signal(&queue->cond);
    pthread_mutex_unlock(&queue->mutex);
}

int get_task(TaskQueue* queue) {
    pthread_mutex_lock(&queue->mutex);
    while (queue->head == NULL) {
        pthread_cond_wait(&queue->cond, &queue->mutex);
    }
    Task* task = queue->head;
    int client_socket = task->client_socket;
    queue->head = task->next;
    if (queue->head == NULL) {
        queue->tail = NULL;
    }
    free(task);
    pthread_mutex_unlock(&queue->mutex);
    return client_socket;
}
- **工作线程函数**:工作线程从任务队列中获取任务并处理。
void* worker_thread_function(void* arg) {
    TaskQueue* queue = (TaskQueue*)arg;
    while (1) {
        int client_socket = get_task(queue);
        char buffer[1024] = {0};
        ssize_t bytes_read = read(client_socket, buffer, sizeof(buffer));
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("Received from client: %s\n", buffer);
            const char* response = "Message received successfully.";
            write(client_socket, response, strlen(response));
        } else if (bytes_read == -1) {
            perror("Read failed");
        }
        close(client_socket);
    }
    pthread_exit(NULL);
}
- **主线程与工作线程池的整合**:主线程接受客户端连接请求,并将其添加到任务队列中,工作线程从任务队列中获取任务进行处理。
int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    TaskQueue queue;
    pthread_t worker_threads[5];

    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    memset(&client_addr, 0, sizeof(client_addr));

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

    if (listen(server_socket, BACKLOG) == -1) {
        perror("Listen failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port %d...\n", PORT);

    init_task_queue(&queue);

    for (int i = 0; i < 5; i++) {
        if (pthread_create(&worker_threads[i], NULL, worker_thread_function, (void*)&queue) != 0) {
            perror("Thread creation failed");
            exit(EXIT_FAILURE);
        }
    }

    while (1) {
        client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_socket == -1) {
            perror("Accept failed");
            continue;
        }
        add_task(&queue, client_socket);
    }

    for (int i = 0; i < 5; i++) {
        pthread_join(worker_threads[i], NULL);
    }

    pthread_mutex_destroy(&queue.mutex);
    pthread_cond_destroy(&queue.cond);
    close(server_socket);
    return 0;
}
  1. 数据处理与业务逻辑 工作线程从客户端接收数据后,需要根据业务需求进行处理。例如,在一个简单的聊天服务器中,工作线程需要解析接收到的消息格式,将消息转发给其他客户端。在一个文件传输服务器中,工作线程需要处理文件的读取、写入和传输逻辑。
    • 消息解析:假设客户端发送的消息格式为“命令:参数”,工作线程可以使用strtok函数来解析消息。
void handle_message(char* buffer) {
    char* command = strtok(buffer, ":");
    char* argument = strtok(NULL, ":");
    if (command != NULL && argument != NULL) {
        if (strcmp(command, "SEND") == 0) {
            // 处理发送消息的逻辑
            printf("Received SEND command with argument: %s\n", argument);
        } else if (strcmp(command, "RECEIVE") == 0) {
            // 处理接收消息的逻辑
            printf("Received RECEIVE command with argument: %s\n", argument);
        }
    }
}
- **业务逻辑实现**:根据具体的业务需求,实现相应的功能。例如,在聊天服务器中,需要维护一个在线用户列表,将消息转发给指定的用户。
typedef struct User {
    char username[50];
    int socket;
    struct User* next;
} User;

User* user_list = NULL;

void add_user(const char* username, int socket) {
    User* new_user = (User*)malloc(sizeof(User));
    strcpy(new_user->username, username);
    new_user->socket = socket;
    new_user->next = user_list;
    user_list = new_user;
}

void remove_user(int socket) {
    User* current = user_list;
    User* prev = NULL;
    while (current != NULL && current->socket != socket) {
        prev = current;
        current = current->next;
    }
    if (current != NULL) {
        if (prev == NULL) {
            user_list = current->next;
        } else {
            prev->next = current->next;
        }
        free(current);
    }
}

void forward_message(const char* message, int sender_socket) {
    User* current = user_list;
    while (current != NULL) {
        if (current->socket != sender_socket) {
            write(current->socket, message, strlen(message));
        }
        current = current->next;
    }
}

四、性能优化与注意事项

  1. 性能优化
    • 减少锁竞争:在多线程编程中,锁的使用可能会导致性能瓶颈。尽量减少锁的持有时间,将临界区代码优化到最小。例如,在任务队列的实现中,get_taskadd_task函数中的锁操作尽量简洁,避免在锁内执行耗时操作。
    • 使用线程局部存储(TLS):对于每个线程需要独立使用的数据,可以使用线程局部存储。在POSIX线程库中,可以使用pthread_key_create函数创建一个线程局部存储键,使用pthread_setspecific函数设置线程局部存储的值,使用pthread_getspecific函数获取线程局部存储的值。例如:
pthread_key_t tls_key;

void* thread_function(void* arg) {
    // 设置线程局部存储的值
    pthread_setspecific(tls_key, (void*)123);
    // 获取线程局部存储的值
    int value = (int)pthread_getspecific(tls_key);
    printf("Thread-specific value: %d\n", value);
    pthread_exit(NULL);
}

int main() {
    pthread_t new_thread;
    pthread_key_create(&tls_key, NULL);
    pthread_create(&new_thread, NULL, thread_function, NULL);
    pthread_join(new_thread, NULL);
    pthread_key_delete(tls_key);
    return 0;
}
- **合理设置线程数量**:根据服务器的硬件资源和业务负载,合理设置工作线程的数量。如果线程数量过多,会导致线程上下文切换开销增大;如果线程数量过少,无法充分利用多核CPU的性能。可以通过实验和性能测试来确定最优的线程数量。

2. 注意事项 - 资源管理:多个线程共享进程资源,需要注意资源的正确释放。例如,在使用完文件描述符后,需要及时关闭;在使用完动态分配的内存后,需要及时释放。 - 异常处理:在多线程编程中,异常处理更加复杂。需要考虑线程间的同步和错误传递。例如,当一个线程发生错误时,需要通知其他相关线程,并进行相应的处理。 - 线程安全函数:在多线程环境下,需要使用线程安全的函数。一些标准库函数,如strtok函数不是线程安全的,在多线程环境下使用可能会导致数据竞争问题。可以使用strtok_r函数替代strtok函数,strtok_r函数是线程安全的。

通过以上设计和优化,可以构建一个高效、稳定的Linux C语言多线程服务器模型,满足不同的业务需求。在实际开发中,还需要根据具体的应用场景进行进一步的调整和优化。