【Linux】多线程:线程控制

目录

一、创建线程:pthread_create 

二、线程终止:pthread_exit、return、pthread_cancel

三、线程等待:pthread_join

四、线程分离:pthread_detach

五、如何创建并使用多线程 

六、对线程进行封装


一、创建线程:pthread_create 

pthread_create 是 POSIX 线程(pthread)库中的一个函数,用于创建一个新线程。它的函数原型如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

各参数的详细说明如下:

  1. pthread_t *thread

    这是一个指向 pthread_t 类型的指针,用于保存新创建线程的线程标识符。pthread_t 是一个线程句柄,用于在之后的线程操作中引用该线程。
  2. const pthread_attr_t *attr

    这是一个指向 pthread_attr_t 类型的指针,指定线程的属性。如果设置为 NULL,则线程将使用默认属性。pthread_attr_t 用于定义线程的各种属性,例如栈大小、调度策略等。
  3. void *(*start_routine)(void*)

    这是一个指向函数的指针,函数的返回类型为 void*,接收一个 void* 类型的参数。这个函数将在新线程中执行。函数的签名应为:
    void* my_thread_function(void* arg);
    
  4. void *arg

    这是传递给 start_routine 函数的参数。由于是 void* 类型,调用者可以传递任何类型的数据,但需要在 start_routine 函数内进行类型转换。

返回值

  • pthread_create 成功时返回 0
  • 失败时返回一个错误代码,具体错误代码可以通过 man pthread_create 查找相关文档。常见的错误包括 EAGAIN(系统资源不足),EINVAL(无效的参数),EPERM(没有足够的权限)等。

示例代码

以下是一个简单的使用 pthread_create 的示例:

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

// 线程执行的函数
void* my_thread_function(void* arg) {
    int* p = (int*)arg;
    printf("Thread received value: %d\n", *p);
    return NULL;
}

int main() {
    pthread_t thread;
    int value = 42;

    // 创建线程
    int ret = pthread_create(&thread, NULL, my_thread_function, &value);
    if (ret != 0) {
        fprintf(stderr, "Error creating thread: %d\n", ret);
        return EXIT_FAILURE;
    }

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

    return EXIT_SUCCESS;
}

二、线程终止:pthread_exit、return、pthread_cancel

如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1、 从线程函数 return。 但这种方法对主线程不适用,从 main 函数 return 相当于调用exit。

2、线程可以调用 pthread_ exit 终止自己。

3、 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程。

【注意:线程不可使用exit函数,因为该函数是用来终止进程的。当进程中的某一个线程使用后,该进程也会终止!】

pthread_exit 函数用于终止当前线程的执行,并且可以选择性地返回一个值给调用 pthread_join 的线程。它的函数原型如下:

void pthread_exit(void *retval);

参数详解

  • void *retval
    • 这是一个指向 void 的指针,线程终止时返回的值。如果你希望主线程或其他线程通过 pthread_join 获取这个返回值,可以传递一个合适的指针。如果不需要返回值,可以传递 NULL

主要功能

  1. 终止线程

    调用 pthread_exit 会使当前线程立即终止。线程的资源(例如栈)在终止时会被释放,但线程的线程句柄和某些状态信息可能会被保留,直到其他线程调用 pthread_join 来回收这些资源。
  2. 返回值

    线程可以通过 pthread_exit 返回一个指针,这个指针将被传递给调用 pthread_join 的线程。这在需要线程间传递结果或状态时非常有用。
  3. 线程资源释放

    调用 pthread_exit 后,线程的资源会被保留,直到其他线程调用 pthread_join。这样可以避免由于线程终止而导致的资源泄漏问题。

示例代码

以下是一个简单的示例,展示了如何使用 pthread_exit 来终止线程并返回一个值:

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

void* my_thread_function(void* arg) {
    int* result = malloc(sizeof(int));
    *result = 42; // 线程计算的结果

    // 线程结束,返回计算结果
    pthread_exit(result);
}

int main() {
    pthread_t thread;
    int* result;

    // 创建线程
    int ret = pthread_create(&thread, NULL, my_thread_function, NULL);
    if (ret != 0) {
        fprintf(stderr, "Error creating thread: %d\n", ret);
        return EXIT_FAILURE;
    }

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

    // 使用线程返回的结果
    printf("Thread returned value: %d\n", *result);
    free(result); // 释放动态分配的内存

    return EXIT_SUCCESS;
}

注意事项

  1. 资源管理

    在调用 pthread_exit 之前,确保线程的所有资源已经处理妥当。未处理的资源可能会导致内存泄漏或其他问题。
  2. 与 return 的区别

    在一个线程中使用 return 语句终止线程会隐式地调用 pthread_exit,并且线程返回值会作为 pthread_exit 的参数传递给 pthread_join。因此,return 和 pthread_exit 在功能上是等效的,但 pthread_exit 更明确地表示线程的结束和返回。

 【注意,pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用 malloc 分配的在堆上的空间,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。】

 【了解】pthread_cancel 函数用于请求取消一个正在运行的线程。它的函数原型如下:

int pthread_cancel(pthread_t thread);

参数详解

  • pthread_t thread
    • 这是你希望取消的线程的标识符,通常是通过 pthread_create 创建的线程句柄。

返回值

  • pthread_cancel 成功时返回 0
  • 失败时返回一个错误代码,常见的错误代码包括:
    • ESRCH:指定的线程无效或已终止。
    • EINVAL:线程不是当前进程的线程,或者线程已经是分离状态(即已取消)。

示例代码

以下是一个示例,演示了如何请求取消一个线程,并在线程中设置取消点:

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

// 线程执行的函数
void* my_thread_function(void* arg) {
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); // 启用取消
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); // 取消请求在取消点处理

    printf("Thread is running...\n");

    // 模拟长时间运行的操作
    for (int i = 0; i < 10; ++i) {
        sleep(1); // 每秒钟检查一次取消请求
        printf("Working %d...\n", i);
    }

    printf("Thread exiting normally.\n");
    return NULL;
}

int main() {
    pthread_t thread;

    // 创建线程
    int ret = pthread_create(&thread, NULL, my_thread_function, NULL);
    if (ret != 0) {
        fprintf(stderr, "Error creating thread: %d\n", ret);
        return EXIT_FAILURE;
    }

    // 等待几秒钟,然后请求取消线程
    sleep(3);
    pthread_cancel(thread);

    // 等待线程结束
    pthread_join(thread, NULL);
    printf("Thread has been joined.\n");

    return EXIT_SUCCESS;
}

【注意:当线程被取消后,该线程的退出结果会被设置为-1。】

三、线程等待:pthread_join

pthread_join 函数,用于等待一个线程结束,是阻塞等待的,并获取该线程的退出状态。它的函数原型如下:

int pthread_join(pthread_t thread, void **retval);

参数详解

  1. pthread_t thread

    这是你希望等待结束的线程的标识符,通常是通过 pthread_create 函数返回的线程句柄。
  2. void **retval

    这是一个指向 void* 类型的指针的指针,用于接收线程退出时返回的值。如果不需要获取线程的返回值,可以将其设置为 NULL。如果希望获取线程的返回值,传递一个指向 void* 的指针,pthread_join 会将线程的返回值存储在这个位置。

返回值

  • pthread_join 成功时返回 0
  • 失败时返回一个错误代码,常见的错误代码包括:
    • ESRCH:指定的线程无效或已终止。
    • EINVAL:线程不属于当前进程,或线程不可连接(例如线程已经是分离状态)。
    • EDEADLK:发生了死锁,通常是由于线程等待自己或者其他会导致死锁的条件。

使用示例

以下是一个简单的示例,展示了如何使用 pthread_join 等待线程结束并获取其返回值:

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

// 线程执行的函数
void* my_thread_function(void* arg) {
    int* p = (int*)arg;
    printf("Thread received value: %d\n", *p);
    int* result = malloc(sizeof(int));
    *result = *p * 2; // 计算结果并返回
    pthread_exit(result); // 线程结束并返回结果
}

int main() {
    pthread_t thread;
    int value = 42;
    int* result;

    // 创建线程
    int ret = pthread_create(&thread, NULL, my_thread_function, &value);
    if (ret != 0) {
        fprintf(stderr, "Error creating thread: %d\n", ret);
        return EXIT_FAILURE;
    }

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

    // 使用线程返回的结果
    printf("Thread returned value: %d\n", *result);
    free(result); // 释放分配的内存

    return EXIT_SUCCESS;
}

注意事项

  1. 线程分离(Detached)

    如果线程处于分离状态(通过 pthread_detach 设置),则不能使用 pthread_join。否则,pthread_join 会返回 EINVAL 错误代码。
  2. 避免死锁

    不要在一个线程中等待另一个线程,而这个线程又在等待第一个线程。这样的操作会导致死锁。
  3. 清理资源

    如果线程返回了动态分配的内存(如上例所示),记得在主线程中使用 free 释放这些资源,避免内存泄漏。

为什么要进行线程等待?

        当线程退出时,在Linux内核中,该轻量级进程会销毁自身的资源。但在pthread库中,仍会保留线程控制块pthread_attr_t 结构体,该结构体中会留存线程的一系列属性和信息,其中就包括该线程执行方法结束时的返回值。而该线程控制块必须在被进行线程等待后才可以被回收所占用的资源,并且由pthread_join函数的输出型参数带出线程退出的返回值。

所以使用pthread_join函数进行线程等待是必要的,一是能够防止系统资源的浪费,对其及时进行清理,防止“僵尸线程”的产生;二是可以获得线程的退出状态或返回信息,获取线程的执行结果。  

四、线程分离:pthread_detach

pthread_detach 函数用于将线程设置为“分离状态”。在分离状态下,线程的资源会在其终止时自动释放,而无需其他线程调用 pthread_join 来显式回收这些资源。它的函数原型如下:

int pthread_detach(pthread_t thread);

参数详解

  • pthread_t thread
    • 这是你希望设置为分离状态的线程的标识符,通常是通过 pthread_create 创建的线程句柄。

返回值

  • 成功:返回 0
  • 失败:返回一个错误代码,常见的错误代码包括:
    • ESRCH:指定的线程无效或已终止。
    • EINVAL:指定的线程不是当前进程的线程,或者线程已经是分离状态。

分离状态的特点

  1. 自动资源释放

    一旦线程被设置为分离状态,它的资源(如线程句柄和线程栈)会在线程终止时自动释放。这样可以避免因未调用 pthread_join 而导致的资源泄漏。
  2. 不可再连接

    分离状态的线程不能再通过 pthread_join 被连接或回收。因此,如果你设置了线程为分离状态,必须确保线程结束时的资源不再需要显式管理。
  3. 使用场景

    当你创建的线程不需要由其他线程获取其退出状态或结果时,通常可以将其设置为分离状态。例如,后台线程或守护线程常常被设置为分离状态。

示例代码

以下是一个示例,展示了如何使用 pthread_detach 来将线程设置为分离状态:

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

void* my_thread_function(void* arg) {
    printf("Thread is running...\n");
    sleep(2); // 模拟线程工作
    printf("Thread is done.\n");
    return NULL;
}

int main() {
    pthread_t thread;

    // 创建线程
    int ret = pthread_create(&thread, NULL, my_thread_function, NULL);
    if (ret != 0) {
        fprintf(stderr, "Error creating thread: %d\n", ret);
        return EXIT_FAILURE;
    }

    // 将线程设置为分离状态
    ret = pthread_detach(thread);
    if (ret != 0) {
        fprintf(stderr, "Error detaching thread: %d\n", ret);
        return EXIT_FAILURE;
    }

    // 主线程继续执行其他操作
    printf("Main thread continues...\n");
    sleep(3); // 等待子线程完成

    printf("Main thread done.\n");
    return EXIT_SUCCESS;
}

注意事项

  1. 避免资源泄漏

    在将线程设置为分离状态之前,确保你不再需要获取线程的返回值或状态。如果需要获取线程的返回状态,应该使用 pthread_join,而不是 pthread_detach
  2. 线程状态

    如果一个线程已经结束并被设置为分离状态,尝试再次设置它为分离状态或尝试 pthread_join 将会失败。

当然,线程分离除了由主线程对其他线程进行分离外,还可以由线程本身进行分离,这时需要用到pthread_self函数获取该线程自身的线程ID。

pthread_self 函数用于获取调用线程的线程标识符(thread identifier)。这个函数在多线程编程中非常有用,因为它允许线程了解自己的身份,这在进行线程特定的操作或资源管理时可能是必要的。

函数原型

pthread_t pthread_self(void);

返回值

pthread_self 函数返回一个 pthread_t 类型的值,这个值代表调用该函数的线程的标识符。pthread_t 是一个线程库中定义的用于表示线程句柄的数据类型。

示例代码改写:

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

void* my_thread_function(void* arg) {
    // 将线程设置为分离状态
    ret = pthread_detach(pthread_self());
    if (ret != 0) {
        fprintf(stderr, "Error detaching thread: %d\n", ret);
        return (void*)-1;
    }
    printf("Thread is running...\n");
    sleep(2); // 模拟线程工作
    printf("Thread is done.\n");
    return NULL;
}

int main() {
    pthread_t thread;

    // 创建线程
    int ret = pthread_create(&thread, NULL, my_thread_function, NULL);
    if (ret != 0) {
        fprintf(stderr, "Error creating thread: %d\n", ret);
        return EXIT_FAILURE;
    }

    // 主线程继续执行其他操作
    printf("Main thread continues...\n");
    sleep(3); // 等待子线程完成

    printf("Main thread done.\n");
    return EXIT_SUCCESS;
}

五、如何创建并使用多线程 

在程序中,我们可以采取多种方式管理线程。在本节代码中,我们使用循环的方式创建多线程。那我们如何为每个线程分配任务呢?在学习完pthread_create函数之后,我们了解到该函数有一个形参为函数指针,也就是我们常说的“回调函数”,当线程创建完毕之后,该线程会立刻去执行该函数。

而在实际应用中,对于多线程的控制通常还会涉及到线程的互斥与同步,所以在本节代码当中我们仅展示如何简单地创建并使用多线程来执行各自的任务。

在多线程编程中,我们通常将主线程作为线程的控制总线,对创建的多线程进行统一管理。而主线程的任务就是:1、创建额外线程;2、保留每个线程的TID(线程ID);3、进行线程等待,获取线程的返回状态;4、主线程退出。【当然,在主线程中可以对其他线程进行取消操作;而在各自线程的内部也可以创建额外线程,但是这会使得多线程难于管理。】

在分支线程中,执行流程主要是:1、将需要执行的任务的函数指针通过参数传递给pthread_create函数,当线程创建完毕后,该线程就会跳转到该函数的起始地址处执行该函数的代码。2、进行线程退出,一般是通过return正常返回或者调用pthread_exit函数。

本次代码将会使用vector数组对每个线程的TID进行存储。

【需要注意的是,线程的创建数量并不是越多越好,而是要适量。在进行多线程编程时,要考虑线程的管理和CPU调度。一般情况下,线程数接近CPU核心数是理想的线程数量。在线程的互斥章节中我们会加深讲解。】

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

#define BUFF_SIZE 128

// 线程函数
void *thread_function(void *arg)
{
    char *name = (char *)arg;
    int cnt = 2;
    //执行2次
    while (cnt--)
    {
        printf("%s is running!!!\n", name);
        sleep(1);
    }
    // 在线程退出之前释放为每个线程所申请的资源
    delete[] name;
    // pthread_exit((void*)0); // 结束线程
    return (void *)0;
}

int main()
{
    // 存储每个线程的tid
    std::vector<pthread_t> tids;

    // 假设创建5个线程,此时加上主线程,该进程中共有6个线程
    for (int i = 0; i < 5; i++)
    {
        pthread_t tid = 0;
        // 注意,此处为什么不直接char buff[BUFF_SIZE] 呢?即为什么不直接在栈上创建空间呢?
        // 因为此处需要传递给pthread_create函数的参数是该处空间的地址。
        // 而在栈上创建的空间只存在于本次循环中,循环结束后该片空间会被销毁。在下次循环时,又会在相同的地址位置创建数组空间。
        // 进而多线程将会使用同一片内存区域,在之后创建的线程会影响之前的线程资源。这是不允许的。
        // 所以我们选择在堆上创建数组空间,使得每个线程的资源都是独立的!!!
        char *buff = new char[BUFF_SIZE];
        snprintf(buff, BUFF_SIZE, "i am thread-%d!", i + 1);
        int ret = pthread_create(&tid, NULL /*使用默认线程属性*/, thread_function, (void *)buff);
        if (ret != 0)
        {
            perror("pthread_create false!!!");
            exit(-1);
        }
        tids.emplace_back(tid);
    }

    // 对主线程所创建的额外线程进行阻塞等待,并获取线程的退出信息
    for (int i = 0; i < 5; i++)
    {
        //注意:指针变量本身也是有空间的,在64位系统下是8个字节。即指针变量本身也可以存储变量,如整型变量等
        void *res = NULL;
        int ret = pthread_join(tids[i], (void **)&res);
        if (ret != 0)
        {
            perror("pthread_join false!!!");
            exit(-1);
        }
        printf("thread-%d jioned success!!!, exit status is %ld\n", i + 1, (long)(res));
    }

    //主线程退出,同时也表示该进程退出
    return 0;
}

以上程序简单地演示了多线程的执行过程。实际上,多线程如果不加以控制,会造成资源竞争、程序难以控制、结果不符合预期等情况。而在后续的章节中,我们会着重讲解线程的互斥与同步,以解决多线程编程中不可预料的问题。 

六、对线程进行封装

#include <pthread.h>
#include <string>
#include <functional>
using FuncType = std::function<void(const std::string&)>;//包装器
//线程类的封装
class Thread
{
private:
    pthread_t _tid;//线程ID
    std::string _thread_name;//线程名
    FuncType _func;//线程的执行函数
    bool _is_running;//线程的状态
    //...
private:
    void Excute()
    {
        _is_running = true;
        _func(_thread_name);
        _is_running = false;
    }
    //类中的函数参数包含this指针,使用static修饰
    static void* ThreadRoute(void* arg)
    {
        Thread* self = static_cast<Thread*>(arg);
        self->Excute();
        return (void*)0;
    }
public:
    Thread(std::string thread_name, FuncType func)
    :_thread_name(thread_name), _func(func)
    {
        _is_running = false;
    }

    //线程启动
    bool Start(){
        int ret = pthread_create(&_tid, NULL, ThreadRoute, (void*)this);
        if (ret != 0){
            return false;
        }
        std::cout << _thread_name << " has Started" << std::endl;
        return true;
    }
    //线程取消
    bool Stop()
    {
        if(_is_running){
            int ret = pthread_cancel(_tid);
            if (ret != 0){
                return false;
            }   
            std::cout << _thread_name << " has Stoped" << std::endl;
            _is_running = false;
        }
        return true;
    }
    //回收线程
    bool Join()
    {
        if(!_is_running){
            int ret = pthread_join(_tid, NULL);//不关心线程返回值,设置为NULL
            if (ret != 0){
                return false;
            }   
        }
        std::cout << _thread_name << " has Joined" << std::endl;
        return true;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白也有开发梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值