当前位置: 首页 > article >正文

Linux —— 《线程控制》

文章目录

  • 前言:
  • 为什么要链接pthread库?
  • 线程控制:
    • 线程创建:
      • start_routine?
      • 传递自定义类型
        • 同一份栈空间?
    • 线程等待:
      • 返回值与参数?
      • 创建多线程
    • 线程终止
    • 线程分离

前言:

上一文我们学习了解了线程的相关概念,我们以生动形象的概念阐释了线程这一具体的概念,我们同时也输出了一个重要的结论:“线程是CPU调度的基本单位”。在那一刻,我们同样也输出了重要的知识,在Linux中不存在进程和线程控制块这个区分,而针对于Linux,CPU只认轻量级进程,而不是什么线程控制块。
那同样,进程我们是存在控制的,例如创建、删除、等待和替换,那么针对于线程我们同样也是要对其进行管理控制的,因此本文重点讲解线程控制

为什么要链接pthread库?

在正式讲解线程控制的具体操作,我想先来打通一个疑惑,就是为什么我们上次在写代码时,要在使用g++编译的时候加上选项-pthread?这个操作的本质是链接pthread库,那为什么我要链接它呢?

让我们先暂时回到上文,还记得我说过,Linux本质没有线程的,就算你非说有,那也只是轻量级进程,而我们目前想认为你Linux生成的就是线程,所以就有两个概念—— “用户级线程”和“内核级线程”
image-20241129225750240

很明显这是两个不同的概念,而为了将这两个概念打通,就不得不提供接口,让用户按照普通的方式调用函数使用线程即可,也使用户也可以认为自己使用的就是现场,同时又可以兼顾Linux系统使用轻量级进程的方法。因此我们就不得不使用pthread库。
image-20241129230242000

线程控制:

线程创建:

我们使用函数:pthread_create
在上一文我们也介绍和粗略的使用过,但是我们并没有研究其参数的各种含义,下面我们就来介绍和使用。

#include <pthread.h>

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

线程创建成功时返回 0,创建失败时返回错误码。

start_routine?

这个和我们的当初创建的学习信号中自定义捕捉很像,同样是传递一个函数指针,但是这里传进来的函数的我们并没有介绍介绍

void* start_routine(void* args)

函数呈现的是这样的一种格式,这个函数和自定义捕捉的函数一致,捕捉到了信号我们就在这个函数里做动作,同理创建好线程后,这里就是线程执行的地方。举个最简单的例子:

// 创建单线程
#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* start_routine(void* args)
{
    while(true)
    {
        sleep(1);
        std::cout << "new thread running..." << std::endl;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, nullptr);
    while(true)
    {
        std::cout << "main thread running..." << std::endl;
        sleep(1);
    }
    return 0;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

传递自定义类型

要知道我们这两个参数的类型都是void*的,这就给了我们很多可能,我们不仅仅可以传递普通的类型,我们也可以传递结构体/类,这大大的提高了我们使用线程的灵活性:

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

class Person
{
public:
    std::string Print()
    {
        return _name + ":" + std::to_string(_age);
    }
    std::string _name;
    int _age;
};

void* start_routine(void* args)
{
    Person *tp = (Person *)args;
    std::string info = tp->Print();
    while (true)
    {
        sleep(1);
        std::cout << "new thread running..." << info << std::endl;
    }
}

int main()
{
    Person p;
    p._age = 20;
    p._name = "Eric";

    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, &p);
    while(true)
    {
        std::cout << "main thread running..." << std::endl;
        sleep(1);
    }
    return 0;
}

PixPin_2024-11-30_14-03-20

同一份栈空间?

这里见识到了线程的灵活之处,不仅仅可以传递内置类型的数据,就连我们的自定义类型的数据我们都能进行传递,但是这种做法显然不是很好的。
因为在进行对象实例化的时候,对象是在main函数的栈区创建的,然后再传递给新线程。换句话说,就是主线程与新线程共用了同一个栈空间里的资源,这很明显是不对的,因为一但我对主线程的对象进行修改,新线程同样也会看到,并同样也会对其修改造成最终的数据与我们期待的不一致。这个问题通常会发生在创建多线程时,当我需要修改原来的对象然后传递给新线程2的时候,因此我们在这里不得不停下来好好思考思考。

image-20241130142154146

在这里先对代码进行了修改:

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

class Person
{
public:
    std::string Print()
    {
        return _name + ":" + std::to_string(_age);
    }
    std::string _name;
    int _age;
};

void* start_routine(void* args)
{
    while(true)
    {
        sleep(1);
        Person *tp = (Person *)args;
        std::string info = tp->Print();
        std::cout << "new thread running..." << info << std::endl;
    }
}

int main()
{
    Person p;
    p._age = 20;
    p._name = "Eric";
	
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, &p);

    std::cout << "ready to change data" << std::endl;
    sleep(3);
    // 修改数据
    p._name = "Alan";
    p._age = 18;
    std::cout << "data changed" << std::endl;
    sleep(30); // 这里我简化了,其实应该是释放新线程再退出主线程好一点。

    return 0;
}

在这里插入图片描述
这个错误不是没有解决办法的,我们要解决的就是主线程用自己的空间,新线程也用自己的空间,那很好办,直接在各自的堆上new一个就好了。这样就算你创建了两个新线程,这两个线程的数据也是在堆空间上独立的!
image-20241130142341382

线程等待:

当然和之前进程一样,主线程也是要等待新线程的结束然后回收资源,这当然也是避免一种类似“僵尸进程”的情况,但是线程提供的接口我们又能做很多工作,下面我们就来看看看线程等待的具体操作。
首先认识接口函数:pthread_join

#include <pthread.h>

int pthread_join(
    	pthread_t thread,	/* 被等待的线程的线程 ID */ 
    	void **retval);		/* 获取被等待的线程在退出时的返回值 */

与进程不同的是,这里的线程等待默认就是阻塞等待,直到所等待的的新线程终止为止。
同样的是,等待成功会返回0,否则返回对应的错误码,而这个错误码并不与进程等待失败的那个位图一致。

测试一下:

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

void *start_routine(void* args)
{
    int cnt = 5;
    while(cnt)
    {
        std::cout << "new thread running... cnt: " << cnt << std::endl;
        --cnt;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    std::cout << "main thread running..." << std::endl;

    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, nullptr);

    int n = pthread_join(tid, nullptr);
    if(n == 0)
    {
        std::cout << "wait suceess!" << std::endl;
    }
    else
    {
        std::cout << "wait failed...." << std::endl;
    }
    return 0;
}

PixPin_2024-11-30_14-45-46

返回值与参数?

我们在刚刚的初次使用上,并没有研究第二个参数retval,而这里要注意它的类型是void**,而它的作用,是用来接收被等待的线程结束时的返回值,那么我们就可以浅浅的试一试使用它:

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

void *start_routine(void* args)
{
    int cnt = 5;
    while(cnt)
    {
        std::cout << "new thread running... cnt: " << cnt << std::endl;
        --cnt;
        sleep(1);
    }
    return (void*)"thread done"; // 返回一个stirng类型
}

int main()
{
    std::cout << "main thread running..." << std::endl;
    
    // 线程创建
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, nullptr);
	
    // 线程等待
    void *ret = nullptr; // 用来接收线程结束时的返回值
    int n = pthread_join(tid, &ret);
	
    std::string r = (const char *)ret; // 强制类型转换,获得返回值的合适类型
    if (n == 0)
    {
        std::cout << "wait suceess!" << std::endl;
        std::cout << "new thread return: “" << r << "”"<< std::endl;
    }
    else
    {
        std::cout << "wait failed...." << std::endl;
    }
    return 0;
}

PixPin_2024-11-30_14-56-26

同样的我们也可以对自定义类型进行传参,比如这里我还是创建Person类,但是我这里只是new一个而不对其进行初始化,相当于直接传递了一个“空类”,然后我们在新创建的线程中,对这个“空类”进行初始化,然后我们返回这个类,看看我们是否能读取出来?

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

class Person
{
public:
    std::string _name;
    int age;
};

void *start_routine(void* args)
{
    int cnt = 5;
    while(cnt)
    {
        std::cout << "new thread running... cnt: " << cnt << std::endl;
        --cnt;
        sleep(1);
    }

    // 设置对象的数据,并返回
    Person* pt = (Person*)args;
    pt->_name = "Carl";
    pt->age = 19;
    return (void *)pt;
}

int main()
{
    std::cout << "main thread running..." << std::endl;

    // new实例化对象
    Person *p = new Person;
    // 创建线程
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, p);

    void *ret = nullptr; // 用来接收线程结束时的返回值
    int n = pthread_join(tid, &ret);

    Person *r = (Person *)ret; // 强制类型转换,获得返回值的合适类型
    if (n == 0)
    {
        std::cout << "wait suceess!" << std::endl;
        std::cout << "new thread return: “" << r->_name << ":" << r->age << "”" << std::endl;
    }
    else
    {
        std::cout << "wait failed...." << std::endl;
    }
    return 0;
}

PixPin_2024-11-30_15-04-47

创建多线程

结合已有的知识,我们已经完全具备创建多线程,然后在主线程结束之前回收这些多线程,比如现在我想创建10个线程,然后各个线程去打印一段话就好了,最后再一次返回,主线程依次等待回收。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <vector>

const pthread_t num = 10;
void *start_routine(void *args)
{
    std::string tt = (const char *)args;
    std::cout << tt << " is running..."<< std::endl;
    sleep(1);
    return args;
}

int main()
{
    // 创建线程
    std::vector<pthread_t> tids;
    for (int i = 1; i <= num; ++i)
    {
        char *name = new char[128];
        snprintf(name, 128, "thread_%d", i);
        pthread_t tid;
        pthread_create(&tid, nullptr, start_routine, name);
        tids.push_back(tid);
    }

    // 回收线程
    for (int i = 0; i < tids.size(); ++i)
    {
        void *ret = nullptr;
        pthread_join(tids[i], &ret);
        char *r = (char *)ret;
        std::cout << r << "quit..." << std::endl;
    }
    return 0;
}

image-20241130155424335

线程终止

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

  1. 使用 return 终止线程:非主线程可以在执行的函数中使用 return 终止当前线程。
  2. 使用 pthread_exit 终止线程:线程自己可以调用该函数终止自己。
  3. 使用 pthread_cancel 终止线程:该函数能通线程 ID 终止任意线程。

切记你最好不要用exit(1)这样的方式终止线程,因为这不仅仅会终止线程,还会直接终止掉你的进程,这个必须要额外注意!

线程分离

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放
资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

#include <pthread.h>

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

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach( pthread_self() );

注意:这里的pthread_self()是获取自己线程的ID,哪个线程调用这个函数就返回哪个线程的ID

pthread_self 函数获得的线程 ID 不等于内核的 LWP 值,pthread_self 函数获得的是用户级原生线程库的线程 ID,而 LWP 是内核的轻量级进程ID,它们之间是一对一的关系。
后续我们会逐一展开

**一个线程要是被分离了,那么该线程就是处于分离状态,是不能被join的!但是依旧属于进程内部,只是不再需要被等待了!**joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

  • 虽然分离出去的线程已经不归主线程管了,但一般还是建议让主线程最后再退出。
  • 分离出去的线程可以被 pthread_cancel 函数终止,但不能被 pthread_join 函数等待。
  • 一个线程可以将其他线程分离出去,也可以将自己分离出去。

http://www.kler.cn/a/419112.html

相关文章:

  • nmap基本用法
  • 【小白学机器学习39】如何用numpy生成总体,生成样本samples
  • 【RISC-V CPU debug 专栏 2.3 -- Run Control】
  • .NET周刊【11月第4期 2024-11-24】
  • React与Ant Design入门指南
  • springboot336社区物资交易互助平台pf(论文+源码)_kaic
  • Linux命令进阶·如何切换root以及回退、sudo命令、用户/用户组管理,以及解决创建用户不显示问题和Ubuntu不显示用户名只显示“$“符号问题
  • 桶排序(代码+注释)
  • webUI自动化(十)iframe切换
  • 【docker集群应用】Docker数据管理与镜像创建
  • Flutter:encrypt插件 AES加密处理
  • 10.请求拦截和响应拦截
  • Rust代写 OCaml代做 Go R语言 SML Haskell Prolog DrRacket Lisp
  • Jackson库--ObjecMapper
  • vue3 与 spring-boot 完成跨域访问
  • Maven java 项目,想执行verify阶段指令,通常需要配置哪些插件呢?
  • YOLOv8-ultralytics-8.2.103部分代码阅读笔记-ops.py
  • Java知识及热点面试题总结(二)
  • 远程桌面协助控制软件 RustDesk v1.3.3 多语言中文版
  • 精准用户获取与私域流量运营:多商户链动 2+1 模式商城小程序的赋能策略