Linux-线程2

上一篇我们写到了:线程在共享区的内核结构!

同时,我们通过ps -aL知道了有线程id:LWP

所以,我们可以认为:

Linux线程=用户级线程+内核的LWP

用户级执行流:内核级LWP=1:1

同时,每一个线程都会有自己的独立的栈结构!(看上图)

其实,线程与线程之间,几乎没有密码,即线程的栈上的数据,也是可以被其他线程看到并访问的。

现在我们通过一段代码来证明:

复制代码
int g_val=0;
void*handler(void*args)
{
    while(true)
    {
        // g_val++;
        std::cout<<"mypthread g_val: "<<g_val++<<",&g_val:"<<&g_val<<std::endl;
        sleep(1);
    }
}
int main()
{
    pthread_t pid;
    pthread_create(&pid,nullptr,handler,0);
    while(true)
    {
        // g_val++;
        std::cout<<"main thread g_val: "<<g_val++<<",&g_val:"<<&g_val<<std::endl;
        sleep(1);

    }
    pthread_join(pid,0);

    return 0;
}

我们 可以看到,全局变量是被所有的线程同时看到并访问的!

如果我线程想要一个私有的全局变量呢?该怎么办?

加__thread编译选项,

但是,线程的局部存储,只能定义内置类型,不能用来修饰自定义类型!

接下来,我们利用

多线程来进行简单的抢票机制:

复制代码
#define NUM 5
int ticket=1000;
class ThreadData
{
public:
    ThreadData(int num)
    {
        _threadname="thread-"+std::to_string(num);
    }

// private:
public:
    std::string _threadname;
};

void*mythread(void*args)
{
    ThreadData*td=static_cast<ThreadData*>(args);
    const char*name=td->_threadname.c_str();
    while(true)
    {
        if(ticket>0)
        {
            usleep(1000);
            printf("%s,get a ticket:%d\n",name,ticket);
            ticket--;
        }
        else
            break;
    }
    return nullptr;
}

int main()
{
    std::vector<pthread_t> pids;
    std::vector<ThreadData*>thread_datas;
    for(int i=0;i<NUM;i++)
    {
        pthread_t pid;
        ThreadData*td=new ThreadData(i);
        thread_datas.push_back(td);
        pthread_create(&pid,nullptr,mythread,thread_datas[i]);
        pids.push_back(pid);
    }
    for(int i=0;i<NUM;i++)
    {
        pthread_join(pids[i],nullptr);
    }
    for(int i=0;i<NUM;i++)
    {
        delete thread_datas[i];
    }
    return 0;
}

上面部分能不能改成?

复制代码
ThreadData td;
td.threadname=xxxx;

不可以,因为这个变量属于当前for循环的代码块,更是main函数的内部,所以,这变量属于当前主线程的栈中,自己创建线程进来后,for循环执行完毕,td会全部释放掉,你给别人传递进来的是一个指针,不可让当前线程指向主线程的栈,而且还是临时变量!

而我们这里采用了new的方式,td会将拷贝值给args,其中变量指向的内容其实是堆空间,而且每个for循环都会重新创建一个堆空间,所以,每个线程都可访问堆空间,但它们访问的是堆空间中不同区域,为了虽然td指针变量会被释放,但线程内部相当于自己有了一个我自己独占的一小块堆空间(存有我的信息)

当我们写出上面的代码的时候,一运行,会发现出现数据不一致问题?为什么会出现这种情况呢?我们不是已经设定了if条件了吗?

接着,我们继续从CPU怎么到内存中读取数据的过来介绍一下!

ps:寄存器≠寄存器的内容

线程在执行的时候,将共享数据,加载到CPU寄存器的本质:

把数据的内容,变成了自己的上下文,以拷贝的方式,给自己单独拿一份!

有了堆数据加载到CPU寄存器的本质后,我们就可以再回头看看为什么我们写的有出现数据不一致问题也显然了!

那么,我们怎么解决?

对共享数据的任何访问,保证任何时候只有一个执行流访问!(互斥)(ps:我们前几篇有讲到)

https://blog.csdn.net/go_bai/article/details/154755520?spm=1001.2014.3001.5501

补充线程接口:

上一篇我们已经介绍了pthread_create、pthread_join这两个接口了,现在,我们正式了解常见接口:

创建线程:

复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
复制代码
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通
过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。
对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小

获取线程自身ID

复制代码
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。
该线程ID和前面说的线程ID不是一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,
是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,
该内存单元的地址即为新创建线程的线程ID,
属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:

概念:

  1. TID:线程ID,是线程库(如NPTL)维护的地址空间虚拟地址,用于库层面标识线程。

  2. LWP:轻量级进程,是操作系统(OS)直接管理的底层执行流,用户层的"线程控制块"本质对应LWP,是线程与OS交互的核心载体。

  3. NPTL(原生线程库):Linux下统一管理所有用户创建线程的核心库,所有线程的创建、调度、销毁均由其统筹,负责衔接用户线程与OS的LWP。

二、执行流与栈结构本质

  1. 执行流本质:每个线程的执行流,本质是一条函数调用链(即代码执行的先后调用关系)。

  2. 栈的核心作用:在应用层为函数调用链提供临时变量的空间开辟,支撑调用链的正常执行。

  3. 栈帧的工作机制:

  • 函数调用时,会先在栈中为当前函数创建栈帧结构(存储函数参数、局部变量、返回地址等);

  • 若当前函数再调用其他函数,会在已创建栈帧的"上方"(栈地址增长方向)继续创建新栈帧;

  • 宏观上,栈是被"整体使用"的连续空间,其实际占用由调用链中依次生成的栈帧结构构成,变量定义本质是在对应函数的栈帧上开辟空间。

在上篇,我们也了解到了

线程终止

只终止某个线程,而不终止整个进程,有三个方法:

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

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

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

复制代码
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的
或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时
线程函数已经退出了
复制代码
thread:线程ID
返回值:成功返回0;失败返回错误码

线程等待:

为什么要线程等待?

1.防止新线程造成内存泄漏!(已经退出的线程,其空间没有被释放,仍然在进程的地址空间内)

2.若需要,可获取子进程退出结果!(创建新的线程不会复用刚才退出线程的地址空间。)

主线程等待的时候,默认是阻塞等待的!

第二的参数:对指针的解引用代表指针所指向的目标!

线程不同的退出方式,得到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参数

现在,我们来用代码验证一下:

复制代码
void *mythread1(void *args)
{
    int *b = (int *)malloc(1024);
    *b = 2;
    pthread_exit((void *)b);
}

void *mythread2(void *args)
{
    int *a = (int *)malloc(1024);
    *a = 10;
    return (void *)a;
}
void *mythread3(void *args)
{
    while (true)
    {
        std::cout << "hello" << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    void *ret;
    pthread_create(&tid, nullptr, mythread1, 0);
    pthread_join(tid, &ret);
    printf("mythread1 return, thread id %X, return code:%d\n", tid, *(int *)ret);
    free(ret);

    pthread_create(&tid, nullptr, mythread2, 0);
    pthread_join(tid, &ret);
    printf("mythread2 return, thread id %X, return code:%d\n", tid, *(int *)ret);
    free(ret);

    pthread_create(&tid, nullptr, mythread3, 0);
    sleep(4);
    pthread_cancel(tid);
    pthread_join(tid, &ret);

    if (ret == PTHREAD_CANCELED)
        printf("mythread3 return, thread id %X, return code:PTHREAD_CANCELED\n", tid);
    else
        printf("mythread3 return, thread id %X, return code:NULL\n", tid);

    return 0;
}

分离线程

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

void*mythread(void*args)
{
    pthread_detach(pthread_self());
    int i=0;
    while(i<5)
    {
        std::cout<<"hello"<<std::endl;
        sleep(1);
        i++;
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,mythread,0);
    if(pthread_join(tid,nullptr)==0)
    {
        std::cout<<"pthread_join success";
    }
    else
    {
        std::cout<<"pthread_join fail";
    }
    return 0;
}

我们可以看到,退出时,没有调用pthread_join();

好了,关于本次的分享就到此结束了,希望大家一起进步!

最后,到了本次鸡汤环节:

路虽远行则可至,事虽难做则可成!

相关推荐
shizhan_cloud1 小时前
DNS 服务器
linux·运维
q***13342 小时前
Linux系统离线部署MySQL详细教程(带每步骤图文教程)
linux·mysql·adb
j_xxx404_2 小时前
C++:继承(概念及定义|作用域|基类与派生类转换|默认成员函数|与友元、静态成员关系|多继承|组合)
数据结构·c++
闲聊MoonL2 小时前
【AMBA】Caches协议分析
笔记
小雪_Snow2 小时前
Ubuntu 安装教程
linux·ubuntu
欧阳x天3 小时前
C++入门(二)
开发语言·c++
IT逆夜3 小时前
linux系统安全及应用
linux·运维
Forest_HAHA4 小时前
<10>_Linux网络基础(上)
linux·服务器
('-')4 小时前
《从根上理解MySQL是怎样运行的》第四章学习笔记
笔记·学习·mysql