Linux之线程控制

线程控制

Linux下没有真正意义的线程 , 只有轻量级进程的概念, 所以Linux OS只会提供轻量级进程创建的系统调用, 不会直接提供线程创建的接口.

用户在使用线程接口创建了一个线程, 实际是在中间的软件层把一个线程 对应到内核里的一个LWP , 用户认为的线程在内核就是对应一个LWP. 所以Linux对于线程的解决方案是中间的软件层解决, 但这个软件层并不属于OS, 而是设计者封装 的名为pthread原生线程库.

pthread 线程库是应用层原生 线程库, 应用层 指的是这个线程库并非系统调用接口提供的, 而是第三方为我们提供的, 原生指的是大部分Linux系统都会默认帮我们安装好该线程库.

线程创建

之前在线程的简单控制下, 我们只给线程传递了一个参数, 要想传递多个参数, 我们直接给线程传递一个对象即可:

我们创建了一个数据对象, 其中包括了线程名称, 创建时间, 执行的函数, 向pthread_create传参时只需把Thread_Data*转为void*, 然后在ThreadRoutine内使用的时候转换回去即可:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <functional>
#include <vector>

using func_t = std::function<void()>;
const int threadnum = 5;

class Thread_Data
{
public:
    Thread_Data(const std::string& s, uint64_t t, func_t f)
    :threadName(s)
    ,createTime(t)
    ,func(f)
    {}

    std::string threadName;
    uint64_t createTime;
    func_t func;
};

void Print()
{
    std::cout << "I am one of the threads!" << std::endl;
}

void* ThreadRountine(void* arg)
{
    Thread_Data* td = static_cast<Thread_Data*>(arg);//隐式类型转换
    usleep(1000);//调整一下函数执行次序

    while(true)
    {
        std::cout << "new thread" << " thread name: " << td->threadName << " create time: " << td->createTime << std::endl;
        td->func();

        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    std::vector<pthread_t> tids;

    for(int i = 1; i <= threadnum;i++)
    {
        char name[64];
        snprintf(name, sizeof(name), "%s-%d", "thread", i);
        Thread_Data* arg = new Thread_Data(name, time(nullptr), Print);
        
        pthread_create(&tid, nullptr, ThreadRountine, (void*)arg);
        tids.push_back(tid);

        sleep(1);
    }
    
    std::cout << std::endl;
    
    while(true)
    {
        std::cout << "I am main  thread" << std::endl;
        sleep(1);
    }

    return 0;
}

调用ps -aL可以发现有6个线程在运行, 运行结果有些不整齐, 不能确定哪个线程先被调度:

在运行函数内故意触发一个异常:

cpp 复制代码
void* ThreadRountine(void* arg)
{
    Thread_Data* td = static_cast<Thread_Data*>(arg);//隐式类型转换
    usleep(1000);//调整一下函数执行次序

    while(true)
    {
        std::cout << "new thread" << " thread name: " << td->threadName << " create time: " << td->createTime << std::endl;
        td->func();

        if(td->threadName == "thread-4")
        {
            std::cout << td->threadName << " 触发了异常!!!!!" << std::endl;
            int a = 1;
            a /= 0; // 故意制作异常
        }
        sleep(1);
    }
}

如我们之前所说, 多线程如果一个线程崩溃, 整个进程都会崩溃:


线程获取自身id

pthread_ create函数会产生一个线程ID, 存放在第一个参数指向的地址中. 该线程ID和前面说的线程LWP不是一回事, 线程LWP属于进程调度的范畴, 因为线程是轻量级进程, 是操作系统调度器的最小单位, 所以需要一个数值来唯一表示该线程.

pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID, 属于NPTL线程库的范畴. 线程库的后续操作, 就是根据该线程ID来操作线程的.

线程库NPTL提供了pthread_ self函数, 可以获得线程自身的ID:

pthread_t pthread_self(void);

头文件: pthread.h

功能: 获取线程自己的tid

返回值: 返回自己的tid


线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return. 这种方法对主线程不适用, 从main函数return相当于调用exit.

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

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

方法一: return

线程执行的函数返回值是void*, 不需要返回值的话, 我们返回空指针就能终止该线程:

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

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    while(true)
    {
        std::cout << "I am main thread" << std::endl;
        sleep(1);
    }
    return 0;
}

**方法二:**pthread_ exit

POSIX线程库提供了一个接口用于结束线程, void pthread_exit(void* retval);

void pthread_exit(void* retval);

头文件:pthread.h

功能:终止当前线程

参数:void* retval是线程的返回值, 目前暂时设置为空指针, 注意不要指向一个局部变量.

返回值:无返回值,跟进程一样, 线程结束的时候无法返回到它的调用者(自身)

cpp 复制代码
void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }
    //return nullptr;
    pthread_exit(nullptr);
}

运行结果和上面一样.

方法三: pthread_cancel

int pthread_cancel(pthread_t thread);

头文件:pthread.h

功能:取消标识符为thread的线程。

参数:pthread_t thread是需要取消的线程标识符

返回值:取消成功返回0,取消失败返回错误码。

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

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 2;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }

    return (void *)10;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    sleep(2);

    int n = pthread_cancel(tid);//主线程休眠2秒后, 终止进程
    std::cout << "cancel success: " << tid << ", n: " << n << std::endl;

    return 0;
}

可以看到主线程把创建的子线程取消成功, 返回值为0

注意: 如果直接调用exit()呢?

进程直接被终止, 所以多线程中线程退出不要轻易使用exit, 会导致整个进程都退出.


线程等待与线程返回值

和进程一样, 线程在执行完毕时, 如果task_struct结构体不回收, 就会导致内存泄漏(类似未被回收的僵尸进程). 所以我们需要使用 pthread_join 函数将线程加入等待队列, 加入等待队列的线程会被回收, 但是回收的现象我们是看不到的.

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

头文件: pthread.h

功能: 将标识符为tid的线程加入等待队列。

参数: pthread_t thread是需要等待的线程标识符, void** retval是线程结束返回的信息, 是一个输出型参数

**返回值:**等待成功返回0, 等待失败返回错误码.

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

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }
    pthread_exit(nullptr);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    pthread_join(tid, nullptr);
    std::cout << "wait success: " << tid << std::endl;
    return 0;
}

即使不调用pthread_join, 线程依然会正常退出, 但是资源将不会被回收, 从而导致资源泄漏

返回值

  1. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数, 正如我们上面那样.

如果需要线程给主线程一个返回值呢? 分为3种情况:

2. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值

3. 如果thread线程是自己调用pthread_exit终止的, value_ptr所指向的单元存放的是pthread_exit的参数

比如, 可以在threadRoutine函数中以void*的格式返回10:

但是这个变量要怎么让主线程接收到呢?

pthread_join函数有一个**输出型参数void** retval,**由于是输出型参数, 需要被修改的内容是void*类型, 所以使用二级指针. 我们在主线程内定义一个void*类型的ret指针变量, 当一个线程被回收的时候, 将&ret传参, 它的返回值就会被放进这个ret里.

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

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 2;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }

    pthread_exit((void *)10);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    void* ret = nullptr;
    pthread_join(tid, &ret);
    std::cout << "wait success: " << tid << ", ret: "<< (int64_t)ret <<std::endl;
    return 0;
}

不仅传参可以传递一个对象参数, 我们也可以返回一个对象:

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

class ThreadReturn
{
public:
    ThreadReturn(pthread_t id, const std::string &info, int code)
        : _id(id), _info(info), _code(code)
    {}

public:
    pthread_t _id;
    std::string _info;
    int _code;
};

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 2;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }

    ThreadReturn* ret = new ThreadReturn(pthread_self(), "thread quit normal", 10);
    pthread_exit((void*)ret);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    void* ret = nullptr;
    int n = pthread_join(tid, &ret);
    std::cout << "wait success: " << tid << "n: " << n << std::endl;

    ThreadReturn* r = static_cast<ThreadReturn*>(ret);
    std::cout << "main thread get new thread info:" << r->_code << ", " << r->_id << ", " << r->_info << std::endl;
    delete r;

    return 0;
}
  1. 如果thread线程被别的线程调用pthread_ cancel异常终掉, value_ ptr所指向的单元里存放的是常数-1, 库中其实是一个宏 PTHREAD_ CANCELED:
cpp 复制代码
#define PTHREAD_CANCELED ((void *) -1)
cpp 复制代码
#include<pthread.h>
#include<iostream>
#include<unistd.h>

class ThreadReturn
{
public:
    ThreadReturn(pthread_t id, const std::string &info, int code)
        : _id(id), _info(info), _code(code)
    {}
    
public:
    pthread_t _id;
    std::string _info;
    int _code;
};

std::ostream& operator<<(std::ostream& o, ThreadReturn* tr)
{
    o << tr->_code << ", " << tr->_id << ", " << tr->_info;
    return o;
}

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 2;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }

    ThreadReturn* ret = new ThreadReturn(pthread_self(), "thread quit normal", 10);
    pthread_exit((void*)ret);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    sleep(1);
    int n = pthread_cancel(tid);//1秒后取消tid进程
    std::cout << "cancel success: " << tid << ", n: " << n << std::endl;

    void* ret = nullptr;
    n = pthread_join(tid, &ret);//等待被取消的进程
    std::cout << "wait success: " << tid << ", n: " << n << ", ret: " << (int64_t)ret << std::endl;
   
    return 0;
}

线程分离

默认情况下, 新创建的线程是 joinable的, 线程退出后, 需要对其进行pthread_join操作, 否则无法释放资源, 从而造成系统泄漏.

但是有时根本不关心线程的返回值, 那pthread_join的阻塞式等待就会成为负担. 这个时候, 我们可以用pthread_detach函数将线程分离, 当线程退出时,系统自动释放线程资源.

int pthread_detach(pthread_t thread);

头文件:pthread.h

功能:设置标识符为thread的线程分离状态。

参数:pthread_t thread是需要分离的线程标识符。

返回值:取消成功返回0,取消失败返回错误码。

这个函数既可以分离线程组内的其他线程, 也可以调用pthread_self分离自己. 我们创建一个新线程, 让新进程在第一步就分离执行, 观察主线程的返回值判断能否成功回收它.

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

void* threadRoutine(void*arg)
{
    pthread_detach(pthread_self());//线程函数内部分离自己
    std::string name = static_cast<const char*>(arg);
    int cnt = 2;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    sleep(1);

    int n = pthread_join(tid, nullptr);//等待被取消的进程
    std::cout << "wait success: " << tid << ", n: " << n << std::endl;

    return 0;
}

线程只要分离, 主线程就管不了它了, 而且我们发现确实不能回收该分离的线程了, 返回错误码22

其它线程也可以分离它, 只要有tid即可.

总结: 如果线程需要给主线程返回结果, 主线程用pthread_join回收; 主线程如果不关心线程的结果, 那么就可以把这个线程设为分离状态.


关于pthread进一步讨论

如何理解pthread_t?

在正式讨论之前, 首先之前说过, 我们之前用到的关于线程控制的接口, 都不是系统直接提供的接口, 而是原生线程库pthread提供的.

Linux系统不存在真的线程, 而是用轻量级进程模拟线程, OS它不会提供创建线程的接口, 顶多会提供创建轻量级进程的接口, 但是我们平时只谈线程, 所以在系统之上有一层软件层(pthread库)提供线程接口. 所以Linux中的线程称为用户级线程 , 内核中只会创建轻量级进程. 轻量级进程由内核进行管理, 那线程由谁管理呢? ----pthread库

所以管理就涉及先描述再组织, 就要涉及类似struct tcb的概念, 对内核 中的LWP之类的属性作封装, 所以tcb是在pthread库内管理, 而不是内核中. 因为pthread是动态库, 属于用户空间.

既然程序运行期间要使用pthread库里的函数, 就要把pthread库动态加载进内存中, 并通过页表映射到地址空间中的共享区.

此时进程内部所有线程 都可以看到动态库中的数据, 而我们所说的每个线程都有自己私有的栈, 其中除了主线程 采用的栈是进程地址空间原生的栈 , 其余的线程采用的 就是在堆中开辟的, 在共享区中用指针和size等属性维护这些堆空间. 当然, 共享区中也有每个线程自己的tcb, 还要有自己的线程局部存储(TLS), 下个话题再谈. (TLS和线程上下文不是一个东西, 线程上下文信息保存在PCB里属于内核空间不需要用户关心, 而TLS在共享区是属于用户空间的)

此外, 线程库是动态库, 动态库是共享的, 整个系统只有一份, 所以动态库的内部要管理整个系统的由多个用户创建的所有线程.

现在再来谈什么是pthread_t tid.

库为了能够快速的找到每一个线程, 所以提供了一个pthread_t tid, 代表线程属性集合在库中的起始地址. 因此我们想要找到一个用户级线程, 只需要知道线程的 tid, 之后就可以从该结构体中获取线程的各种信息(比如返回值).


线程局部存储

正如预期, 主线程和新线程能看到同样的全局变量:

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

int g_val = 100; //全局变量本身就是被所有线程所共享的

void* threadRoutine(void* argc)
{
    std::string name = static_cast<const char*>(argc);
    while(true)
    {
        std::cout << name << ", g_val: " << g_val << std::endl;  
        g_val++;
        sleep(1);
    }    
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
    while(true)
    {
        std::cout <<  "main thread, " << "g_val: " << g_val << std::endl; 
        sleep(1);
    }

    pthread_join(tid, nullptr);
    return 0;
}

现在在g_val的前面加上__thread修饰:

再次编译运行发现只有新线程的g_val在发生变化, 实际上__thread是一个编译选项, 告诉编译器被__thread修饰的变量在每一个线程内保留一份自己的线程局部存储.


程序语言角度理解pthread

C++11内部的多线程的本质 是对**原生线程库的封装.**具体在其它文章展开.


fork和exec*

线程中可以fork吗? 线程中可以exec*程序替换吗? 如何理解?

  1. 可以, 子进程将会成为调用 fork 的线程的副本.

  2. 可以, 在一个多线程程序中调用exec*函数时, 它会替换当前进程为新的程序. 当前进程的所有线程都将被终止, 只有调用exec*的线程会继续执行(但实际上是作为新程序的入口点)。


相关推荐
冷眼看人间恩怨26 分钟前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
红龙创客35 分钟前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin37 分钟前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
yuanbenshidiaos2 小时前
c++---------数据类型
java·jvm·c++
十年一梦实验室3 小时前
【C++】sophus : sim_details.hpp 实现了矩阵函数 W、其导数,以及其逆 (十七)
开发语言·c++·线性代数·矩阵
taoyong0013 小时前
代码随想录算法训练营第十一天-239.滑动窗口最大值
c++·算法
这是我583 小时前
C++打小怪游戏
c++·其他·游戏·visual studio·小怪·大型·怪物
fpcc3 小时前
跟我学c++中级篇——C++中的缓存利用
c++·缓存
呆萌很3 小时前
C++ 集合 list 使用
c++
诚丞成4 小时前
计算世界之安生:C++继承的文水和智慧(上)
开发语言·c++