【Linux】Linux线程控制

一. Linux线程控制

1.1 验证线程的问题

验证线程是可以被创建出来的,以及在Linux中线程是用轻量级进程模拟实现的

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>

void *threadrun(void *arg)
{
    std::string name((char *)arg);
    while (1)
    {
        std::cout << "我是一个新线程,name:" << name << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");
    while (1)
    {
        std::cout << "我是主线程" << std::endl;
        sleep(1);
    }
    return 0;
}


pthread_create创建了一个新线程,新线程执行threadrun函数,主线程继续向下执行

新线程执行的threadrun的起始地址就是新线程的入口地址,main函数的地址就是主线程的入口地址,两个线程执行各自的代码!

对于线程执行不同代码的代码块划分,根本不用我们划分,编译器已经为我们划分好了!

undefined reference to pthread_create没有被定义是因为pthread是第三方库,不是C/C++的标准库,因此在链接的时候需要指明链接的库名称,不需要指明查找路径是因为这个库在lib64目录下,系统会自动去到这个路径下查找



通过上面的 ps axj | head -1 && ps axj | grep TestThread指令查询到只有一个进程,可以验证创建的是两个线程,线程是进程的一个分支。使用kill -9 杀死进程,两个线程都被干掉,说明信号是发给进程的,进程一旦退出,其线程分支也会退出

使用ps -aL可以查看OS中运行的线程

可以看到两个线程的pid是一样的,但是LWP不一样,LWP:light weight process就是轻量级进程

pid和LWP相同的线程是主线程

那CPU在调度的时候是看PID还是LWP?

LWP,因为线程是CPU调度的基本单位,LWP是线程编号

那之前的进程为什么使用PID进行调度?

因为之前的进程是单进程,PID==LWP,所以用PID也是可以进行调度的。

这样就能说明Linux中是用轻量级进程模拟实现的线程。

线程的时间片划分问题:

当一个进程创建了多个线程后,这些线程共享进程的时间片,也就是进程的时间片被等分,这样就避免了有的恶意程序创建多个线程来占据时间片
线程异常

一个线程出现异常,整个进程都会被干掉

.

消息混杂是因为两个线程访问的是同一个共享的显示器文件,如果想要避免这种情况需要加锁。

1.2 pthread线程库(POSIX线程库)

为什么要有这个库?这个库是什么东西

pthread库叫做原生线程库

cpp 复制代码
// C++11
void threadrun()
{
    while (1)
    {
        std::cout << "我是一个新线程" << std::endl;
        sleep(1);
    }
}

int main()
{
    std::thread t(threadrun);
    while(1)
    {
        std::cout << "我是主线程" <<std::endl;
        sleep(1);
    }
    t.join();
    return 0;
}

1.3 线程控制

1.3.1 线程创建

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

  • pthread_t *thread 是线程的id,输出型参数
  • const pthread_attr_t *attr 线程的属性,默认是nullptr
  • void *(*start_routine) (void *) 线程要执行的函数的入口,返回值为void* 参数为void* 的函数
  • void *arg传给线程的参数
  • 返回值:成功返回0,失败返回错误号

1.3.2 线程等待

复制代码
int pthread_join(pthread_t thread, void **retval);
On success, pthread_join() returns 0; on error, it returns an error number.

pthread_t thread 线程ID
void **retval线程退出结果,因为线程调用函数的返回值是指针,要接受这个返回值就需要传指传参,就需要二级指针。

线程创建好后,主线程必须等待回收新线程,否则会造成类似僵尸进程,导致内存泄漏!
pthread_self()返回当前线程的线程号

cpp 复制代码
void showid(pthread_t &tid)
{
    printf("%ld", tid);
}

std::string Fomatid(const pthread_t &tid)
{
    char ch[64];
    snprintf(ch, sizeof(ch), "0x%lx", tid);
    return ch;
}

void *rout(void *args)
{
    std::string name = static_cast<const char *>(args);
    int cnt = 3;
    while (cnt--)
    {
        sleep(1);
        std::cout << "我是一个新线程,name:" << name << "线程id:" << Fomatid(pthread_self()) << std::endl;
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, rout, (void *)"thread-1");
    showid(tid);
    int cnt = 3;
    while (cnt--)
    {
        std::cout << "我是main线程,线程号:" << Fomatid(pthread_self()) << std::endl;
        sleep(1);
    }
    return 0;
}

为什么看到的线程号和LWP不一样呢?因为在用户层面不能让用户看到LWP的值,用户层只知道线程而不能知道轻量级进程的概念。

主线程和新线程都能够调用同一个函数,因为他们共享地址空间,而且主线程和新线程同时修改全局变量,全局变量也不会发生写时拷贝

在主线程和新线程同时调用Formatid函数的时候,这个函数被重入了,叫做可重入函数,但是没有涉及全局的变量,这个函数是没有问题的

cpp 复制代码
class Task
{
public:
    Task(int a, int b) : _a(a), _b(b)
    {
    }
    int Excute()
    {
        return _a + _b;
    }
    ~Task() {}

private:
    int _a;
    int _b;
};

class Result
{
public:
    Result(int ret) : _result(ret)
    {
    }
    int GetRes() { return _result; }
    ~Result() {}

private:
    int _result;
};

void *rout(void *args)
{
    Task *t = static_cast<Task *>(args);
    Result *res = new Result(t->Excute());
    sleep(1);
    return res;
}

int main()
{
    pthread_t tid;
    Task *t = new Task(1, 2);
    pthread_create(&tid, nullptr, rout, t);

    Result *ret = nullptr;
    pthread_join(tid, (void **)&ret);
    std::cout << "ret = " << ret->GetRes() << std::endl;
    return 0;
}

对于pthread_create创建的新线程,执行新线程的函数,可以传任意类型的参数,只要做好类型转换就好。

  1. main 函数结束代表主线程结束,一般也代表进程结束
  2. 新线程对应的入口函数结束,代表当前线程结束

1.3.3 线程终止

  1. 线程的入口函数终止,线程也就终止了!!

    能不能用exit()函数终止线程呢?答案是不能的,因为exit()是用来终止进程的

    2.pthread_exit()

cpp 复制代码
NAME
       pthread_exit - terminate calling thread

SYNOPSIS
       #include <pthread.h>

       void pthread_exit(void *retval);

void *retval传void* 类型的指针来作为返回值

pthread_exit(res)return res效果等效

  1. pthread_cancel

    取消线程一定是在目标线程跑起来之后取消,线程被取消,返回值是-1,是PTHREAD_CANCELED

线程退出,pthread_join输出型参数拿到的返回值一定就是线程设定的返回值,如果线程异常,进程终止,pthread_join也拿不到结果

1.3.4 线程分离

在线程中有阻塞等待和非阻塞等待回收子进程的方法,0(阻塞)和WNOHANG(非阻塞),但是线程等待回收只有阻塞回收,那想要新线程结束后自动回收释放,不要主线程阻塞等待回收有什么方法吗?

将线程的状态由默认的joinable修改为分离状态(!joinable)。

线程分离:主线程将新线程在主线程中分离出去;新线程将自己在主线程中分离出去

cpp 复制代码
NAME
       pthread_detach - detach a thread

SYNOPSIS
       #include <pthread.h>

       int pthread_detach(pthread_t thread);// 分离的线程号
RETURN VALUE
       On success, pthread_detach() returns 0; on error, it returns an error number.
cpp 复制代码
//主线程将新线程分离
int main()
{
    pthread_t tid;
    Task *t = new Task(1, 2);
    pthread_create(&tid, nullptr, rout, t);
    pthread_detach(tid);
    return 0;
}
// 新线程将自己在主线程中分离
void *rout(void *args)
{
    pthread_detach(pthread_self());
    return res;
    // pthread_exit(res);
}

分离的线程,依旧在进程的地址空间中,进程的所有资源,被分离的线程依旧可以访问可以操作。只是主进程不再等待新进程了。如果主线程join被分离的线程,join会失败报错!

1.3.5 创建多线程

这里我们创建了10个线程,为什么这10个线程的i值都是9呢?

因为每次传给新线程的是id的起始地址,而在for循环每次进行的时候,id数组不会重新创建而是会重写覆盖掉原来的数组内容,因此在新进程sleep的时候,原来的数据被覆盖掉了,即使将id写成全局的数组也没办法改变这一情况。

cpp 复制代码
void *routine(void *args)
{
    sleep(1);
    std::string name = static_cast<const char *>(args);
    delete (char*)args;
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "我是新线程:name" << name << std::endl;
    }
    return nullptr;
}

int main()
{
    std::vector<pthread_t> tids;
    // 创建多线程
    for (int i = 0; i < num; i++)
    {
        pthread_t tid;
        char* id = new char[64];
        snprintf(id, 64, "thread-%d", i);
        int n = pthread_create(&tid, nullptr, routine, id);
        if (n == 0)
            tids.push_back(tid);
        else
            continue;
    }

    for (int i = 0; i < num; i++)
    {
        int n = pthread_join(tids[i], nullptr);
        if (n == 0)
            std::cout << "回收成功" << std::endl;
    }
    return 0;
}

手动申请id空间就能避免向同一块内存写数据的情况,当然申请的空间需要手动释放掉。

二. 线程id及进程地址空间布局

2.1线程id及进程地址空间布局

2.2 LWP和库中的线程联动

线程ID:线程ID是pthread库在创建线程时线程控制块(TCB)的起始虚拟地址

线程传参和返回值?

线程的返回值在执行结束后会写到TCB的result属性中

线程分离?

在线程的控制块TCB中还有一个int joinable的属性,线程创建时默认为1,线程退出时需要join等待回收;当线程被分离的时候,就会将这个属性写0,线程结束后线程TCB在用户空间会自动被释放。
Linux所有的线程都在库中实现,不同进程的线程之间不能互相访问,因为进程的地址空间不一样

事实上,线程创建的所有资源,其他同进程下的线程都能访问到,因为地址空间是相同的,只是为了数据干净,我们程序员控制线程不去访问其他线程的资源。

2.3 线程局部存储

我们知道,线程之间的数据也是共享的,只不过程序员控制着线程不去访问这些不属于该线程的数据。

线程的局部存储是指对于一个被__thread关键字修饰的全局变量,在每一个线程的空间内都开辟一块空间用于存储该变量,且线程之间互补可见,A线程对它做修改,B线程看不到。因此也就不存在并发问题。

相关推荐
Aevget4 小时前
MFC扩展库BCGControlBar Pro v37.2新版亮点:控件功能进一步升级
c++·mfc·界面控件
四维碎片4 小时前
QSettings + INI 笔记
笔记·qt·算法
Tansmjs4 小时前
C++与GPU计算(CUDA)
开发语言·c++·算法
zzcufo5 小时前
多邻国第5阶段17-18学习笔记
笔记·学习
BlackWolfSky5 小时前
鸿蒙中级课程笔记4—应用程序框架进阶1—Stage模型应用组成结构、UIAbility启动模式、启动应用内UIAbility
笔记·华为·harmonyos
LUCIFER5 小时前
[驱动进阶——MIPI摄像头驱动(五)]rk3588+OV13855摄像头驱动加载过程详细解析第四部分——ISP驱动
linux·驱动开发
中屹指纹浏览器6 小时前
指纹浏览器性能优化实操——多实例并发与资源占用管控
经验分享·笔记
暮云星影6 小时前
四、linux系统 应用开发:UI开发环境配置概述 (一)
linux·ui·arm
挖矿大亨6 小时前
c++中的函数模版
java·c++·算法
阿基米东6 小时前
基于 C++ 的机器人软件框架(具身智能)开源通信库选型分析
c++·机器人·开源