自学内容网 自学内容网

【Linux】线程控制

在Linux操作系统内部,不管是进程还是线程。它以PCB为准,只有在用户态里才有线程的概念。一般实现线程会用到一个POSIX线程库,在这里可以通过调用POSIX库里的函数来实现有关线程的各种操作。不过内核中也有一种内核级线程。

  1. 用户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进行管理。如POSIX线程库。

  2. 系统级线程(核心级线程):由操作系统内核进行管理。操作系统内核给应用程序提供相应的系统调用和应用程序接口API,以使用户程序可以创建、执行、撤消线程。

POSIX线程库:

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 “pthread_” 打头的
 
要使用这些函数库,要通过引入头文 <pthread.h>
 
链接这些线程函数库时要使用编译器命令的 “-lpthread” 选项

一、创建线程

         1 .线程创建函数

与fork父子进程类似,创建出新进程后,新线程去执行划分给他的函数。主线程继续往下执行自己的代码。这里不分先后的执行。

#include <pthread.h>

int pthread_create(
    pthread_t *thread, /* 线程id,将线程id存入,线程标识符,线程栈的起始地址,输出型参数,用以获取创建成功的线程 ID */
    const pthread_attr_t *attr,/* 设置创建线程的属性,使用 nullptr 表示使用默认属性 */
    void *(*start_routine) (void *), /* 函数地址,该线程启动后需要去执行的函数 */
    void *arg);/* 线程要去执行的函数的形参,没参数时可填写 nullptr */

    //返回值:成功返回0,失败返回错误码,如:
    //EAGAIN   描述: 超出了系统限制,如创建的线程太多。 
    //EINVAL   描述: tattr 的值无效。

错误检查:

传统的一些函数是,成功返回 0 ,失败返回 -1 ,并且对全局变量 errno 赋值以指示错误。
  
pthreads 函数出错时不会设置全局变量 errno (而大部分其他 POSIX 函数会这样做)。而是将错误代码通过返回值返回
  
pthreads 同样也提供了线程内的 errno 变量,以支持其它使用 errno 的代码。对于 pthreads 函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno 变量的开销更小

 

例:

// 新线程
void* fun(void* args)
{
    while (true)
    {
        std::cout << "新线程正在运行" << std::endl;
        sleep(1);
    }
}

// 主线程
int main()
{
    // 记录线程 id
    pthread_t tid;

    // 创建新线程,并让新线程去执行 fun 函数
    if(pthread_create(&tid, nullptr, fun, nullptr)!=0)
    {
        perror("pthread_create:");
        exit(-1);
    }

    // 主线程继续执行自己后续的代码
    while (true)
    {
        std::cout << "主线程正在运行" << std::endl;
        sleep(1);
    }

    return 0;
}

执行程序:主新进程各自自行,互不干涉

我们可以打开查看进程的监控和查看轻量级进程的监控:

可以发现,进程只有一个。线程有两个,其中一个线程的LWP与PID一样,这是主进程。比主进程LWP大1的是新进程。

解释:

  • LWP (Light Weight Process) 表示的就是线程 (轻量级进程) 的 ID,其中主线程的 LWP 值等同于进程的 PID 值。

         2 .线程传参

 pthread_create函数创建出来的线程所执行的函数的形参类型是 void* ,可以传递任何类型的参数,当然也能传对象给线程使用。

例:这里我将新线程命名传给新进程,新进程执行打印自己名字的函数

struct Data
{
    Data(const std::string &name): _name(name)
    {}
    std::string GetName()
    {
        return _name;
    }
    std::string _name;
};

void *fun(void *args)
{
    Data *td = static_cast<Data *>(args);

    while (true)
    {
        std::cout << "新线程 " << td->GetName() << " 正在运行" << std::endl;
        sleep(1);
    }
}

int main()
{
    Data *d = new Data("thread 1");

    // 创建新线程,将d对象作为fun函数的参数
    pthread_t tid;
    pthread_create(&tid, nullptr, fun, d);

    while (true)
    {
        std::cout << "主线程正在运行" << std::endl;
        sleep(1);
    }

    return 0;
}

执行结果:

         3 . 线程ID及进程地址空间布局

问题:tid 是什么样子的——虚拟地址

pthread_ create 函数会产生一个线程 ID ,存放在第一个参数指向的地址中。该线程 ID 和前面说的线程 ID(LWP)不是一回事。给用户提供的线程ID,不是内核中的LWP,而是自己维护的一个唯一值。
我们将tid打印出来看:
前面讲的线程 ID 属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create 函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程 ID ,属于NPTL 线程库的范畴。线程库的后续操作,就是根据该线程 ID 来操作线程的。
线程库 NPTL 提供了 pthread_ self 函数,可以获得线程自身的 ID:
pthread_t pthread_self ( void );
pthread_t 到底是什么类型呢?取决于实现。对于 Linux 目前实现的 NPTL 实现而言, pthread_t 类型的线程 ID 本质就是一个进程地址空间上的一个地址
系统中可以同时存在多个执行流,也就是多个线程。OS要对这些线程进行管理—— 先描述,再组织
在这里我们是链接动态线程库,所以这里是库对线程进行管理。与打开文件类似。我们打开文件的时候,会创建一个文件描述符表,将打开的文件放在这个表当中。表里面存放着文件的地址。这里的库也会创建一个数据结构将创建的线程放进去进行管理。
当创建一个新线程的时候,会先描述,创建线程控制块(如图)。然后将各个块的地址管理起来。
可以看见,每个线程有自己独立的栈和空间和数据。

pthread_create创建进程函数会将 tid 修改为现场控制块的地址。 未来我们只需要找到线程控制块的地址即可;

         4. 创建多线程 

  • 每个线程都由各自的线程 ID 所管理着,创建多线程时可以使用一个数据结构将每个创建出来的线程的 ID 管理起来。

例:

struct Data
{
    Data(const std::string &name)
        : _name(name)
    {}
    std::string thread_name()
    {
        return _name;
    }
    std::string _name;
};

void *thread_run(void *args)
{
    Data *td = static_cast<Data *>(args);

    while (true)
    {
        std::cout << "新线程 " << td->thread_name() << " 正在运行" << std::endl;
        sleep(1);
    }
    return nullptr;
}

const int gnum = 5; // 5 个线程

int main()
{
    // 存储创建的所有线程的线程 ID
    std::vector<pthread_t> tids(gnum);

    // 创建 5 个线程并给每个线程传递一个 Data 对象,都执行打印自己名字的函数
    for (size_t i = 0; i < gnum; i++)
    {
        std::string name = "thread " + std::to_string(i + 1);
        Data *td = new Data(name);
        pthread_create(&tids[i], nullptr, thread_run, td);
    }

    while (true)
    {
        std::cout << "主线程正在运行" << std::endl;
        sleep(1);
    }

    return 0;
}

执行结果:

使用 ps -aL 指令打开监控可以查到主和新线程这 6 个线程的情况。

二、线程终止

如果只是想终止某个线程而不是整个进程,可以有如下 3 种方法

1. 从线程函数 return 。这种方法对主线程不适用 , main 函数 return 相当于调用 exit
 
2. 线程可以调用 pthread_ exit 终止自己。
 
3. 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程。

         1 .使用 return 终止线程

  • return的作用是退出函数。在线程调用的函数中使用 return 退出当前线程(执行完毕,没有代码执行了,就退出了),但是在main函数中使用return代表整个进程退出(main函数执行完毕),也就是说只要主线程退出了那么整个进程就退出了,此时该进程曾经申请的资源就会被释放,而其他线程会因为没有了资源,自然而然的也退出了。

         2 .使用 pthread_exit 函数终止线程

pthread_exit 是 POSIX 线程(也称为 pthreads)库中的一个函数,用于显式地终止调用它的线程。当线程通过 pthread_exit 函数退出时,它可以返回一个指向某个对象的指针,这个对象可以是任何类型的指针。需要注意的是,返回的指针不会被自动释放,因此接收者(如通过 pthread_join 获取退出状态的线程)需要负责适当地处理这个指针(例如,释放分配的内存)。

因此:函数返回的指针所指向的内存单元必须是全局的或者是用malloc (new)分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。

#include <pthread.h>  
void pthread_exit(void *retval);

//retval:这是一个指向任意类型的指针,通常用于表示线程的退出状态或返回值。如果不需要返回值,可以传递 NULL。

注意:

  • 线程一旦调用 pthread_exit,就会立即终止执行,其调用之后的任何代码都不会被执行。
  • 如果线程通过 pthread_exit 退出,那么该线程应占用的资源(如栈空间)将由 pthread 库自动回收。
  • 使用 pthread_exit 退出的线程不会释放它通过 mallocnew 等方式分配的内存。这些内存必须在线程退出前由程序员显式释放。
  • 线程可以通过 pthread_join 函数等待另一个线程的终止,并获取该线程的返回值(通过 pthread_exit 传递的 retval 参数)。

示例:

void* thread_function(void* arg) {  
    // 线程的工作内容  
    printf("Thread is running\n");  
  
    // 假设我们在这里完成了工作,现在准备退出  
    // 返回一个指向整数的指针,表示退出状态  
    int* status = (int*)malloc(sizeof(int));  
    *status = 0; // 假设 0 表示成功  当然也可以传入其他类型指针,强转就行了
    pthread_exit((void*)status);  
  
    // 以下代码不会被执行,因为线程已经通过 pthread_exit 退出了  
    printf("This line will not be executed\n");  

    // free(status);  
}  
int main() {  
    pthread_t thread;  
  
    // 创建线程  
    pthread_create(&thread, NULL, thread_function, NULL);  
  
    int* retval=nullptr;
    // 等待线程结束,并获取其返回值  
    pthread_join(thread, (void**)&retval);  
  
    // 处理线程的返回值  
    if (thread_return != NULL) {  
        printf("Thread returned: %d\n", *thread_return);  
        free(thread_return); // 释放之前线程函数分配的内存  
    }  
  
    return 0;  
}

执行结果:

 

         3 .  使用 pthread_cancel 函数终止线程

pthread_cancel  是 POSIX 线程(pthread)库中的一个函数,用于向指定的线程发送取消请求,请求该线程终止执行。这个函数的行为和效果受到多个因素的影响,包括线程对取消请求的反应状态、取消类型的设置以及线程中取消点的存在与否。

#include <pthread.h>  
int pthread_cancel(pthread_t thread);

//thread:指定要取消的线程的标识符(ID)。

返回值:

  • 成功时返回 0
  • 失败时返回错误码。

取消请求的处理

  1. 取消点线程在接收到取消请求后,并不会立即终止。它将继续执行,直到到达一个取消点(Cancellation Point)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。这些取消点通常是特定的系统调用或库函数。与信号有一点类似,在特定的点进行检测执行。

注意:

  • pthread_cancel 调用本身并不等待线程终止,它只是发送了一个取消请求。如果需要确保线程已经终止,可以使用 pthread_join 函数。
  • 线程在无限循环或没有自然取消点的情况下,可能无法响应取消请求。在这种情况下,可以通过在循环体中添加 pthread_testcancel 调用来手动创建取消点。
  • 取消一个线程可能会导致资源释放的问题,特别是如果线程在持有锁或其他资源时被取消。因此,在编写多线程程序时,需要仔细考虑如何安全地释放这些资源。

例如:新线程执行5秒后被主线程取消

void* thread_run(void* args)
{
    std::string thread_name = static_cast<const char*>(args);

    while (true)
    {
        std::cout << thread_name << " 正在运行" << std::endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_run, (void*)"thread -1");
    std::cout << "主线程正在运行" << std::endl;
    sleep(5);
    pthread_cancel(tid);    // 主线程取消新线程
    std::cout << "新线程已被终止" << std::endl;
    
    return 0;
}

执行结果:

三、等待线程

问题:为什么需要线程等待?

已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
 
创建新的线程不会复用刚才退出线程的地址空间。

         1 .线程等待函数

  • 该函数在等待成功时会返回 0,失败时返回对应的错误码。
#include <pthread.h>

int pthread_join(
    pthread_t thread,/* 被等待的线程的线程 ID */ 
    void **retval);/* 获取被等待的线程在退出时的返回值 */
  • 调用该函数的线程将阻塞等待新线程,直到所等待的的新线程终止为止,被等待的线程以不同的方式终止时,通过 pthread_join 得到的终止状态也是不同的。

         2 .获取返回值

  • pthread_create 创建出的线程所调用函数的返回值是一个 void* 类型的值,而 pthread_join 函数的第二个输出型参数是 void** 类型,就是为了获取新线程所调用函数的返回值。

例子:

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h>  
  
// 线程运行的函数  
void* threadFunction(void* arg) {  
    // 假设我们不需要传递任何参数给线程函数  
    // 这里的arg参数只是为了满足pthread_create的签名  
    (void)arg;  
  
    // 动态分配一个字符串  
    char* message = strdup("Hello from thread!");  
    if (message == NULL) {  
        // 如果内存分配失败,则线程返回NULL  
        return NULL;  
    }  
  
    // 线程返回字符串的地址  
    return (void*)message;  
}  
  
int main() {  
    pthread_t threadId;  
    void* threadResult;  
    char* message;  
  
    // 创建一个新线程  
    if (pthread_create(&threadId, NULL, threadFunction, NULL) != 0) {  
        printf("Error: unable to create thread\n");  
        return 1;  
    }  
  
    // 等待线程结束并获取其返回值  
    if (pthread_join(threadId, &threadResult) != 0) {  
        printf("Error: unable to join thread\n");  
        return 1;  
    }  
  
    // 将void*类型的返回值转换为char*类型  
    message = (char*)threadResult;  
  
    // 打印字符串  
    printf("%s\n", message);  
  
    // 释放字符串以避免内存泄漏  
    free(message);  
  
    return 0;  
}

执行结果:

我们也可以返回自定义类型(对象)。这可以让我们得到线程执行任务结束后给我们返回执行的结果。但是要记住释放返回的内存

调用该函数的线程将挂起等待 , 直到 id thread 的线程终止 thread 线程以不同的方法终止 , 通过 pthread_join 得到的终止状态是不同的,总结如下:
1. 如果 thread 线程通过 return 返回 ,value_ ptr 所指向的单元里存放的是 thread 线程函数的返回值。
 
2. 如果 thread 线程被别的线程调用 pthread_ cancel 异常终掉 ,value_ ptr 所指向的单元里存放的是常数PTHREAD_ CANCELED。
 
3. 如果 thread 线程是自己调用 pthread_exit 终止的 ,value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。
 
4. 如果对 thread 线程的终止状态不感兴趣 , 可以传 NULL value_ ptr 参数。

    

四、分离线程

         1 .线程分离概念

一般情况下,主线程是需要阻塞等待新线程的,如果不等待线程退出,就有可能造成类似 “僵尸” 问题。


但如果主线程就是不想等待新线程退出,也不关心新线程的返回值,此时就可以将新线程分离,被分离的线程在退出时会自动释放资源。类似信号设置为忽略一样。


分离出的新线程依旧要使用进程的资源,且依旧在该进程内运行,甚至这个新线程崩溃了也会影响整个进程。线程分离只是让主线程不需要再阻塞等待整个新线程罢了,在新线程退出时系统会自动回收该线程所持有的资源。


一个线程可以将其他线程分离出去,也可以将自己分离出去。

pthread_detach(pthread_self());


等待和分离会发生冲突一个线程不能既是可被等待的又是分离出去的
虽然分离出去的线程已经不归主线程管了,但一般还是
建议让主线程最后再退出
分离出去的线程可以被 pthread_cancel 函数终止,但不能被 pthread_join 函数等待。

         2 .线程分离函数

#include <pthread.h>

int pthread_detach(pthread_t thread);    // thread 是要分离出去的线程的 ID

//线程分离成功时返回 0,失败时则返回对应错误码。

例:

void* thread_function(void* arg) {  
    // 线程执行的代码  
    printf("Thread is running\n");  
    return nullptr;
}  
  
int main() {  
    pthread_t thread_id;  
    int result;  
  
    // 创建线程  
    result = pthread_create(&thread_id, NULL, thread_function, NULL);  
    if (result != 0) {  
        perror("Failed to create thread");  
        exit(EXIT_FAILURE);  
    }  
    // 将线程设置为分离状态  
    result = pthread_detach(thread_id);  
    if (result != 0) {  
        perror("Failed to detach thread");  
        exit(EXIT_FAILURE);  
    }  
    sleep(3);
    int n=pthread_join(thread_id,nullptr);
    std::cout<<"pthread_join return val : "<<n<<std::endl;  
    return 0;  
}

执行结果:

可以看到,线程分离后pthread_join的返回值是22,说明等待出错。


原文地址:https://blog.csdn.net/2301_77438812/article/details/142376657

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!