Linux——多线程

news2024/11/27 3:59:26

Linux多线程

  • 多线程
    • 进程内进行资源划分
    • 什么是线程
    • 进一步理解线程
    • 线程的优缺点
    • Linux进程VS线程
    • 线程的异常
  • 创建线程两个的接口
  • 线程的控制
    • 线程的创建
    • 线程的终止
    • 线程的等待
    • 线程取消
    • C++的线程库
    • 线程的分离
    • 如何理解每个线程都有自己独立的栈结构
  • 封装线程接口

多线程

进程内进行资源划分

之前说过页表有用户级页表和内核级页表,现在再来扩展一下。
在这里插入图片描述
这里也能解释为什么对于常量字符串类型为什么不能修改了,因为要修改的时候会从虚拟地址转化成物理地址,然后检查权限是否可以修改等等。

如何看待地址空间和页表呢?
1.比如进程看到的堆区,栈区等等各种的资源窗口。
2.页表决定进程真正拥有资源的情况。(因为虚拟地址空间根本无法决定实际情况)
3.合理的对地址空间+页表进行资源划分,我们就可以对一个进程所有资源进行分类。(比如虚拟地址空间中的堆区,栈区,内核区,通过页表映射到不同的物理地址内存)

从虚拟地址到物理地址是怎么样通过页表访问的呢?
在这里插入图片描述
如果是32位地址的OS,就有232个地址,那么页表是不是就要有232个条目呢?一个条目算上物理地址,是否命中,读写权限等等就需要很多的内存,总共算下来需要的内存是非常庞大的,这样显然是不可能的。
其实物理内存早就被划分好了区域,每一块叫做数据页。(也叫做页框)
在这里插入图片描述
每一个页框也被管理:

struct Page
{
	//内存属性——4KB
}

然后通过struct Page类型的数组来管理。
并且磁盘中是数据也是被划分成4KB大小一块。
然后再说说虚拟地址空间的地址:32个比特位都有相对应的含义。
在这里插入图片描述
首先前十个对应的是页目录记录页表的地址,后面的10个是页表记录物理内存的地址,最后面的12个是物理内存起始地址中的偏移量。
在这里插入图片描述
页表当中储存的就是页框的起始地址,最后通过偏移量来确定在物理内存中的实际大小,212也正好就是4KB。
也就是说其实在查找的时候OS其实只会创建一个页目录和一个页表,其他暂时不用的页表就先不用,也就是说不需要多少内存。

什么是线程

之前对于进程的概念是内核数据结构+进程对应的代码和数据。
在这里插入图片描述
之前创建一个子进程室友自己的独立性的,如果今天创建多个进程,和第一个进程指向同一个PCB,看到是同一块虚拟地址空间,然后让每个这种“进程”执行虚拟地址空间中的部分代码,这些“进程”就叫做线程。
在Linux下,创建的线程其实就是PCB而已。
因为通过进行资源划分,单个“进程”的执行粒度一定比之前的进程更细。
在这里插入图片描述

CPU不会看是不是线程,只会去处理每个线程或者是进程。
OS也需要去管理这些线程。
专门管理线程的叫做TCB。
在windows操作系统就是这么设计的,CPU会先找到某个进程,然后进入这个进程中再去找线程。
(这样的设计是很复杂的,也不好维护)
从被执行的角度来看,进程和线程的区别并不是很大。

这就是为什么Linux中的线程只是复用PCB,用PCB来表示“线程”。

线程其实就是进程的一个执行流:

线程在进程的内部运行,线程在进程的地址空间内运行,拥有该进程的一部分资源。

也就是说:
在这里插入图片描述
这里整体才算是一个完整的进程。
内核视角:承担分配系统资源的基本实体。(创建进程所需要的各种资源)
如果按照概念来说,相对比之前说的进程只有一个执行流,现在的进程是拥有多个执行流。

在Linux中,什么是线程呢?是CPU调度的基本单位。
在Linux中,一个线程被称为轻量级进程。

总结:

1.Linux内核中没有真正意义上的线程,是用PCB来模拟线程的,是一种完全属于自己的一套线程方案。
2.站在CPU的角度,每一个PCB都可以被叫做轻量级进程
3.Linux线程是CPU调度的基本单位,而进程是承担分配系统资源的基本单位。
4.进程是整体申请资源,线程是向进程申请资源。

Linux线程的优点是什么呢?
比Windows操作系统的线程简单,维护成本低,可靠,高效。

线程的具体作用呢?
就像迅雷的边播放边下载。

Linux无法直接提供创建线程的系统调用,只能提供创建轻量级进程的接口。

进一步理解线程

先来用一份代码来看看线程:
pthread_create函数介绍
在这里插入图片描述
第一个参数是线程id,第二个参数是线程属性(大部分情况设置为nullptr),第三个参数是回调函数,让该线程执行这个函数。第四个参数是第三个参数的回调函数的参数。
成功返回0,失败返回错误码。
并且这个函数是第三方库的内容:pthread。
这是因为Linux没有真正意义上的线程。

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>
using namespace std;

void *pthread_routine(void* args)
{
    while(true)
    {
        cout << "我是新线程" << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, pthread_routine, (void *)"111111");//参数记得强制转换成void*
    assert(n == 0);
    (void)n;
    while(true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
任何Linux操作系统都必须默认携带这个库,这个库叫做原生线程库。
在这里插入图片描述
运行的时候发现只有一个进程。
在这里插入图片描述
终止之后就没有了。

然后用ps -al查看轻量级进程。
在这里插入图片描述
这里有两个执行流,PID相同,说明属于同一个进程,旁边的LWP不同,这个就是轻量级进程的id。
两个id相同的是主线程,不同的是新线程。
这里也说明CPU进行调度的时候是以LWP为标准特定执行流的。

并且,主线程还可以给新线程发送内容:

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>
using namespace std;

void *pthread_routine(void* args)
{
    const char* p = (const char*)args;
    while(true)
    {
        cout << "我是新线程" << p << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, pthread_routine, (void *)"111111");//参数记得强制转换成void*
    assert(n == 0);
    (void)n;
    while(true)
    {
        cout << "我是主线程,新线程id是:" << tid << endl;//这里顺便打印新线程的id
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
新线程确实可以接收到主线程发送的信息,但是新线程的id却非常怪,这里可以用16进制的方式打印出来看看。
在这里插入图片描述
最后发现这是一个地址,那么是什么地址呢?
这个后面再说。

这里补充一点,线程一旦创建,几乎所有的资源都是被所有线程共享的。

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>
using namespace std;

int i = 0;

void *pthread_routine(void* args)
{
    const char* p = (const char*)args;
    while(true)
    {
        cout << "我是新线程"<<  "i:"<< i++ << "&i:" << &i << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, pthread_routine, (void *)"111111");//参数记得强制转换成void*
    assert(n == 0);
    (void)n;
    while(true)
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer),"0x%x", tid);
        cout << "我是主线程" << "i:"<< i << "&i:" << &i << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

虽然线程共享了大部分资源,但是线程也一定会有私有属性,都有什么呢?

1.线程只要会被CPU调度,那么PCB属性就是私有的。
2.上下文一定是私有的,不然怎么独立调度呢?
3.拥有独立的栈结构。(用来保存自己的数据)
2和3是证明线程动态运行的证据。

与进程之间切换相比,线程需要操作系统左的工作会少很多,为什么呢?
1.线程不需要切换页表和虚拟地址空间
2.CPU中有一个叫做cache,其实就是高速缓存。这个功能就是保存热点数据(就是保存这条代码附近的代码,因为很有可能会访问到附近的代码)
也就是说如果某个进程先让部分代码放入eache中,然后CPU去这里找,如果未命中才会重新去内存中找。
也就是说如果是进程之间切换,不同的进程数据是不共享的,降低了效率。
但是线程的数据是共享的,eache可以不用切换。

总结:

在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
一切进程至少都有一个执行线程。
线程在进程内部运行,本质是在进程地址空间内运行。
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

线程的优缺点

优点:

创建一个新线程的代价要比创建一个新进程小得多。
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
线程占用的资源要比进程少很多。
能充分利用多处理器的可并行数量。
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
计算密集型应用(CPU,加密,解密,算法等),为了能在多处理器系统上运行,将计算分解到多个线程中实现。
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

缺点:

性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。(并不是线程越多越好,要合适,最好要和CPU的核数相同)
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。

Linux进程VS线程

进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:

线程ID
一组寄存器

errno
信号屏蔽字
调度优先级

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id

在这里插入图片描述

线程的异常

一个线程出异常,会影响到另外线程。

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>
using namespace std;

void *pthread_routine(void* args)
{
    while(true)
    {
        cout << "我是新线程" << endl;
        sleep(1);
        int* p = nullptr;
        *p = 0;
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, pthread_routine, (void *)"111111");//参数记得强制转换成void*
    assert(n == 0);
    (void)n;
    while(true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
一旦出异常的时候,会给这个进程发送信号,发送信号也是发送给所有线程,然后就会终止所有线程。

创建线程两个的接口

之前说过,程序员只需要线程,但是Linux又不直接提供创建线程的接口,只提供第三方库(软件层次)创建轻量级进程的接口,下面来介绍这些接口。

创建轻量级进程或者进程的底层接口:(区别就是创建的时候是否共享地址空间)
在这里插入图片描述
第一个参数是新执行流要执行的代码,第二个参数是栈结构。

在这里插入图片描述
这个接口是和fork差不多,只不过是共享了地址空间。
但是两个接口不是很常用

线程的控制

线程的创建

pthread_create
功能:创建一个新的线程
原型
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变量的开销更小。

首先来创建一组线程来看看:

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <vector>
#include <unistd.h>
using namespace std;

void *pthread_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        cout << "new " << name << endl;
        sleep(1);
    }
}

int main()
{
    vector<pthread_t> tids;
#define NUM 10
    for(int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        char buffer[64];
        snprintf(buffer,sizeof(buffer),"%s:%d","thread",i);
        int n = pthread_create(&tid, nullptr, pthread_routine, (void *)buffer);
        sleep(1);
    }

    while(true)
    {
        cout << "main thread" << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
如果将主线程的创建线程过程中的sleep注释掉会发生什么呢?
在这里插入图片描述
有些编号的线程完全没有出现过,这是为什么呢?
因为创建线程之后哪个线程先运行是不确定的,并且:
在这里插入图片描述
这个函数的最后一个参数传过去的是缓冲区的起始地址。
有时候某些进程先运行:
在这里插入图片描述
那么这种情况如何避免呢?

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <vector>
#include <unistd.h>
using namespace std;

struct ThreadData
{
    pthread_t tid;
    char buffer[64];
};
void *pthread_routine(void* args)//当前函数是被重入状态,如果忽略打印操作就是可重入函数
{
    ThreadData* p = static_cast<ThreadData*>(args);
    while(true)
    {
        cout << "new " << p->buffer << endl;
        sleep(1);
    }
    delete p;
}
int main()
{
#define NUM 10
    for(int i = 0; i < NUM; i++)
    {
        ThreadData *p = new ThreadData;//这里每个创建的起始地址都不相同
        char buffer[64];
        snprintf(p->buffer,sizeof(p->buffer),"%s:%d","thread",i);
        pthread_create(&p->tid, nullptr, pthread_routine, p);
    }
    while(true)
    {
        cout << "main thread" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
那么pthread_routine为什么能可重入呢?
因为每一个线程都有自己独立的栈结构。

线程的终止

之前讲过一个exit的函数,这个函数是让进程终止,而不是线程,如果当前进程中任何一个执行流调用了exit函数,那么当前进程都会退出。
那么如何终止一个线程呢?函数中的return可以,还有一个函数可以:
在这里插入图片描述
参数默认设置为nullptr就可以了。

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <vector>
#include <unistd.h>
#include <cstdlib>
using namespace std;

struct ThreadData
{
    pthread_t tid;
    char buffer[64];
};
void *pthread_routine(void* args)
{
    ThreadData* p = static_cast<ThreadData*>(args);
    while(true)
    {
        cout << "new " << p->buffer << endl;
        sleep(1);
        pthread_exit(nullptr);//线程终止
    }
    delete p;
}

int main()
{
#define NUM 10
    for(int i = 0; i < NUM; i++)
    {
        ThreadData *p = new ThreadData;
        char buffer[64];
        snprintf(p->buffer,sizeof(p->buffer),"%s:%d","thread",i);
        pthread_create(&p->tid, nullptr, pthread_routine, p);
        //sleep(1);
    }

    while(true)
    {
        cout << "main thread" << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

线程的等待

线程也是要被等待的,如果不等待会造成类似于僵尸进程的问题——内存泄漏。
线程被等待的原因:
1.获取新线程退出的信息。
2.回收新线程对应PCB内核资源等,防止内存泄漏。(这里暂时无法查看)

这是等待线程的函数:
在这里插入图片描述
第一个参数是线程的id,第二个参数暂时设置为nullptr。

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <vector>
#include <unistd.h>
#include <cstdlib>
using namespace std;

struct ThreadData
{
    pthread_t tid;
    char buffer[64];
};
void *pthread_routine(void* args)
{
    ThreadData* p = static_cast<ThreadData*>(args);
    while(true)
    {
        cout << "new " << p->buffer << endl;
        sleep(1);
        pthread_exit(nullptr);
    }
}

int main()
{
    vector<ThreadData*> arr;
#define NUM 10
    for(int i = 0; i < NUM; i++)
    {
        ThreadData *p = new ThreadData;//这里每个创建的起始地址都不相同
        char buffer[64];
        snprintf(p->buffer,sizeof(p->buffer),"%s:%d","thread",i);
        pthread_create(&p->tid, nullptr, pthread_routine, p);
        arr.push_back(p);
        //sleep(1);
    }
    for(auto& e : arr)
    {
        cout << "等待线程id:" << e->tid << "成功" << endl;
        int n = pthread_join(e->tid, nullptr);//阻塞等待
        assert(n==0);
        delete e;
    }
    while(true)
    {
        cout << "main thread" << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
那么,该函数的第二个参数是什么呢?其实是线程的返回值。(输出型参数)
在这里插入图片描述
这里返回的是一个void*,输出型参数用一个void**才能传回。

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <vector>
#include <unistd.h>
#include <cstdlib>
using namespace std;

struct ThreadData
{
    int num;//线程的编号
    pthread_t tid;
    char buffer[64];
};
void *pthread_routine(void* args)
{
    ThreadData* p = static_cast<ThreadData*>(args);
    while(true)
    {
        cout << "new " << p->buffer << endl;
        sleep(1);
        pthread_exit(nullptr);
    }
    return (void*)p->num;//这里返回的是一个整数,这里强制转换成void*就相当于将四字节的内容写入了八字节的空间里,void* ret = (void*)p->num;
}

int main()
{
    vector<ThreadData*> arr;
#define NUM 10
    for(int i = 0; i < NUM; i++)
    {
        ThreadData *p = new ThreadData;
        p->num = i+1;
        char buffer[64];
        snprintf(p->buffer,sizeof(p->buffer),"%s:%d","thread",i);
        pthread_create(&p->tid, nullptr, pthread_routine, p);
        arr.push_back(p);
        //sleep(1);
    }
    for(auto& e : arr)
    {
        void* ret = nullptr;//注意,这里是void*
        int n = pthread_join(e->tid, &ret);//这里传过去的是ret的地址,等待传回来的返回值是void*的,就等于直接写入的ret当中
        assert(n==0);
        cout << "等待线程:" << e->buffer << "成功"<< "编号:" << (long long)e->num << endl;//因为是64位系统,需要转换成longlong类型 
        delete e;
    }
    while(true)
    {
        cout << "main thread" << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
同理,线程中再堆区开辟的空间地址也可以拿到。

线程取消

注意:线程取消的前提是线程已经在运行了。
在这里插入图片描述
参数是线程id。

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <vector>
#include <unistd.h>
#include <cstdlib>
using namespace std;

struct ThreadData
{
    int num;//线程的编号
    pthread_t tid;
    char buffer[64];
};
void *pthread_routine(void* args)
{
    ThreadData* p = static_cast<ThreadData*>(args);
    while(true)
    {
        cout << "new " << p->buffer << endl;
        sleep(1);
    }
    return (void*)100;//正常退出就返回100
}

int main()
{
    vector<ThreadData*> arr;
#define NUM 10
    for(int i = 0; i < NUM; i++)
    {
        ThreadData *p = new ThreadData;
        p->num = i+1;
        char buffer[64];
        snprintf(p->buffer,sizeof(p->buffer),"%s:%d","thread",i);
        pthread_create(&p->tid, nullptr, pthread_routine, p);
        arr.push_back(p);
        //sleep(1);
    }
    sleep(3);
    for(auto& e : arr)
    {
        pthread_cancel(e->tid);
        cout << "取消的线程id:" << e->tid << "成功" << endl;
    }
    for(auto& e : arr)
    {
        void* ret = nullptr;
        int n = pthread_join(e->tid, &ret);
        assert(n==0);
        cout << "等待线程:" << e->buffer << "成功"<< "编号:" << (long long)ret << endl;//因为是64位系统,需要转换成longlong类型 
        delete e;
    }
    while(true)
    {
        cout << "main thread" << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
线程一旦被取消,退出码就是-1。

C++的线程库

C++当中有一个创建多线程的函数:
https://legacy.cplusplus.com/reference/thread/thread/?kw=thread
在这里插入图片描述
但是这里要注意:任何语言在Linux中要实现多线程,必定要使用pthread库。
C++11中的多线程,本质就是对pthread库的封装。

线程的分离

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
这种就是线程分离。
这是获得当前调用这个函数id的接口:
在这里插入图片描述

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <vector>
#include <unistd.h>
#include <cstdlib>
using namespace std;

string changid(const pthread_t &pthread_id)
{
    char buffer[128];
    snprintf(buffer,sizeof(buffer),"0x%x",pthread_id);
    return buffer;
}

void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        cout << name << "running" << changid(pthread_self()) << endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void*)"thread");
    cout << "main thread running... new thread id:" << changid(tid) <<endl;
    pthread_join(tid, nullptr);

    return 0;
}

在这里插入图片描述
然后来看线程分离的接口:
在这里插入图片描述
参数是线程id。

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <cstdlib>
using namespace std;

string changid(const pthread_t &pthread_id)
{
    char buffer[128];
    snprintf(buffer,sizeof(buffer),"0x%x",pthread_id);
    return buffer;
}

void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    pthread_detach(pthread_self());//设置自己为分离状态
    int count = 5;
    while(count)
    {
        cout << name << "running" << changid(pthread_self()) << endl;
        count--;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void*)"thread");
    string main_tid = changid(pthread_self());
    cout << "main thread running... new thread id:" << changid(tid) << "main thread id:" << main_tid << endl;
    sleep(2);//这里是为了防止新线程还没有进行分离主线程就已经开始等待了
    //如果线程设置了分离,这里就不能等待了
    int n = pthread_join(tid, nullptr);
    cout << "result" << n << ":" << strerror(n) << endl;//这里进行报错之后继续向下运行代码
    return 0;
}

在这里插入图片描述

如何理解每个线程都有自己独立的栈结构

首先了解一下线程库和轻量级进程的关系:
在这里插入图片描述
我们用户都是通过pthread库来创建线程的。
在原生线程库当中,我们用这些接口创建的线程别人也可以同时使用。(因为是共享库)
并且也需要对这些线程进行管理:
在这里插入图片描述
每个结构体对应一个轻量级的进程。
Linux的方案;用户级线程,这些属性在库中,内核提供线程执行流的调度。
Linux用户级线程:Linux内核轻量级进程 == 1:1

那么线程的id究竟是什么呢?
在这里插入图片描述
在这里插入图片描述
也就是说一旦线程结束,通过返回值就会传给共享区的TCB中。
这也能说明为什么每个线程都有自己的栈结构了。
主线程使用的栈是在主线程栈,其他线程的栈是在共享区。(其实也就是线程库当中)

那么什么是线程的局部存储呢?
之前创建过一个全局变量,证明两个线程都会共享这个变量。
如果在这个变量前面加上:__thread就可以将一个内置类型设置为线程局部存储。
也就是说给每个线程都来一份这个变量,两个线程在使用这个变量的时候互不影响。
如果以后给线程设置私有属性可以加上这个。

封装线程接口

这里就用Linux的线程接口来实现C++中的多线程部分功能。

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <cstdlib>
#include <memory>
using namespace std;

class Thread;//声明
class Context//上下文,相当于一个大号的结构体
{
public:
    Thread *this_;
    void* args_;
public:
    Context():this_(nullptr),args_(nullptr)
    {}
    ~Context()
    {}
};
class Thread
{
    typedef function<void* (void*)> func_t;
public:
    //这里需要加一个静态,因为不加静态就是类成员函数,还有一个隐藏的this指针,也就说明这等于前面有一个缺省参数
    //所以在类内创建线程,想让对应的线程执行方法需要在方法前面加一个static
    static void* start_routine(void* args)
    {
        //但是静态方法不能调用成员方法或者成员变量,这里可以设置一个上下文
        Context* ctx = static_cast<Context*>(args);
        void* ret = ctx->this_->run(ctx->args_);//这里让自身去调用这个方法
        delete ctx;
        return ret;
    }
    void* run(void* args)
    {
        return _func(args);//调用该函数
    }
    Thread(func_t func,void* args,int num):_func(func),_args(args)
    {
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "thread_%d", num);
        _name = buffer;

        Context* ctx = new Context();
        ctx->this_ = this;
        ctx->args_ = _args;//这里是将自身的部分数据传给ctx
        int n = pthread_create(&_tid, nullptr, start_routine, ctx);//这里要通过调用函数来转化,直接传func是不行的,因为类型是C++的类,不是C语言的类
        assert(n==0);
        (void)n;
    }
    void join()
    {
        int n = pthread_join(_tid,nullptr);
        assert(n==0);
        (void)n;
    }
    ~Thread()
    {}
private:
    string _name;//线程名字
    pthread_t _tid;//线程id
    func_t _func;//线程调用的函数
    void* _args;//传给函数的参数
};
#include "Thread.hpp"

void* thread_run(void* args)
{
    string work_type = static_cast<const char*>(args);
    while(true)
    {
        cout << "我是一个新线程:" << work_type <<endl;
        sleep(1);
    }
}
int main()
{
    unique_ptr<Thread> thread1(new Thread(thread_run, (void*)"hellothread",1));
    unique_ptr<Thread> thread2(new Thread(thread_run, (void*)"countthread",2));
    unique_ptr<Thread> thread3(new Thread(thread_run, (void*)"logthread",3));
    thread1->join();
    thread2->join();
    thread3->join();

    return 0;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/620967.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java代码块和属性的赋值顺序

代码块 类的成员之四&#xff1a;代码块(初始化块)&#xff08;重要性较属性、方法、构造器差一些&#xff09; 1.代码块的作用&#xff1a;用来初始化类、对象的信息 2.分类&#xff1a;代码块要是使用修饰符&#xff0c;只能使用static 分类&#xff1a;静态代码块 vs 非静态…

nacos升级到2.0.3(单机模式)

前提&#xff1a;https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明 Spring Cloud AlibabaSpring CloudSpring BootNacos2.2.7.RELEASESpring Cloud Hoxton.SR122.3.12.RELEASE2.0.3 一、pom.xml文件 <parent><groupId>org.springframework.boot&…

网工内推 | 高级网工专场,上市公司,3年经验以上,HCIE证书优先

01 名创优品&#xff08;广州&#xff09;有限责任公司 &#x1f537;招聘岗位&#xff1a;高级网络工程师 &#x1f537;职责描述&#xff1a; 1、负责集团总部有线&#xff06;无线、公有云、仓库的网络规划建设与运维&#xff1b; 2、负责公有云的网络台日常上线部署、规划…

3.3 分析特征内部数据分布与分散状况

3.3 分析特征内部数据分布与分散状况 3.3.1 绘制直方图 bar()3.3.2 绘制饼图 pie()3.3.3 绘制箱线图 boxplot()3.3.4 任务实现1、绘制国民生产总值构成分布直方图2、绘制国民生产总值构成分布饼图3、绘制国民生产总值分散情况箱线图 小结 3.3.1 绘制直方图 bar() 直方图&#x…

Vue源码解析

【尚硅谷】Vue源码解析之虚拟DOM和diff算法 【Vue源码】图解 diff算法 与 虚拟DOM-snabbdom-最小量更新原理解析-手写源码-updateChildren] 文章目录 2. snabbdom 简介 及 准备工作2.1 简介2.2 搭建初始环境1. 安装snabbdom2. 安装webpack5并配置3. 复制官方demo Example 3. …

IJCAI 2023 | 如何从离散时间事件序列中学习因果结构?

本文分享一篇我们在IJCAI 2023的最新工作&#xff0c;文章分析了在离散时间事件序列上存在的瞬时效应问题&#xff0c;提出了一种利用瞬时效应的结构霍克斯模型&#xff0c;且在理论上证明了事件序列上的瞬时因果关系同样是可识别的。 相关论文&#xff1a; Jie Qiao et al. “…

基于SpringBoot的家庭记账管理系统的设计与实现

摘 要 随着社会的发展&#xff0c;社会的方方面面都在利用信息化时代的优势。互联网的优势和普及使得各种系统的开发成为必需。 本文以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;它主要是采用java语言技术和mysql数据库来完成对系统的设计。整个…

数据结构之线性表

1.线性表的定义 线性表是由n(n≥0)个类型相同的数据元素组成的有限 序列&#xff0c;记作:L &#x1d44e;0, &#x1d44e;1, ⋯ , &#x1d44e;&#x1d456; , ⋯ , &#x1d44e;&#x1d45b;−1 2 线性表的顺序存储结构实现 线性表的顺序存储结构称为顺序表&#xff08;…

2023年前端面试汇总-HTML

1. src和href的区别 src和href都是用来引用外部的资源&#xff0c;它们的区别如下&#xff1a; src表示对资源的引用&#xff0c;它指向的内容会嵌入到当前标签所在的位置。src会将其指向的资源下载并应用到文档内&#xff0c;如请求js脚本。当浏览器解析到该元素时&#xff…

HyperLogLog数据结构

基数计数(cardinality counting) 通常用来统计一个集合中不重复的元素个数&#xff0c;例如统计某个网站的UV&#xff0c;或者用户搜索网站的关键词数量。数据分析、网络监控及数据库优化等领域都会涉及到基数计数的需求。 要实现基数计数&#xff0c;最简单的做法是记录集合中…

34岁出来面试,还被拒绝有多惨?

我强烈建议大家定期去参加一下外面的面试&#xff0c;尤其是BAT大厂的面试&#xff0c;不要一直闷在公司里&#xff0c;不然你很容易被这个世界遗弃。 前言 昨天&#xff0c;我们小组长奉命去面了一个34岁的测试员。 去了大概半个多小时吧&#xff0c;回来后&#xff0c;他的…

图数据库(二):Java操作图数据库

在上篇文章中&#xff0c;我们介绍了什么是Neo4j&#xff0c;什么是Cypher以及Neo4j的使用&#xff0c;今天我们学习一下如何使用Java操作Neo4j图数据库。 Cypher查询 在使用Java操作Neo4j之前&#xff0c;我们先了解一点&#xff0c;Cypher语句简单查询。 本文使用的是Neo4j…

Flutter的状态管理之Provider

Provider简介 Flutter Provider是Flutter中一个非常流行的状态管理库&#xff0c;它可以帮助开发者更加方便地管理应用程序中的状态。Provider提供了一种简单的方式来共享和管理应用程序中的数据&#xff0c;并且可以根据数据的变化来自动更新UI界面。 Provider的核心思想是将…

网络协议——什么是RIP协议?工作原理是什么?

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 作者会持续更新网络知识和python基础知识&#xff0c;期待你的关注 目录 一、什么是RIP协议&#xff1f; 二、为什么要使用RIP&#xff1f; 三、RIP用在哪里&#xff1f; 四、RIP协议的工作原理 五、总结 …

Redis安装布隆过滤器

目录 1 什么是布隆过滤器1.1 布隆过滤器的原理1.2 布隆过滤器缺点 2 插件形式安装2.1 下载布隆过滤器插件 3 docker方式单机安装4 Redis集群部署安装4.1 创建目录4.2 redis配置文件4.3 配置docker-compose.yml文件4.4 启动布隆过滤器集群4.5 配置集群4.6 布隆过滤器常用命令4.7…

如何将simulink中的元件(光伏板)导入到plecs中使用

simulink中有一些元件在plecs中是没有的&#xff0c;如果想要直接使用simulink的库&#xff0c;可以这样操作&#xff1a; 1 新建mdl文件&#xff08;simulink的文件类型&#xff09;&#xff0c;并在该文件中搭建好想要的模型、元件&#xff08;只放想要导出的元件就可以了&…

商城检索 DSL

模糊匹配过滤&#xff08;按照属性、分类、品牌、价格区间、库存&#xff09;排序分页高亮聚合分析 一. 搜索关键字 检索字段&#xff1a;商品sku标题 “skuTitle” : “华为 HUAWEI Mate 30 Pro 亮黑色 8GB256GB麒麟990旗舰芯片OLED环幕屏双4000万徕卡电影四摄4G全网通手机”…

基于Java+SpringBoot+Vue前后端分离考试学习一体机设计与实现(视频讲解)

博主介绍&#xff1a;✌全网粉丝3W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面有丰富的经验…

docker-compose单机容器集群编排

docker-compose dockerfile模板文件可以定义一个独立的应用容器&#xff0c;如果需要多个容器就需要服务编排。服务编排有很多技术方案 docker-compose开源的项目实现对容器集群的快速编排 docker-compose将所管理的容器分为三层&#xff0c;分别为工程&#xff0c;服务&#…

Jenkins+RF持续集成测试(三) 生成测试报告并发给指定人

在上一篇《定时更新SVN完成构建》中讲了定时从SVN拉取最新的测试脚本&#xff0c;并自动构建的过程。这篇我将介绍怎么配置测试robot报告&#xff0c;并发送给指定人群。 1、要发给指定人&#xff0c;首先需要在job配置&#xff0c;构建后操作中增加“Editable Email Notificat…