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

Linux C语言线程创建与终止

2022-02-255.6k 阅读

线程概述

在深入探讨 Linux C 语言中线程的创建与终止之前,我们先来理解一下线程的基本概念。线程,也被称为轻量级进程(LWP, Light Weight Process),是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

一个进程可以包含多个线程,这些线程共享进程的大部分资源,如地址空间、打开的文件描述符等,但每个线程拥有自己独立的栈空间、寄存器状态和程序计数器。这种资源共享与独立的特性使得线程在并发编程中具有高效性和灵活性。相比于进程,线程的创建和销毁开销更小,线程间的通信也更为便捷,因为它们共享同一地址空间。

在 Linux 系统中,线程的实现依赖于内核提供的系统调用和相关的线程库。最常用的线程库是 POSIX 线程库(Pthreads),它为 C 语言程序员提供了一套标准的 API 来创建、管理和控制线程。

线程创建

pthread_create 函数

在 Linux C 语言中,创建线程使用 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:这是一个函数指针,指向新线程开始执行的函数。该函数的返回值类型必须是 void *,并且接受一个 void * 类型的参数。
  • arg:传递给 start_routine 函数的参数。如果不需要传递参数,可以将其设置为 NULL

代码示例

下面是一个简单的示例代码,展示如何使用 pthread_create 创建一个线程:

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

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

int main() {
    pthread_t thread;
    char *message = "Hello, from the 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);

    printf("Thread has finished execution\n");
    return 0;
}

在上述代码中:

  1. 定义了 print_message 函数,它将在新线程中执行。该函数接受一个 void * 类型的参数,并将其转换为 char * 类型后打印出来。
  2. main 函数中,声明了一个 pthread_t 类型的变量 thread 用于存储线程标识符。
  3. 使用 pthread_create 创建线程,传递 NULL 作为线程属性,print_message 作为线程执行函数,以及 message 作为传递给线程函数的参数。
  4. 检查 pthread_create 的返回值,如果返回值不为 0,表示线程创建失败,打印错误信息并退出程序。
  5. 使用 pthread_join 等待新创建的线程结束。pthread_join 函数的作用是阻塞当前线程(这里是主线程),直到指定的线程(thread)结束执行。

线程属性

线程属性可以通过 pthread_attr_t 结构体来设置。在使用之前,需要先对属性对象进行初始化,使用 pthread_attr_init 函数:

int pthread_attr_init(pthread_attr_t *attr);

初始化完成后,可以设置各种属性,例如栈大小:

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

设置完属性后,将 attr 指针传递给 pthread_create 函数的第二个参数。当不再需要使用属性对象时,需要使用 pthread_attr_destroy 函数进行清理:

int pthread_attr_destroy(pthread_attr_t *attr);

以下是一个设置线程栈大小属性的示例:

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

void *thread_function(void *arg) {
    return NULL;
}

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

    // 初始化线程属性
    ret = pthread_attr_init(&attr);
    if (ret != 0) {
        printf("Error initializing thread attributes: %d\n", ret);
        return 1;
    }

    // 设置栈大小为 8192 字节
    ret = pthread_attr_setstacksize(&attr, 8192);
    if (ret != 0) {
        printf("Error setting stack size: %d\n", ret);
        pthread_attr_destroy(&attr);
        return 1;
    }

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

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

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

    return 0;
}

在这个示例中,首先初始化线程属性对象 attr,然后设置其栈大小为 8192 字节。接着使用设置好属性的 attr 创建线程,创建完成后等待线程结束,并最终销毁线程属性对象。

线程终止

线程的自然终止

线程的自然终止是指线程执行完其线程函数后正常结束。例如,在前面的 print_message 函数中,当函数执行到 return NULL 语句时,线程就自然终止了。

void *print_message(void *arg) {
    char *message = (char *)arg;
    printf("%s\n", message);
    return NULL; // 线程自然终止
}

当线程自然终止时,它所占用的系统资源(如栈空间)会被系统自动回收。然而,如果线程是通过共享资源(如共享内存、文件描述符等)与其他线程进行通信的,那么在终止前需要确保对这些共享资源进行了正确的清理和同步操作,以避免资源泄漏或数据不一致的问题。

pthread_exit 函数

线程也可以通过调用 pthread_exit 函数来主动终止自己的执行。pthread_exit 函数的原型如下:

#include <pthread.h>
void pthread_exit(void *retval);
  • retval:这是一个 void * 类型的指针,用于传递线程的返回值。其他线程可以通过 pthread_join 函数获取这个返回值。

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

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

void *thread_function(void *arg) {
    int result = 42;
    printf("Thread is about to exit\n");
    pthread_exit((void *)&result);
}

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

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

    ret = pthread_join(thread, &thread_result);
    if (ret != 0) {
        printf("Error joining thread: %d\n", ret);
        return 1;
    }

    int *result = (int *)thread_result;
    printf("Thread returned: %d\n", *result);

    return 0;
}

在这个示例中,thread_function 函数中调用 pthread_exit 主动终止线程,并传递一个指向整数 result 的指针作为返回值。在 main 函数中,通过 pthread_join 获取线程的返回值,并将其转换为 int * 类型后打印出来。

pthread_cancel 函数

除了线程自然终止和主动调用 pthread_exit 终止外,还可以从其他线程中调用 pthread_cancel 函数来终止指定的线程。pthread_cancel 函数的原型如下:

#include <pthread.h>
int pthread_cancel(pthread_t thread);
  • thread:要终止的线程的标识符。

当调用 pthread_cancel 时,系统会向指定的线程发送一个取消请求。然而,目标线程并不一定会立即响应这个请求,它需要在合适的取消点(Cancelation Point)处进行处理。取消点是线程执行过程中预先定义的一些位置,在这些位置上线程会检查是否有取消请求,并根据情况决定是否终止。

一些标准的库函数(如 pthread_testcancelsleepreadwrite 等)内部都包含了取消点。如果线程在执行过程中没有经过这些取消点,那么即使调用了 pthread_cancel,线程也不会立即终止。

以下是一个使用 pthread_cancel 的示例:

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

void *thread_function(void *arg) {
    while (1) {
        printf("Thread is running...\n");
        sleep(1); // sleep 函数包含取消点
    }
    return NULL;
}

int main() {
    pthread_t thread;
    int ret;

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

    sleep(3); // 主线程等待 3 秒
    ret = pthread_cancel(thread);
    if (ret != 0) {
        printf("Error canceling 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 been canceled and joined\n");
    return 0;
}

在这个示例中,thread_function 函数在一个无限循环中打印信息,并通过 sleep 函数暂停 1 秒。主线程在创建线程后等待 3 秒,然后调用 pthread_cancel 取消线程。由于 sleep 函数是一个取消点,所以线程会在执行 sleep 时响应取消请求并终止。最后,主线程使用 pthread_join 等待线程结束。

取消类型与状态

线程对取消请求的响应方式可以通过设置取消类型和状态来控制。取消类型有两种:延迟取消(Deferred Cancelation)和异步取消(Asynchronous Cancelation)。

  • 延迟取消:这是默认的取消类型。在线程执行到取消点时,才会检查并响应取消请求。这种方式可以确保线程在合适的时机进行清理工作,避免资源泄漏。
  • 异步取消:当设置为异步取消时,线程会立即响应取消请求,而不需要等待到达取消点。这种方式适用于那些对取消响应要求非常及时的场景,但需要特别小心,因为线程可能在执行任何代码时被取消,可能导致资源清理不完整。

可以使用 pthread_setcanceltype 函数来设置取消类型:

#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
  • type:新的取消类型,可以是 PTHREAD_CANCEL_DEFERREDPTHREAD_CANCEL_ASYNCHRONOUS
  • oldtype:用于存储旧的取消类型的指针。如果不需要获取旧的取消类型,可以设置为 NULL

线程还可以通过设置取消状态来决定是否响应取消请求。使用 pthread_setcancelstate 函数:

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
  • state:新的取消状态,可以是 PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DISABLE
  • oldstate:用于存储旧的取消状态的指针。如果不需要获取旧的取消状态,可以设置为 NULL

以下是一个示例,展示如何设置取消类型和状态:

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

void *thread_function(void *arg) {
    int oldtype, oldstate;

    // 设置取消类型为异步取消
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldtype);
    // 设置取消状态为禁用
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);

    printf("Thread is running...\n");
    sleep(5); // 线程不会响应取消请求,因为取消状态被禁用

    // 重新启用取消状态
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate);
    printf("Thread is now cancelable\n");

    while (1) {
        printf("Thread is still running...\n");
        sleep(1); // 现在线程可以响应取消请求
    }
    return NULL;
}

int main() {
    pthread_t thread;
    int ret;

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

    sleep(3); // 主线程等待 3 秒
    ret = pthread_cancel(thread);
    if (ret != 0) {
        printf("Error canceling 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 been canceled and joined\n");
    return 0;
}

在这个示例中,thread_function 函数首先将取消类型设置为异步取消,并禁用取消状态。因此,在第一次 sleep 期间,即使主线程调用 pthread_cancel,线程也不会响应。然后,线程重新启用取消状态,此时线程在后续的 sleep 中会响应取消请求。

线程终止时的清理

pthread_cleanup_push 和 pthread_cleanup_pop

当线程终止时,无论是自然终止、通过 pthread_exit 还是被 pthread_cancel 取消,都可能需要执行一些清理操作,例如释放动态分配的内存、关闭文件描述符等。pthread_cleanup_pushpthread_cleanup_pop 函数提供了一种机制来注册清理函数,这些函数会在线程终止时自动被调用。

pthread_cleanup_push 函数的原型如下:

#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
  • routine:指向清理函数的指针。清理函数的原型必须是 void function(void *)
  • arg:传递给清理函数的参数。

pthread_cleanup_pop 函数的原型如下:

#include <pthread.h>
void pthread_cleanup_pop(int execute);
  • execute:如果为非零值,那么在 pthread_cleanup_pop 调用时会执行之前通过 pthread_cleanup_push 注册的清理函数;如果为 0,则不会执行清理函数,但会移除注册的清理函数。

需要注意的是,pthread_cleanup_pushpthread_cleanup_pop 必须成对出现,并且它们在代码中必须在同一作用域内,否则会导致未定义行为。

以下是一个示例,展示如何使用 pthread_cleanup_pushpthread_cleanup_pop 进行内存清理:

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

void cleanup_handler(void *arg) {
    printf("Cleaning up: %s\n", (char *)arg);
    free(arg);
}

void *thread_function(void *arg) {
    char *message = strdup((char *)arg);
    if (message == NULL) {
        perror("strdup");
        pthread_exit(NULL);
    }

    pthread_cleanup_push(cleanup_handler, message);

    printf("Thread is running with message: %s\n", message);
    // 模拟一些工作
    sleep(2);

    pthread_cleanup_pop(1); // 执行清理函数
    return NULL;
}

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

    ret = pthread_create(&thread, NULL, thread_function, (void *)message);
    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 execution\n");
    return 0;
}

在这个示例中,thread_function 函数使用 strdup 函数分配了一块内存来存储传递进来的消息。然后通过 pthread_cleanup_push 注册了 cleanup_handler 清理函数,该函数会在 pthread_cleanup_pop 调用时释放之前分配的内存。在 pthread_cleanup_pop 中传递 1,以确保清理函数被执行。

清理函数的执行时机

清理函数会在以下几种情况下被执行:

  1. 线程通过 pthread_exit 终止:无论是主动调用 pthread_exit 还是线程函数自然返回,只要 pthread_cleanup_popexecute 参数为非零值,注册的清理函数就会被执行。
  2. 线程被 pthread_cancel 取消:同样,只要 pthread_cleanup_popexecute 参数为非零值,清理函数就会在取消处理时被执行。在这种情况下,清理函数可以用于释放资源、关闭文件描述符等,以确保线程在被取消时不会留下未处理的资源。
  3. 调用 pthread_cleanup_popexecute 参数为非零值:即使线程没有终止,手动调用 pthread_cleanup_pop 并传递非零的 execute 参数也会执行注册的清理函数。这在某些情况下,例如在线程执行过程中提前进行资源清理时非常有用。

线程创建与终止的错误处理

在使用 pthread_createpthread_exitpthread_cancel 等线程相关函数时,正确处理错误是非常重要的。这些函数通常会返回一个整数值来表示操作的结果。例如,pthread_create 函数在成功时返回 0,在失败时返回一个非零的错误码。

可以通过 strerror 函数将错误码转换为可读的错误信息。以下是一个在 pthread_create 中处理错误的示例:

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

void *thread_function(void *arg) {
    return NULL;
}

int main() {
    pthread_t thread;
    int ret;

    ret = pthread_create(&thread, NULL, thread_function, NULL);
    if (ret != 0) {
        char error_message[256];
        strcpy(error_message, strerror(ret));
        printf("Error creating thread: %s\n", error_message);
        return 1;
    }

    printf("Thread created successfully\n");
    // 等待线程结束
    pthread_join(thread, NULL);

    return 0;
}

在这个示例中,如果 pthread_create 返回非零值,表示线程创建失败。通过 strerror 函数将错误码转换为字符串,并打印出错误信息。

对于 pthread_cancelpthread_join 等函数,同样需要检查它们的返回值并进行相应的错误处理。例如,pthread_cancel 在成功取消线程时返回 0,失败时返回非零错误码:

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

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

int main() {
    pthread_t thread;
    int ret;

    ret = pthread_create(&thread, NULL, thread_function, NULL);
    if (ret != 0) {
        char error_message[256];
        strcpy(error_message, strerror(ret));
        printf("Error creating thread: %s\n", error_message);
        return 1;
    }

    sleep(3);
    ret = pthread_cancel(thread);
    if (ret != 0) {
        char error_message[256];
        strcpy(error_message, strerror(ret));
        printf("Error canceling thread: %s\n", error_message);
        return 1;
    }

    ret = pthread_join(thread, NULL);
    if (ret != 0) {
        char error_message[256];
        strcpy(error_message, strerror(ret));
        printf("Error joining thread: %s\n", error_message);
        return 1;
    }

    printf("Thread has been canceled and joined\n");
    return 0;
}

在这个示例中,对 pthread_createpthread_cancelpthread_join 的返回值都进行了检查,并在出现错误时打印出相应的错误信息。这样可以帮助开发者快速定位和解决线程操作中出现的问题。

总结

在 Linux C 语言编程中,线程的创建与终止是并发编程的重要基础。通过 pthread_create 函数可以创建新的线程,并根据需要设置线程属性。线程可以自然终止、通过 pthread_exit 主动终止,或者被其他线程使用 pthread_cancel 取消。在线程终止时,合理使用清理函数(如通过 pthread_cleanup_pushpthread_cleanup_pop 注册)可以确保资源的正确释放。同时,对线程相关函数的错误处理也是编写健壮的多线程程序的关键。深入理解这些概念和技术,将有助于开发者编写出高效、稳定的多线程应用程序。

希望通过本文的详细介绍和示例代码,读者能够对 Linux C 语言中线程的创建与终止有更深入的理解和掌握,并在实际项目中灵活运用这些知识。