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

Linux C语言多线程编程基础

2022-08-083.0k 阅读

一、线程基础概念

在Linux环境下使用C语言进行多线程编程,首先要理解线程是什么。线程,有时被称为轻量级进程(LWP, Light Weight Process),是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。与进程不同,线程之间共享进程的地址空间,包括代码段、数据段和堆空间,但每个线程都有自己独立的栈空间。

1.1 线程与进程的区别

进程是资源分配的最小单位,而线程是程序执行的最小单位。进程有自己独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响;而线程只是一个进程中的不同执行路径,它们共享进程的资源。如果一个线程崩溃,很可能导致整个进程崩溃。

例如,一个浏览器进程可能包含多个线程,一个线程负责页面渲染,一个线程负责网络请求等。这些线程共享浏览器进程的内存空间等资源,提高了资源利用率和执行效率。

1.2 多线程编程的优势

  1. 提高程序响应性:例如在一个图形界面应用程序中,主线程负责处理用户界面交互,而可以开启一个子线程进行数据的后台加载。这样在数据加载过程中,用户仍然可以操作界面,不会出现界面假死的情况。
  2. 充分利用多核处理器:现代计算机大多是多核处理器,多线程程序可以将不同的任务分配到不同的核心上并行执行,从而提高程序的整体执行效率。
  3. 简化程序结构:对于一些复杂的任务,可以将其分解为多个相对简单的子任务,每个子任务由一个线程负责执行,使程序结构更加清晰。

二、Linux下C语言线程库

在Linux系统中,使用POSIX线程库(通常称为pthread库)来进行多线程编程。这个库提供了一系列的函数来创建、管理和同步线程。要使用pthread库,需要在编译时链接该库,通常使用-lpthread选项。

2.1 线程创建

使用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类型变量的指针,该变量用于存储新创建线程的ID。
  • attr:用于设置线程的属性,通常可以设置为NULL,表示使用默认属性。
  • start_routine:是一个函数指针,指向线程要执行的函数。
  • arg:是传递给start_routine函数的参数。

下面是一个简单的示例代码,创建一个新线程并输出一条消息:

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

// 线程执行函数
void *print_message(void *ptr) {
    char *message;
    message = (char *) ptr;
    printf("%s\n", message);
    return NULL;
}

int main() {
    pthread_t thread;
    char *message = "Hello, from thread!";
    int ret;

    // 创建线程
    ret = pthread_create(&thread, NULL, print_message, (void *) message);
    if (ret != 0) {
        printf("Error creating thread: %d\n", ret);
        return 1;
    }

    printf("Thread created successfully\n");

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

    return 0;
}

在上述代码中,print_message函数是新线程要执行的函数,通过pthread_create创建线程,并将message字符串作为参数传递给该函数。pthread_join函数用于等待线程结束,防止主线程提前退出。

2.2 线程ID与线程终止

每个线程都有一个唯一的ID,类型为pthread_t。可以使用pthread_self函数获取当前线程的ID,函数原型为:

pthread_t pthread_self(void);

线程的终止有几种方式:

  1. 线程函数返回:如上述示例中,print_message函数执行完毕后返回,线程也就自然终止。
  2. 调用pthread_exit函数:在线程内部可以调用pthread_exit函数来主动终止线程,其函数原型为:
void pthread_exit(void *retval);

其中retval是线程的返回值,可以被pthread_join函数获取。 3. 其他线程调用pthread_cancel函数取消该线程:函数原型为:

int pthread_cancel(pthread_t thread);

被取消的线程可以通过设置取消点来决定何时响应取消请求。

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

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

void *thread_function(void *arg) {
    printf("Thread is running\n");
    pthread_exit((void *) 1);
}

int main() {
    pthread_t thread;
    int ret;
    void *thread_result;

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

    printf("Thread created successfully\n");

    // 等待线程结束并获取返回值
    ret = pthread_join(thread, &thread_result);
    if (ret != 0) {
        printf("Error joining thread: %d\n", ret);
        return 1;
    }

    printf("Thread returned: %ld\n", (long) thread_result);

    return 0;
}

在这个示例中,thread_function函数调用pthread_exit并传递返回值1,主线程通过pthread_join获取该返回值并输出。

三、线程同步

由于多个线程共享进程的资源,可能会出现竞态条件(Race Condition)。例如,多个线程同时访问和修改同一个共享变量,导致结果不可预测。为了避免这种情况,需要使用线程同步机制。

3.1 互斥锁(Mutex)

互斥锁是一种简单的同步工具,用于保证在同一时间只有一个线程可以访问共享资源。在pthread库中,使用pthread_mutex_t类型来表示互斥锁。

  1. 初始化互斥锁
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

通常attr参数设置为NULL,表示使用默认属性。也可以使用静态初始化方式:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  1. 加锁与解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

当一个线程调用pthread_mutex_lock时,如果互斥锁处于未锁定状态,该线程将锁定互斥锁并继续执行;如果互斥锁已被其他线程锁定,该线程将被阻塞,直到互斥锁被解锁。

下面是一个使用互斥锁保护共享变量的示例:

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

// 共享变量
int counter = 0;
// 互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

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

int main() {
    pthread_t thread1, thread2;
    int ret;

    // 创建两个线程
    ret = pthread_create(&thread1, NULL, increment_counter, NULL);
    if (ret != 0) {
        printf("Error creating thread1: %d\n", ret);
        return 1;
    }

    ret = pthread_create(&thread2, NULL, increment_counter, NULL);
    if (ret != 0) {
        printf("Error creating thread2: %d\n", ret);
        return 1;
    }

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

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

    printf("Final counter value: %d\n", counter);

    return 0;
}

在这个示例中,两个线程同时对counter变量进行递增操作。通过互斥锁,保证了在同一时间只有一个线程可以修改counter,从而避免了竞态条件。

3.2 条件变量(Condition Variable)

条件变量用于线程间的同步,它允许线程在某个条件满足时被唤醒。条件变量通常与互斥锁一起使用。在pthread库中,使用pthread_cond_t类型来表示条件变量。

  1. 初始化条件变量
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

同样,attr参数通常设置为NULL。也有静态初始化方式:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  1. 等待条件变量与唤醒
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_wait函数会自动解锁互斥锁并使当前线程阻塞,直到条件变量被pthread_cond_signalpthread_cond_broadcast唤醒。唤醒后,线程会重新获取互斥锁。pthread_cond_signal唤醒一个等待在条件变量上的线程,而pthread_cond_broadcast唤醒所有等待在条件变量上的线程。

下面是一个生产者 - 消费者模型的示例,使用条件变量和互斥锁实现:

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

#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0;
int out = 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 item = 1;
    while (1) {
        pthread_mutex_lock(&mutex);
        while ((in + 1) % BUFFER_SIZE == out) {
            // 缓冲区满,等待
            pthread_cond_wait(&not_full, &mutex);
        }
        buffer[in] = item;
        printf("Produced: %d\n", item);
        in = (in + 1) % BUFFER_SIZE;
        item++;
        // 通知缓冲区有数据
        pthread_cond_signal(&not_empty);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        while (in == out) {
            // 缓冲区空,等待
            pthread_cond_wait(&not_empty, &mutex);
        }
        int item = buffer[out];
        printf("Consumed: %d\n", item);
        out = (out + 1) % BUFFER_SIZE;
        // 通知缓冲区有空间
        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t producer_thread, consumer_thread;
    int ret;

    ret = pthread_create(&producer_thread, NULL, producer, NULL);
    if (ret != 0) {
        printf("Error creating producer thread: %d\n", ret);
        return 1;
    }

    ret = pthread_create(&consumer_thread, NULL, consumer, NULL);
    if (ret != 0) {
        printf("Error creating consumer thread: %d\n", ret);
        return 1;
    }

    // 等待线程结束(实际这里可以使用其他方式停止线程,例如信号)
    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_fullnot_empty以及互斥锁,实现了线程间的同步,确保缓冲区既不会溢出也不会下溢。

3.3 读写锁(Read - Write Lock)

读写锁允许同一时间有多个线程进行读操作,但只允许一个线程进行写操作。在pthread库中,使用pthread_rwlock_t类型来表示读写锁。

  1. 初始化读写锁
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

attr参数通常为NULL。也有静态初始化方式:

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
  1. 加锁与解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

pthread_rwlock_rdlock用于读锁定,pthread_rwlock_wrlock用于写锁定。

下面是一个简单的示例,展示读写锁的使用:

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

// 共享数据
int shared_data = 0;
// 读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

void *reader(void *arg) {
    pthread_rwlock_rdlock(&rwlock);
    printf("Reader %ld reads: %d\n", (long) pthread_self(), shared_data);
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

void *writer(void *arg) {
    pthread_rwlock_wrlock(&rwlock);
    shared_data++;
    printf("Writer %ld writes: %d\n", (long) pthread_self(), shared_data);
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

int main() {
    pthread_t reader1, reader2, writer1;
    int ret;

    ret = pthread_create(&reader1, NULL, reader, NULL);
    if (ret != 0) {
        printf("Error creating reader1: %d\n", ret);
        return 1;
    }

    ret = pthread_create(&reader2, NULL, reader, NULL);
    if (ret != 0) {
        printf("Error creating reader2: %d\n", ret);
        return 1;
    }

    ret = pthread_create(&writer1, NULL, writer, NULL);
    if (ret != 0) {
        printf("Error creating writer1: %d\n", ret);
        return 1;
    }

    // 等待线程结束
    pthread_join(reader1, NULL);
    pthread_join(reader2, NULL);
    pthread_join(writer1, NULL);

    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

在这个示例中,多个读线程可以同时读取shared_data,而写线程在写入时会独占shared_data,保证数据的一致性。

四、线程属性

线程属性可以在创建线程时进行设置,通过pthread_attr_t类型来表示。可以设置的属性包括线程的栈大小、调度策略等。

4.1 栈大小设置

可以使用pthread_attr_setstacksize函数来设置线程的栈大小,函数原型为:

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

例如,下面的代码设置线程栈大小为8192字节:

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

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

int main() {
    pthread_t thread;
    pthread_attr_t attr;
    int ret;

    // 初始化线程属性
    pthread_attr_init(&attr);
    // 设置栈大小
    ret = pthread_attr_setstacksize(&attr, 8192);
    if (ret != 0) {
        printf("Error setting stack size: %d\n", ret);
        return 1;
    }

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

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

    // 销毁线程属性
    pthread_attr_destroy(&attr);

    return 0;
}

4.2 调度策略设置

线程的调度策略决定了线程在系统中的执行优先级。在pthread库中,可以使用pthread_attr_setschedpolicy函数来设置调度策略,函数原型为:

int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);

其中policy可以取值为SCHED_OTHER(普通调度策略)、SCHED_FIFO(先进先出调度策略)或SCHED_RR(时间片轮转调度策略)。

下面是一个设置调度策略为SCHED_RR的示例:

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

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

int main() {
    pthread_t thread;
    pthread_attr_t attr;
    struct sched_param param;
    int ret;

    // 初始化线程属性
    pthread_attr_init(&attr);
    // 设置调度策略为SCHED_RR
    ret = pthread_attr_setschedpolicy(&attr, SCHED_RR);
    if (ret != 0) {
        printf("Error setting scheduling policy: %d\n", ret);
        return 1;
    }

    // 设置调度参数(优先级)
    param.sched_priority = sched_get_priority_max(SCHED_RR);
    ret = pthread_attr_setschedparam(&attr, &param);
    if (ret != 0) {
        printf("Error setting scheduling parameters: %d\n", ret);
        return 1;
    }

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

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

    // 销毁线程属性
    pthread_attr_destroy(&attr);

    return 0;
}

在这个示例中,首先设置线程的调度策略为SCHED_RR,然后设置调度参数中的优先级为SCHED_RR策略下的最大优先级。

五、多线程编程的注意事项

  1. 死锁问题:死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放资源时,就会发生死锁。例如,线程A持有锁1并等待锁2,而线程B持有锁2并等待锁1,这样就形成了死锁。为了避免死锁,需要遵循一些原则,如按照相同的顺序获取锁,避免嵌套锁等。
  2. 资源泄漏:在线程中动态分配的资源,如果没有正确释放,可能会导致资源泄漏。例如,在线程中使用malloc分配了内存,但线程提前终止而没有调用free释放内存。要确保所有动态分配的资源在适当的时候被释放。
  3. 线程安全函数:并非所有的C标准库函数都是线程安全的。例如,asctime函数就不是线程安全的,因为它使用了静态缓冲区。在多线程程序中,应尽量使用线程安全的函数,或者自行实现同步机制来保证对非线程安全函数的正确调用。
  4. 调试困难:多线程程序的调试比单线程程序更加困难,因为线程的执行顺序是不确定的,可能会导致一些难以重现的问题。可以使用调试工具如gdb的多线程调试功能,或者添加日志输出来辅助调试。

通过深入理解Linux下C语言多线程编程的基础知识、同步机制、线程属性以及注意事项,开发者可以编写出高效、稳定的多线程程序,充分利用多核处理器的优势,提升程序的性能和响应性。在实际应用中,需要根据具体的需求和场景,合理选择线程同步方式和设置线程属性,以实现最优的多线程解决方案。