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

C 语言多线程编程实践指南

2022-03-247.3k 阅读

一、C 语言多线程编程基础

1.1 多线程概念

在传统的单线程程序中,程序按照顺序依次执行各个指令。而多线程编程允许一个程序内同时存在多个执行流,这些执行流被称为线程。每个线程都可以独立地执行代码片段,它们共享进程的资源,如内存空间、文件描述符等。

多线程编程带来了诸多优势。首先,它能提高程序的响应性,例如在图形界面应用程序中,一个线程可以负责处理用户输入,另一个线程进行复杂的计算,这样用户在进行操作时不会感觉到程序卡顿。其次,多线程可以充分利用多核 CPU 的优势,将不同的任务分配到不同的核心上并行执行,从而加速程序的运行。

1.2 C 语言中的多线程库

在 C 语言中,常用的多线程库有 POSIX Threads(简称 Pthreads),它是 Unix 和类 Unix 系统上的标准线程库。在 Windows 系统上,可以使用 Windows 线程库,不过为了实现跨平台,也可以通过 MinGW 等工具来使用 Pthreads。

Pthreads 提供了一系列函数来创建、管理和同步线程。例如,pthread_create 函数用于创建一个新线程,pthread_join 函数用于等待一个线程结束。

二、创建与管理线程

2.1 创建线程

使用 Pthreads 创建线程需要调用 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:线程属性,通常可以设置为 NULL,表示使用默认属性。
  • start_routine:一个函数指针,指向新线程要执行的函数。
  • arg:传递给 start_routine 函数的参数。

下面是一个简单的创建线程的示例:

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

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

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

    // 创建线程
    int result = pthread_create(&my_thread, NULL, thread_function, &arg);
    if (result != 0) {
        printf("Error creating thread: %d\n", result);
        return 1;
    }

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

    // 等待线程结束
    pthread_join(my_thread, NULL);

    printf("Thread has finished. Main thread exits.\n");
    return 0;
}

在上述代码中,pthread_create 创建了一个新线程,该线程执行 thread_function 函数,并将 arg 作为参数传递给它。pthread_join 函数确保主线程等待新创建的线程完成后再继续执行。

2.2 线程终止

线程可以通过以下几种方式终止:

  1. 线程函数返回:如上述示例中,thread_function 函数执行完毕后返回,线程随之终止。
  2. 调用 pthread_exit 函数:线程可以调用 pthread_exit 函数来主动终止自身。其原型为 void pthread_exit(void *retval);retval 可以用来传递线程的返回值。
  3. 其他线程调用 pthread_cancel 函数:一个线程可以调用 pthread_cancel 函数来请求取消另一个线程。被取消的线程可以通过 pthread_setcancelstatepthread_setcanceltype 函数来设置自己对取消请求的处理方式。

下面是一个使用 pthread_exit 的示例:

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

void* thread_function(void* arg) {
    printf("Thread is running. Exiting now.\n");
    pthread_exit(NULL);
}

int main() {
    pthread_t my_thread;

    int result = pthread_create(&my_thread, NULL, thread_function, NULL);
    if (result != 0) {
        printf("Error creating thread: %d\n", result);
        return 1;
    }

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

    pthread_join(my_thread, NULL);

    printf("Thread has finished. Main thread exits.\n");
    return 0;
}

在这个示例中,线程在执行了一段打印语句后,调用 pthread_exit 主动终止自身。

2.3 线程属性

线程属性可以通过 pthread_attr_t 结构体来设置。常用的属性包括栈大小、调度策略等。

例如,要设置线程的栈大小,可以使用以下代码:

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

void* thread_function(void* arg) {
    printf("Thread is running.\n");
    return NULL;
}

int main() {
    pthread_t my_thread;
    pthread_attr_t attr;

    // 初始化属性对象
    pthread_attr_init(&attr);

    // 设置栈大小为 8192 字节
    pthread_attr_setstacksize(&attr, 8192);

    int result = pthread_create(&my_thread, &attr, thread_function, NULL);
    if (result != 0) {
        printf("Error creating thread: %d\n", result);
        return 1;
    }

    // 销毁属性对象
    pthread_attr_destroy(&attr);

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

    pthread_join(my_thread, NULL);

    printf("Thread has finished. Main thread exits.\n");
    return 0;
}

在上述代码中,首先使用 pthread_attr_init 初始化属性对象,然后使用 pthread_attr_setstacksize 设置栈大小,最后使用 pthread_attr_destroy 销毁属性对象。

三、线程同步

3.1 共享资源与竞态条件

当多个线程共享相同的资源(如全局变量、文件描述符等)时,如果没有适当的同步机制,就可能出现竞态条件。竞态条件是指多个线程同时访问和修改共享资源,导致程序的执行结果依赖于线程执行的相对顺序,从而产生不可预测的行为。

例如,考虑以下代码:

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

int shared_variable = 0;

void* increment(void* arg) {
    for (int i = 0; i < 1000000; i++) {
        shared_variable++;
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    int result1 = pthread_create(&thread1, NULL, increment, NULL);
    int result2 = pthread_create(&thread2, NULL, increment, NULL);

    if (result1 != 0 || result2 != 0) {
        printf("Error creating threads.\n");
        return 1;
    }

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    printf("Shared variable value: %d\n", shared_variable);
    return 0;
}

在这段代码中,两个线程同时对 shared_variable 进行递增操作。由于现代 CPU 的优化以及线程调度的不确定性,可能会出现一个线程在读取 shared_variable 的值后,还未进行递增操作时,另一个线程也读取了相同的值,导致最终的结果小于预期的 2000000。

3.2 互斥锁(Mutex)

互斥锁是一种最基本的线程同步机制,用于保证在同一时刻只有一个线程能够访问共享资源。

在 Pthreads 中,使用 pthread_mutex_t 类型来表示互斥锁,相关函数有 pthread_mutex_init 用于初始化互斥锁,pthread_mutex_lock 用于加锁,pthread_mutex_unlock 用于解锁,pthread_mutex_destroy 用于销毁互斥锁。

下面是使用互斥锁解决上述竞态条件问题的示例:

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

int shared_variable = 0;
pthread_mutex_t mutex;

void* increment(void* arg) {
    for (int i = 0; i < 1000000; i++) {
        // 加锁
        pthread_mutex_lock(&mutex);
        shared_variable++;
        // 解锁
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    int result1 = pthread_create(&thread1, NULL, increment, NULL);
    int result2 = pthread_create(&thread2, NULL, increment, NULL);

    if (result1 != 0 || result2 != 0) {
        printf("Error creating threads.\n");
        return 1;
    }

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    printf("Shared variable value: %d\n", shared_variable);
    return 0;
}

在这个示例中,通过在对 shared_variable 进行操作前后分别加锁和解锁,确保了同一时刻只有一个线程能够修改 shared_variable,从而避免了竞态条件。

3.3 条件变量(Condition Variable)

条件变量用于线程之间的同步,它允许线程在某个条件满足时被唤醒。条件变量通常与互斥锁一起使用。

在 Pthreads 中,使用 pthread_cond_t 类型表示条件变量,相关函数有 pthread_cond_init 用于初始化条件变量,pthread_cond_wait 用于等待条件变量,pthread_cond_signal 用于唤醒一个等待该条件变量的线程,pthread_cond_broadcast 用于唤醒所有等待该条件变量的线程,pthread_cond_destroy 用于销毁条件变量。

下面是一个使用条件变量的示例:

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

int shared_variable = 0;
pthread_mutex_t mutex;
pthread_cond_t cond_var;

void* producer(void* arg) {
    for (int i = 0; i < 5; i++) {
        pthread_mutex_lock(&mutex);
        shared_variable++;
        printf("Producer: incremented shared variable to %d\n", shared_variable);
        pthread_cond_signal(&cond_var);
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    return NULL;
}

void* consumer(void* arg) {
    pthread_mutex_lock(&mutex);
    while (shared_variable < 3) {
        printf("Consumer: waiting for shared variable to reach 3\n");
        pthread_cond_wait(&cond_var, &mutex);
    }
    printf("Consumer: shared variable has reached 3. Consuming...\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t producer_thread, consumer_thread;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond_var, NULL);

    int result1 = pthread_create(&producer_thread, NULL, producer, NULL);
    int result2 = pthread_create(&consumer_thread, NULL, consumer, NULL);

    if (result1 != 0 || result2 != 0) {
        printf("Error creating threads.\n");
        return 1;
    }

    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond_var);

    return 0;
}

在这个示例中,生产者线程不断递增 shared_variable,并在每次递增后发送一个条件信号。消费者线程在 shared_variable 小于 3 时等待条件变量,当接收到信号且 shared_variable 达到 3 时,消费者线程继续执行。

3.4 读写锁(Read - Write Lock)

读写锁用于解决读多写少的场景下的同步问题。它允许多个线程同时进行读操作,但只允许一个线程进行写操作。

在 Pthreads 中,使用 pthread_rwlock_t 类型表示读写锁,相关函数有 pthread_rwlock_init 用于初始化读写锁,pthread_rwlock_rdlock 用于加读锁,pthread_rwlock_wrlock 用于加写锁,pthread_rwlock_unlock 用于解锁,pthread_rwlock_destroy 用于销毁读写锁。

下面是一个使用读写锁的示例:

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

int shared_data = 0;
pthread_rwlock_t rw_lock;

void* reader(void* arg) {
    pthread_rwlock_rdlock(&rw_lock);
    printf("Reader: reading shared data: %d\n", shared_data);
    pthread_rwlock_unlock(&rw_lock);
    return NULL;
}

void* writer(void* arg) {
    pthread_rwlock_wrlock(&rw_lock);
    shared_data++;
    printf("Writer: incremented shared data to %d\n", shared_data);
    pthread_rwlock_unlock(&rw_lock);
    return NULL;
}

int main() {
    pthread_t reader_thread1, reader_thread2, writer_thread;

    pthread_rwlock_init(&rw_lock, NULL);

    int result1 = pthread_create(&reader_thread1, NULL, reader, NULL);
    int result2 = pthread_create(&reader_thread2, NULL, reader, NULL);
    int result3 = pthread_create(&writer_thread, NULL, writer, NULL);

    if (result1 != 0 || result2 != 0 || result3 != 0) {
        printf("Error creating threads.\n");
        return 1;
    }

    pthread_join(reader_thread1, NULL);
    pthread_join(reader_thread2, NULL);
    pthread_join(writer_thread, NULL);

    pthread_rwlock_destroy(&rw_lock);

    return 0;
}

在这个示例中,读线程可以同时获取读锁进行读取操作,而写线程在获取写锁时会独占资源,确保写操作的原子性。

四、多线程编程的性能与调试

4.1 多线程性能优化

  1. 减少锁竞争:尽量缩短持有锁的时间,避免不必要的锁嵌套。例如,在上述互斥锁的示例中,如果对 shared_variable 的操作可以拆分成多个步骤,尽量将不需要锁保护的步骤放在锁外执行。
  2. 合理分配任务:根据 CPU 核心数和任务的特性,合理分配任务到不同的线程。例如,对于计算密集型任务,可以平均分配到多个线程利用多核优势;对于 I/O 密集型任务,可以根据 I/O 设备的数量来确定线程数量。
  3. 避免过度线程化:创建过多的线程会增加系统开销,包括线程的创建、销毁以及上下文切换的开销。要根据实际情况权衡线程数量与性能的关系。

4.2 多线程调试

  1. 打印调试信息:在关键代码处添加打印语句,输出线程的执行状态和共享资源的值。但要注意在多线程环境下,打印操作本身也可能存在竞争条件,需要适当同步。
  2. 使用调试工具:例如 GDB 调试器,它支持多线程调试。可以使用 info threads 命令查看当前所有线程的状态,使用 thread <thread - id> 命令切换到指定线程进行调试。
  3. 静态分析工具:如 Valgrind,它可以检测内存泄漏、竞态条件等问题。Valgrind 的 helgrind 工具专门用于检测多线程程序中的竞态条件。

下面是一个使用 GDB 调试多线程程序的简单步骤:

  1. 编译程序:使用 -g 选项编译程序,以便在可执行文件中包含调试信息。例如:gcc -g -pthread -o my_program my_program.c
  2. 启动 GDB:运行 gdb my_program
  3. 设置断点:使用 break <function - name>break <line - number> 命令在关键函数或代码行设置断点。
  4. 运行程序:使用 run 命令运行程序。
  5. 调试线程:使用 info threads 查看线程信息,使用 thread <thread - id> 切换线程,使用 nextstep 等命令单步执行代码。

五、多线程编程的应用场景

5.1 服务器端编程

在服务器程序中,多线程可以用于处理多个客户端的连接请求。例如,一个 Web 服务器可以为每个客户端连接创建一个新线程,这样可以同时处理多个用户的请求,提高服务器的并发处理能力。

下面是一个简单的多线程服务器示例框架(基于 TCP 套接字):

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

#define PORT 8080
#define BACKLOG 10

void* handle_client(void* arg) {
    int client_socket = *((int*)arg);
    char buffer[1024] = {0};
    int valread = read(client_socket, buffer, 1024);
    printf("Received: %s\n", buffer);
    const char* hello = "Hello from server";
    send(client_socket, hello, strlen(hello), 0);
    printf("Message sent\n");
    close(client_socket);
    return NULL;
}

int main(int argc, char const *argv[]) {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    pthread_t tid;

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

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

    // 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听套接字
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    while (1) {
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
            perror("accept");
            continue;
        }

        // 为每个客户端连接创建新线程
        if (pthread_create(&tid, NULL, handle_client, &new_socket) != 0) {
            perror("pthread_create");
            close(new_socket);
        }
    }

    close(server_fd);
    return 0;
}

在这个示例中,主线程负责监听客户端连接,每当有新的连接到来时,创建一个新线程来处理该客户端的请求。

5.2 多媒体处理

在多媒体处理中,多线程可以用于音频和视频的编码、解码、播放等操作。例如,一个视频播放程序可以使用一个线程解码视频帧,另一个线程渲染视频帧,再用一个线程播放音频,从而实现流畅的播放体验。

5.3 科学计算

在科学计算领域,多线程可以加速复杂的数值计算。例如,矩阵运算、模拟仿真等任务可以将数据分割成多个部分,分配到不同的线程中并行计算,最后将结果合并。

例如,下面是一个简单的矩阵乘法多线程实现示例:

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

#define ROWS_A 2
#define COLS_A 3
#define COLS_B 2

int matrix_A[ROWS_A][COLS_A] = {
    {1, 2, 3},
    {4, 5, 6}
};

int matrix_B[COLS_A][COLS_B] = {
    {7, 8},
    {9, 10},
    {11, 12}
};

int result[ROWS_A][COLS_B];

typedef struct {
    int row;
} ThreadArgs;

void* multiply_row(void* arg) {
    ThreadArgs* args = (ThreadArgs*)arg;
    int row = args->row;
    for (int j = 0; j < COLS_B; j++) {
        result[row][j] = 0;
        for (int k = 0; k < COLS_A; k++) {
            result[row][j] += matrix_A[row][k] * matrix_B[k][j];
        }
    }
    return NULL;
}

int main() {
    pthread_t threads[ROWS_A];
    ThreadArgs args[ROWS_A];

    for (int i = 0; i < ROWS_A; i++) {
        args[i].row = i;
        if (pthread_create(&threads[i], NULL, multiply_row, &args[i]) != 0) {
            printf("Error creating thread\n");
            return 1;
        }
    }

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

    for (int i = 0; i < ROWS_A; i++) {
        for (int j = 0; j < COLS_B; j++) {
            printf("%d ", result[i][j]);
        }
        printf("\n");
    }

    return 0;
}

在这个示例中,每个线程负责计算结果矩阵的一行,通过多线程并行计算加速了矩阵乘法的运算。