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

Linux C语言多线程创建的高级玩法

2023-10-014.0k 阅读

1. 多线程基础回顾

在深入探讨 Linux C 语言多线程创建的高级玩法之前,我们先来回顾一下多线程编程的基础知识。

线程是进程中的一个执行流,一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。在 Linux 环境下,我们主要使用 POSIX 线程库(通常称为 pthread 库)来进行多线程编程。

要使用 pthread 库,我们需要包含头文件 <pthread.h>。在编译时,需要链接 pthread 库,一般使用 -lpthread 选项。

1.1 简单线程创建示例

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

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

// 线程执行函数
void* thread_function(void* arg) {
    printf("This is a thread.\n");
    return NULL;
}

int main() {
    pthread_t thread;
    // 创建线程
    int ret = pthread_create(&thread, NULL, thread_function, NULL);
    if (ret != 0) {
        printf("Error creating thread: %d\n", ret);
        return 1;
    }
    // 等待线程结束
    ret = pthread_join(thread, NULL);
    if (ret != 0) {
        printf("Error joining thread: %d\n", ret);
        return 1;
    }
    printf("Thread has finished.\n");
    return 0;
}

在这个示例中,我们首先定义了一个 thread_function 函数,它将在线程中执行。然后在 main 函数中,使用 pthread_create 函数创建一个新线程。pthread_create 的第一个参数是指向 pthread_t 类型变量的指针,用于存储新线程的标识符;第二个参数通常为 NULL,用于设置线程属性;第三个参数是线程执行函数的指针;第四个参数是传递给线程执行函数的参数。

创建线程后,我们使用 pthread_join 函数等待线程结束。pthread_join 的第一个参数是要等待的线程标识符,第二个参数用于获取线程执行函数的返回值(这里我们不需要,所以设为 NULL)。

2. 线程属性的深入探讨

2.1 线程属性结构体

pthread_create 函数的第二个参数中,可以传入一个指向 pthread_attr_t 类型结构体的指针,用于设置线程的属性。这个结构体定义在 <pthread.h> 中,包含了许多成员来控制线程的各种特性。

要使用 pthread_attr_t 结构体,首先需要对其进行初始化,使用 pthread_attr_init 函数。在使用完毕后,需要使用 pthread_attr_destroy 函数来释放相关资源。

pthread_attr_t attr;
pthread_attr_init(&attr);
// 设置线程属性
//...
pthread_create(&thread, &attr, thread_function, NULL);
pthread_attr_destroy(&attr);

2.2 常见线程属性设置

2.2.1 线程栈大小 线程栈用于存储线程函数的局部变量、函数调用栈等。可以通过 pthread_attr_setstacksize 函数来设置线程栈的大小。

size_t stack_size = 8192 * 1024; // 8MB 栈大小
pthread_attr_setstacksize(&attr, stack_size);

如果栈大小设置过小,可能会导致线程在运行过程中栈溢出;而设置过大则会浪费内存资源。

2.2.2 线程调度策略与优先级 Linux 支持多种线程调度策略,如 SCHED_OTHER(普通调度策略)、SCHED_FIFO(先来先服务调度策略)和 SCHED_RR(时间片轮转调度策略)。可以使用 pthread_attr_setschedpolicy 函数来设置调度策略,并使用 pthread_attr_setschedparam 函数来设置调度参数,包括优先级。

struct sched_param param;
param.sched_priority = 50; // 优先级,值越大优先级越高
pthread_attr_setschedpolicy(&attr, SCHED_RR);
pthread_attr_setschedparam(&attr, &param);

需要注意的是,设置调度策略和优先级通常需要较高的权限,普通用户可能无法成功设置某些策略和优先级。

3. 线程同步机制的高级应用

3.1 互斥锁(Mutex)的深入使用

互斥锁是一种最基本的线程同步机制,用于保护共享资源,确保同一时间只有一个线程可以访问共享资源。

3.1.1 死锁问题及避免 死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放资源时,就会发生死锁。例如:

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

void* thread1_function(void* arg) {
    pthread_mutex_lock(&mutex1);
    // 模拟一些操作
    sleep(1);
    pthread_mutex_lock(&mutex2);
    // 访问共享资源
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}

void* thread2_function(void* arg) {
    pthread_mutex_lock(&mutex2);
    // 模拟一些操作
    sleep(1);
    pthread_mutex_lock(&mutex1);
    // 访问共享资源
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}

在这个例子中,如果 thread1 先获取了 mutex1thread2 先获取了 mutex2,然后它们都试图获取对方持有的锁,就会发生死锁。

为了避免死锁,可以采用以下方法:

  • 按照固定顺序获取锁:例如,总是先获取 mutex1,再获取 mutex2,这样就不会出现相互等待的情况。
  • 使用超时机制:在获取锁时设置一个超时时间,如果在超时时间内未能获取锁,则放弃并进行其他处理。可以使用 pthread_mutex_timedlock 函数来实现。

3.1.2 递归互斥锁 递归互斥锁允许同一个线程多次获取锁,而不会导致死锁。在某些情况下,一个线程可能需要多次访问被互斥锁保护的资源,例如一个递归函数访问共享资源。

pthread_mutexattr_t mutex_attr;
pthread_mutexattr_init(&mutex_attr);
pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_RECURSIVE);

pthread_mutex_t recursive_mutex;
pthread_mutex_init(&recursive_mutex, &mutex_attr);

void recursive_function() {
    pthread_mutex_lock(&recursive_mutex);
    // 访问共享资源
    // 递归调用
    recursive_function();
    pthread_mutex_unlock(&recursive_mutex);
}

3.2 条件变量(Condition Variable)的高级应用

条件变量用于线程间的同步,它允许线程等待某个条件满足后再继续执行。

3.2.1 生产者 - 消费者模型 生产者 - 消费者模型是条件变量的一个经典应用场景。假设有一个共享缓冲区,生产者线程向缓冲区中写入数据,消费者线程从缓冲区中读取数据。

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

#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
int count = 0;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;

void* producer(void* arg) {
    int i;
    for (i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);
        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&not_full, &mutex);
        }
        buffer[in] = i;
        printf("Produced: %d\n", buffer[in]);
        in = (in + 1) % BUFFER_SIZE;
        count++;
        pthread_cond_signal(&not_empty);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void* consumer(void* arg) {
    int i;
    for (i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);
        while (count == 0) {
            pthread_cond_wait(&not_empty, &mutex);
        }
        int data = buffer[out];
        printf("Consumed: %d\n", data);
        out = (out + 1) % BUFFER_SIZE;
        count--;
        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t producer_thread, consumer_thread;
    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);
    return 0;
}

在这个例子中,生产者线程在缓冲区满时等待 not_full 条件变量,消费者线程在缓冲区空时等待 not_empty 条件变量。当条件满足时,通过 pthread_cond_signal 函数唤醒等待的线程。

4. 线程池技术

4.1 线程池概念

线程池是一种多线程处理模式,它预先创建一定数量的线程,并将这些线程放入线程池中。当有任务到来时,从线程池中取出一个线程来执行任务,任务完成后,线程并不销毁,而是返回线程池等待下一个任务。

线程池的优点包括:

  • 减少线程创建和销毁的开销,提高系统性能。
  • 控制并发线程的数量,避免过多线程导致系统资源耗尽。

4.2 线程池实现示例

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

#define THREADS 4
#define QUEUE_SIZE 10

// 任务结构体
typedef struct {
    void (*function)(void*);
    void* arg;
} task;

// 线程池结构体
typedef struct {
    task queue[QUEUE_SIZE];
    int front;
    int rear;
    int count;
    pthread_mutex_t mutex;
    pthread_cond_t not_empty;
    pthread_cond_t not_full;
    int shutdown;
} thread_pool;

// 线程执行函数
void* worker(void* arg) {
    thread_pool* pool = (thread_pool*)arg;
    while (1) {
        pthread_mutex_lock(&pool->mutex);
        while (pool->count == 0 &&!pool->shutdown) {
            pthread_cond_wait(&pool->not_empty, &pool->mutex);
        }
        if (pool->shutdown && pool->count == 0) {
            pthread_mutex_unlock(&pool->mutex);
            pthread_exit(NULL);
        }
        task t = pool->queue[pool->front];
        pool->front = (pool->front + 1) % QUEUE_SIZE;
        pool->count--;
        pthread_cond_signal(&pool->not_full);
        pthread_mutex_unlock(&pool->mutex);
        t.function(t.arg);
    }
    return NULL;
}

// 初始化线程池
void init_thread_pool(thread_pool* pool) {
    pool->front = 0;
    pool->rear = 0;
    pool->count = 0;
    pool->shutdown = 0;
    pthread_mutex_init(&pool->mutex, NULL);
    pthread_cond_init(&pool->not_empty, NULL);
    pthread_cond_init(&pool->not_full, NULL);
}

// 添加任务到线程池
void add_task(thread_pool* pool, void (*function)(void*), void* arg) {
    pthread_mutex_lock(&pool->mutex);
    while (pool->count == QUEUE_SIZE &&!pool->shutdown) {
        pthread_cond_wait(&pool->not_full, &pool->mutex);
    }
    if (pool->shutdown) {
        pthread_mutex_unlock(&pool->mutex);
        return;
    }
    pool->queue[pool->rear].function = function;
    pool->queue[pool->rear].arg = arg;
    pool->rear = (pool->rear + 1) % QUEUE_SIZE;
    pool->count++;
    pthread_cond_signal(&pool->not_empty);
    pthread_mutex_unlock(&pool->mutex);
}

// 销毁线程池
void destroy_thread_pool(thread_pool* pool) {
    pthread_mutex_lock(&pool->mutex);
    pool->shutdown = 1;
    pthread_cond_broadcast(&pool->not_empty);
    pthread_cond_broadcast(&pool->not_full);
    pthread_mutex_unlock(&pool->mutex);
    int i;
    for (i = 0; i < THREADS; i++) {
        pthread_join(pool->threads[i], NULL);
    }
    pthread_mutex_destroy(&pool->mutex);
    pthread_cond_destroy(&pool->not_empty);
    pthread_cond_destroy(&pool->not_full);
}

// 示例任务函数
void task_function(void* arg) {
    int num = *((int*)arg);
    printf("Task %d is running.\n", num);
    sleep(1);
}

int main() {
    thread_pool pool;
    init_thread_pool(&pool);
    pthread_t threads[THREADS];
    int i;
    for (i = 0; i < THREADS; i++) {
        pthread_create(&threads[i], NULL, worker, &pool);
    }
    int tasks[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    for (i = 0; i < 10; i++) {
        add_task(&pool, task_function, &tasks[i]);
    }
    sleep(3);
    destroy_thread_pool(&pool);
    return 0;
}

在这个线程池实现中,我们定义了 task 结构体来表示任务,thread_pool 结构体来管理线程池。线程池包含任务队列、互斥锁、条件变量等成员。worker 函数是线程执行的函数,它从任务队列中取出任务并执行。init_thread_pool 函数用于初始化线程池,add_task 函数用于向线程池添加任务,destroy_thread_pool 函数用于销毁线程池。

5. 多线程与信号处理

5.1 信号与多线程的关系

在多线程程序中,信号处理会带来一些特殊的问题。默认情况下,信号会发送到进程中的任意一个线程。这可能导致一些意外的行为,因为不同线程可能处于不同的执行状态。

为了更好地控制信号处理,我们可以使用 pthread_sigmask 函数来设置线程的信号掩码,从而决定哪些信号该线程可以接收。

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
pthread_sigmask(SIG_BLOCK, &set, NULL);

在这个例子中,我们将 SIGINT 信号(通常由用户按 Ctrl+C 产生)阻塞,使得当前线程不会接收到该信号。

5.2 线程特定的信号处理

我们可以设置某个线程专门处理特定的信号。首先,我们需要创建一个新线程,并在该线程中设置信号处理函数。

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

void signal_handler(int signum) {
    printf("Caught signal %d in signal handling thread.\n", signum);
}

void* signal_thread(void* arg) {
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    pthread_sigmask(SIG_UNBLOCK, &set, NULL);
    struct sigaction sa;
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);
    while (1) {
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t signal_thread_id;
    pthread_create(&signal_thread_id, NULL, signal_thread, NULL);
    while (1) {
        printf("Main thread is running.\n");
        sleep(1);
    }
    return 0;
}

在这个例子中,我们创建了一个专门处理 SIGINT 信号的线程。在该线程中,我们先解除对 SIGINT 信号的阻塞,然后设置信号处理函数 signal_handler。主线程则继续执行自己的任务。

6. 多线程调试技巧

6.1 使用 GDB 调试多线程程序

GDB 是一个强大的调试工具,它支持多线程调试。在调试多线程程序时,可以使用以下 GDB 命令:

  • info threads:显示当前所有线程的信息,包括线程 ID、线程状态等。
  • thread <thread_id>:切换到指定线程进行调试。
  • break <function_name>:在指定函数处设置断点,该断点会作用于所有线程。
  • break <function_name> thread <thread_id>:在指定线程的指定函数处设置断点。

例如,我们有一个简单的多线程程序:

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

void* thread_function(void* arg) {
    int i;
    for (i = 0; i < 5; i++) {
        printf("Thread is running: %d\n", i);
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_function, NULL);
    int i;
    for (i = 0; i < 5; i++) {
        printf("Main thread is running: %d\n", i);
        sleep(1);
    }
    pthread_join(thread, NULL);
    return 0;
}

使用 GDB 调试时,可以这样操作:

gdb./a.out
(gdb) break thread_function
(gdb) run
(gdb) info threads
(gdb) thread <thread_id>
(gdb) next

6.2 日志记录调试

在多线程程序中,日志记录是一种非常有效的调试方法。通过在关键代码位置添加日志输出,可以了解线程的执行流程和状态。

#include <stdio.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>

void log_message(const char* message) {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    struct tm* tm_info;
    tm_info = localtime(&tv.tv_sec);
    char time_str[26];
    strftime(time_str, 26, "%Y-%m-%d %H:%M:%S", tm_info);
    printf("%s.%06ld %s\n", time_str, tv.tv_usec, message);
}

void* thread_function(void* arg) {
    log_message("Thread started");
    int i;
    for (i = 0; i < 5; i++) {
        log_message("Thread is running");
        sleep(1);
    }
    log_message("Thread ended");
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_function, NULL);
    int i;
    for (i = 0; i < 5; i++) {
        log_message("Main thread is running");
        sleep(1);
    }
    pthread_join(thread, NULL);
    return 0;
}

在这个例子中,log_message 函数用于记录日志,包括时间戳和具体消息。通过查看日志,可以清楚地了解线程的运行情况。

通过上述对 Linux C 语言多线程创建的高级玩法的探讨,我们深入了解了线程属性设置、同步机制、线程池技术、信号处理以及调试技巧等方面的知识。这些技术和方法在实际的多线程编程中非常实用,能够帮助我们编写出高效、稳定的多线程程序。在实际应用中,需要根据具体的需求和场景,合理选择和运用这些技术。