Linux线程控制

目录

一、线程的简单控制

1.多线程并行

2.线程结束

3.线程等待

(1)系统调用

(2)返回值

4.线程取消

5.线程分离

二、C++多线程小组件

三、线程库TCB

1.tid

2.局部储存


一、线程的简单控制

1.多线程并行

我们之前学过pthread_create可以创建线程,而且要使用原生线程库必须在编译时加上-lpthread。

我们用这个接口一次性创建五个进程。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;

#define NUM 5

void* start_routine(void* args)
{
    string s;
    s += "running->";
    s += (char*)args;
    while(1)
    {
        cout << "running->" << s;
        sleep(1);
    }
}

int main()
{
    for(int i = 0; i<NUM; ++i)
    {
        pthread_t tid;
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "new thread:%d\n",i+1);
        pthread_create(&tid, nullptr, start_routine, (void*)buffer);
    }

    while(1)
    {
        cout << "main pthread running.\n";
        sleep(1);
    }
    return 0;
}

我们运行三次程序:

我们可能会想,按照代码的逻辑,我们想要看到的结果是:

running->new thread:1

running->new thread:2

running->new thread:3

running->new thread:4

running->new thread:5

main pthread running.

可是为什么运行两次,main pthread running.打印的位置不断变化,而且后面的数字都是5。

首先解决第一个问题,main pthread running.打印的位置为什么不断变化?

这是因为线程的运行顺序是由调度器决定的,各个线程的执行进度不同,主线程不一定在最后才能打印main pthread running.

第二个问题,为什么后面的数字都是5?

这是因为,三次执行中主线程执行顺序靠前。主线程将5次创建线程的代码跑完了,每循环一次sprintf就会将上一次的buffer内容覆盖掉,循环5次,i+1变为5。此时,又因为包括主线程的6个线程共享同一个地址空间,所以它们同时能看到buffer。五个新线程通过参数传递的地址找到buffer并打印出来。

既然你说buffer作为缓冲区被覆盖掉了,那我们为每一个线程构建一个自己的缓冲区不就解决问题了?

这样的思想是没有问题的,我们可以使用一个pthread_data类管理线程,内部包含线程标识符tid和缓冲区buffer。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;

#define NUM 5

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    while(1)
    {
        cout << s;
        sleep(1);
    }
}

int main()
{
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new  pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
    }

    while(1)
    {
        cout << "main pthread running.\n";
        sleep(1);
    }
    return 0;
}

虽然确实1到5都出现了,但是因为我们控制不了线程运行的顺序,所以还是不能保证按数字顺序打印。

2.线程结束

线程执行的函数有一个void*返回值,我们返回空指针就能终止该线程。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;

#define NUM 5

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    int j = 1;
    while(1)
    {
        cout << s;
        sleep(1);
        if(j++ == 5)
            return nullptr;
    }
}

int main()
{
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new  pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)(pd->buffer));
    }

    while(1)
    {
        cout << "main pthread running.\n";
        sleep(1);
    }
    return 0;
}

五个线程都能正常退出,最后只剩下主线程运行。

POSIX线程库也提供了一个接口用于结束线程

void pthread_exit(void* retval);

头文件:pthread.h

功能:终止当前线程。

参数:void* retval是线程的结束信息,设置为空指针就可以了。

cpp 复制代码
void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    int j = 1;
    while(1)
    {
        cout << s;
        sleep(1);
        if(j++ == 5)
            pthread_exit(nullptr);
    }
}

将return nullptr替换成该函数也能实现线程退出。

3.线程等待

(1)系统调用

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

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

头文件:pthread.h

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

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

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

实际上,这个加入等待队列和我们之前的进程等待现象差不多,我们写一段代码,让五个新线程运行3秒终止,主线程负责回收五个新线程,观察一下结果。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;

#define NUM 5

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    int j = 1;
    while(1)
    {
        cout << s;
        sleep(1);
        if(j++ == 3)
            return nullptr;
    }
}

int main()
{
    vector<pthread_data*> vpd;
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new  pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
        vpd.push_back(pd);
    }

    for(auto& e : vpd)
    {
        pthread_join(e->tid, nullptr);
        printf("wait success:%d\n", e->tid);
    }

    return 0;
}

运行结果:

主线程在执行至线程等待代码时,主线程并不会继续往下执行。这也证明了,线程等待是阻塞式等待。

(2)返回值

在之前我们写的start_routine线程函数有一个void*的返回值,它可以返回线程退出相关的信息。

比如说,下图的最后一行就可以以void*的格式返回1。

不过这里有个问题,它虽然能返回结束的信息,但是这个变量要怎么让主线程接收到呢?

pthread_join函数有一个输出型参数void** retval,我们在主线程内定义一个void*类型的ret指针变量。当一个线程被回收的时候,将ret传参,它的返回值就会被放进这个ret里。

又因为返回值类型为void*,如果只将ret传参,那么只会将ret的临时拷贝改变。所以参数必须为void**,这也是使用二级指针的原因。

我让每一个线程都返回1,运行代码。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;

#define NUM 5

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    int j = 1;
    while(1)
    {
        cout << s;
        sleep(1);
        if(j++ == 3)
            break;
    }
    return (void*)1;
}

int main()
{
    vector<pthread_data*> vpd;
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new  pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
        vpd.push_back(pd);
    }

    for(auto& e : vpd)
    {
        void* ret = nullptr;
        pthread_join(e->tid, &ret);
        printf("wait success:%d,exit code:%d\n", e->tid, (long long)ret);
    }

    return 0;
}

运行结果:

把return换成只有一个参数void* retval的pthread_exit,它也可以将结果输出到变量中。

结果与上面的一致。

如果想要让每一个线程都返回各自的错误码,我们可以在pthread_data类中增加一个储存返回值的变量。

我下面就修改代码让每一个线程返回自己的编号。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;

#define NUM 5

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
    int num;
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    string s;
    s += "running->";
    s += p->buffer;
    int j = 1;
    while(1)
    {
        cout << s;
        sleep(1);
        if(j++ == 3)
            break;
    }
    //return (void*)1;
    pthread_exit((void*)(p->num));
}

int main()
{
    vector<pthread_data*> vpd;
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pd->num = i+1;
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
        vpd.push_back(pd);
    }

    for(auto& e : vpd)
    {
        void* ret = nullptr;
        pthread_join(e->tid, &ret);
        printf("wait success:%d,exit code:%d\n", e->tid, (long long)ret);
    }

    return 0;
}

运行结果:

4.线程取消

线程取消也是终止线程的一种方式,可使用下面的系统调用。

int pthread_cancel(pthread_t thread);

头文件:pthread.h

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

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

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

我们创建10个线程,在中途取消前五个线程,查看具体的现象。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;

#define NUM 10

class pthread_data
{
public:
    pthread_t tid;
    char buffer[64];
    int num;
};

void* start_routine(void* args)
{
    pthread_data* p = (pthread_data*)args;
    int j = 1;
    while(1)
    {
        sleep(1);
        if(j++ == 3)
            break;
    }
    //return (void*)1;
    pthread_exit((void*)(p->num));
}

int main()
{
    vector<pthread_data*> vpd;
    for(int i = 0; i<NUM; ++i)
    {
        pthread_data* pd = new pthread_data;
        snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);
        pd->num = i+1;
        pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);
        vpd.push_back(pd);
    }
    
    for(int i = 0; i<NUM; ++i)
    {
        printf("new thread:%d,address:%p\n", i+1, vpd[i]);
    }

    for(int i = 0; i<vpd.size()/2; ++i)
    {
        pthread_cancel(vpd[i]->tid);
    }

    for(auto& e : vpd)
    {
        void* ret = nullptr;
        pthread_join(e->tid, &ret);
        printf("join success:%d,exit code:%d\n", e->tid, (long long)ret);
    }

    return 0;
}

运行结果:

对于取消的前五个线程,等待会直接成功,返回值是-1。

未被取消的后五个线程,仍然阻塞等待,等待成功后返回的是设定的线程编号。

如果一个线程是被取消结束的,退出码就是-1,是一个宏PTHREAD_CANCELED。

5.线程分离

默认情况下,在线程退出后,需要使用pthread_join将它加入等待队列,否则就会造成内存泄漏。

但是主线程只能阻塞式线程,阻塞时主线程只能干等着。而且我们有时根本不关心线程的返回值,那阻塞式等待就是巨大的负担。

那么,能不能模仿之前的轮询检测让主线程也继续干活吗?

可以,我们可以将需要释放的线程变为分离状态。我们将一个进程的所有线程做成一个组,如果将一个进程移出这个组,我们就说该线程处于分离状态。此时,主线程不用再关心该线程的状态,它会由系统自动释放。

我在这里也在说明一下,只要是让线程加入等待队列,那就必须要阻塞式等待;只要是分离状态,就不能再加入等待队列。所以说,可加入阻塞队列和分离状态是相互矛盾的,这也解释了为什么不能轮询非阻塞等待。

int pthread_detach(pthread_t thread);

头文件:pthread.h

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

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

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

这个函数既可以分离线程组内的其他线程,也可以分离自己,但分离自己就需要用到自己的线程标识符tid,线程自己的tid可以由下面的函数获取。

pthread_t pthread_self(void);

头文件:pthread.h

功能:获取线程自己的tid。

返回值:返回自己的tid。

我们创建一个新线程,让新进程在第一步就分离执行,观察主线程能否回收它。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
#include<string.h>
using namespace std;

#define NUM 10

void* start_routine(void* args)
{
    pthread_detach(pthread_self());
    string s = static_cast<const char*>(args);
    for(int i = 0; i<5; ++i)
    {
        cout << s;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void*)"new thread running\n");

    cout<<"main thread tid: 0x"<<(void*)pthread_self()<<endl;
    int n = pthread_join(tid, nullptr);
    cout << "error: " << n << ":" << strerror(n) << endl;

    return 0;
}

线程只要分离,主线程就管不了它了,而且我们发现确实不能回收该分离的线程了,与预期效果一致。

我们把start_routine的前两个语句调换位置。让线程先去构造string对象,然后将其分离。

此时运行观察,我们发现主线程把新线程回收了。这又是怎么回事呢?

这是因为主线程优先运行,在新线程开始运行时,主线程已经在阻塞等待新线程了,这样设置分离也就没有意义了。

既然在新线程中分离线程并不保险,我们就将分离操作全部放在主线程中。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
#include<string.h>
using namespace std;

#define NUM 10

void* start_routine(void* args)
{
    string s = static_cast<const char*>(args);  
    for(int i = 0; i<5; ++i)
    {
        cout << s;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void*)"new thread running\n");
    //在主线程分离新线程
    pthread_detach(tid);
    cout<<"main thread tid: 0x"<<(void*)pthread_self()<<endl;
    int n = pthread_join(tid, nullptr);
    cout << "error: " << n << ":" << strerror(n) << endl;

    return 0;
}

最终结果就不会出错了。

二、C++多线程小组件

既然我们也学习了一些系统调用了,我们可以试着写一个小组件。这个小组件将线程的创建、执行和等待等都封装起来。我们在程序中指定一个函数,让多个线程不断地执行该函数。

thread_handler.h

cpp 复制代码
#include<pthread.h>
#include<assert.h>
#define NUM 64
class Thread;//前置声明

class Context
{
public:
    Context()
        :_this(nullptr)
        ,_args(nullptr)
    {}
    Thread* _this;//线程this指针
    void* _args;//pthread_create需要传递的args
};

class Thread
{
    typedef std::function<void*(void*)> func_t;
public:
    //构造函数创建线程
    Thread(func_t func, void* args, int number = 0)
        :_func(func)
        ,_args(args)
    {
        //对线程进行规范化命名
        char buffer[NUM];
        snprintf(buffer, sizeof(buffer), "thread-%d", number);
        _name = buffer;
        //将线程信息保存到Context变量
        Context* p = new Context;
        p->_this = this;
        p->_args = args;
        int n = pthread_create(&_tid, nullptr, start_routine, (void*)p);
    }

    //运行函数
    void* run(void* args)
    {
        return _func(args);
    }

    //线程执行start_routine,但是它如果是普通成员函数,则参数多了一个this指针
    //将其变为static成员函数就能消除this指针
    //但同时我们又需要执行线程内部成员_func函数,此时既不能访问this指针的内容,之前创建线程传递的void* args也传递不到
    //我们再次构建一个类Context,将这些内容包含进去,将它们通过一个类指针传过去就好了
    static void* start_routine(void* args)
    {
        Context* cp = static_cast<Context*>(args);
        void* ret = cp->_this->run(cp->_args);
    }

    //将加入等待队列
    void join()
    {
        int n = pthread_join(this->_tid, nullptr);
        assert(n == 0);
    }

private:
    std::string _name;
    pthread_t _tid;
    func_t _func; 
    void* _args;
};

test.cc

cpp 复制代码
#include<iostream>
#include<memory>
#include<unistd.h>
#include"thread_handler.h"
using namespace std;

void* handler(void* args)
{
    string s = "new thread:";
    s += static_cast<const char*>(args);
    s += '\n';
    while(1)
    {
        cout << s;
        sleep(1);
    }
}

int main()
{
    unique_ptr<Thread> t1(new Thread(handler, (void*)"thread1", 1));
    unique_ptr<Thread> t2(new Thread(handler, (void*)"thread2", 2));
    unique_ptr<Thread> t3(new Thread(handler, (void*)"thread3", 3));

    t1->join();
    t2->join();
    t3->join();

    return 0;
}

可以看到三个线程在不停地执行我们的函数。

三、线程库TCB

1.tid

我们编写代码让新线程打印自己的 tid,主线程打印自己和新线程的tid。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;

void* start_routine(void* args)
{
    string s = (char*)args;
    while(1)
    {
        cout << s << " tid:0x" << pthread_self() << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, start_routine, (void*)"new pthread");
    assert(n == 0);
    while(1)
    {
        printf("main thread tid:0x%p,new thread tid:0x%p\n", pthread_self(), tid);
        sleep(1);
    }
    return 0;
}

我们发现主线程和新线程打印的新线程tid相同,而且它们是一个地址。

我们一开始就强调,Linux内核中没有线程概念,只有轻量级进程的PCB,更没有TCB这样的数据结构。我们宏观看到的线程是系统的通过clone创造出的轻量级进程,只是这些轻量级进程共用了地址空间等资源。然后这些轻量级进程再通过POSIX原生线程库模拟出我们看到的线程。

既然原生线程库可以保证大量的线程同时工作,那么原生线程库中就必定有能管理这些线程的数据结构,换句话说,TCB结构一定在线程库中。

由于存储线程属性的TCB不在内核中,所以Linux的TCB也叫做用户级线程。

结论:Linux内核只负责调度执行流,用户关心的线程及其属性都由原生线程库维护。Linux的用户级线程和内核轻量级进程都保存着线程的属性,二者基本做到一一对应。

我们将视线转向地址空间:

原生线程库加载到内存后,页表会对虚拟地址空间和其内存物理地址建立映射关系。根据地址空间的分区,线程库会映射在虚拟地址空间中的共享区中,其中TCB等结构和数据当然也在共享区。

看下面的图片

我这里严谨一点:

由于局部储存和栈结构空间都只是在TCB存储指针,虽然这些都是TCB拥有的数据,但它们的内容肯定不会直接保存在TCB里,所以我就把它们和TCB分开了。

你要是认为全部的数据就是TCB也可以接受。

我们能得到以下结论:

  • 共享区内的每个线程都有自己的TCB、局部储存和独立栈,它们由载入内存的原生线程库维护。
  • 主线程的栈就是地址空间的栈区,而新线程的独立栈结构都在共享区,所以线程间才能有独立栈结构。
  • tid是指针类型,TCB在地址空间中的虚拟地址就是tid的值。

2.局部储存

我们知道同一进程的线程相互共用地址空间和页表,所以它们都可以使用一个全局变量。

可如果新线程仍然想用这个变量名,但又不想影响其他线程。这时需要让这个全局变量在每个进程中独立使用,此时就可以使用线程的局部存储属性了。

不使用局部储存

主线程和新线程都用了同一个g_val变量。

在int g_val = 0;前面加上__thread就能将该变量独立出去。

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;

__thread int g_val = 0;

void* start_routine(void* args)
{
    string s = (char*)args;
    while(1)
    {
        ++g_val;
        printf("new thread g_val:%d,address:%p\n", g_val, &g_val);
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, start_routine, (void*)"new pthread");
    assert(n == 0);
    while(1)
    {
        ++g_val;
        printf("main thread g_val:%d,address:%p\n", g_val, &g_val);
        sleep(1);
    }
    return 0;
}

主线程和新线程使用的不再是同一个g_val变量,互相之间也不会受到影响。

结论:在全局变量或static变量前添加 __thread,可以让每个线程的TCB中都有一份独立的变量,不会互相影响。

相关推荐
似霰2 分钟前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
芊寻(嵌入式)13 分钟前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
獨枭14 分钟前
C++ 项目中使用 .dll 和 .def 文件的操作指南
c++
霁月风17 分钟前
设计模式——观察者模式
c++·观察者模式·设计模式
橘色的喵18 分钟前
C++编程:避免因编译优化引发的多线程死锁问题
c++·多线程·memory·死锁·内存屏障·内存栅栏·memory barrier
荒Huang18 分钟前
Linux挖矿病毒(kswapd0进程使cpu爆满)
linux·运维·服务器
泉崎22 分钟前
11.7比赛总结
数据结构·算法
你好helloworld23 分钟前
滑动窗口最大值
数据结构·算法·leetcode
何曾参静谧1 小时前
「C/C++」C/C++ 之 变量作用域详解
c语言·开发语言·c++
AI街潜水的八角1 小时前
基于C++的决策树C4.5机器学习算法(不调包)
c++·算法·决策树·机器学习