当前位置: 首页 > article >正文

Linux线程基础

🌎 Linux线程

文章目录:

Linux线程

    线程概念
      线程的理解

    再谈地址空间

    线程控制
      线程等待
      线程资源共享
      线程退出
      线程异常
      线程分离
      理解线程tid

    线程切换

    线程优缺点

    Linux线程 vs 进程
      资源共享
      线程私有

    线程传参及返回

    C++11多线程


🚀线程概念

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

  线程是进程内部(地址空间内)的一个执行分支,且线程是CPU调度的基本单位。

✈️线程的理解

  加载到内存中的程序,我们称为进程。我们创建一个进程,简单来说是,分配进程地址空间,加载各种数据,与物理内存之间建立映射关系等。做完这些动作,我们才能说创建了进程,也就是说,创建一个进程的开销(时间空间成本开销)是很大的

  并且我们所写的程序,每一个函数都会被解析为一个个代码块,每个代码块都有自己的入口地址(C语言中的函数名即为地址),进程对程序中的函数进行调用时,只能一个个的调用,并不能同时调用多个函数。也就是说,我们代码在进程中全部是串行调用的

  而我们要说的进程,可以使函数并行调用

在这里插入图片描述

  在Linux当中,并不存在真正的 “线程”, 这是因为Linux的设计者认为,线程和进程都是执行流,具有高度相似性,没必要为线程单独设计数据结构与算法,所以 Linux线程是使用进程来模拟线程的! 有些操作系统有真实的线程,比如windows。

  而在CPU看来,不论是进程还是线程,当CPU拿到时并不会明确的区分,因为CPU拿到的都是执行流,所以对于CPU来说进程还是线程根本就不重要。所以 Linux中的所有调度执行流,都可称为 轻量级进程(Light Weight Process, 简称LWP)

  线程在进程中可能存在多个,所以OS需要对线程进行管理,先描述再组织。所以线程的task_struct 称为 TCB(Thread Control Block: 线程控制块)

  那么我们来重新从 内核角度 认识一下进程的概念:承担分配系统资源的基本实体。分配资源包括线程资源的分配等等。


🚀再谈地址空间

  既然我们已经初步了解了线程的概念,那么我们是不是该思考,一个进程既然存在多个执行流,那么是如何对这些代码进行划分的呢?这里就不得不重新认识一下地址空间了。

  内存,作为操作系统密不可分的一块区域,与磁盘不同,因为其距离CPU更近,更快。内存跟文件系统类似,被划分为一个个的块,每个块的大小为4kb(现在电脑中大多内存块大小都为4kb)。

  我们前面学习过,磁盘中的文件系统,每个文件都有自己数据块,每个数据块有自己的inode,并且基本单位为4kb大小。

  不论在磁盘还是在内存,4kb大小的空间(存储的才叫做内容),都被称为 页框 或 页帧

  这些大小的内存块需不需要被管理呢?内存当中存在很多内存块,明显是需要的。那么 先描述再组织

// 一些标志位
#define Kernel 0x1
#define User 0x2
#define USR 0x4
#define NoUse 0x8
#define Lock 0xF
//...

struct Page
{
	int flag;
	// 其他字段
}

  假设我们有一个4GB的内存,以4kb内存块为单位,那么计算下来,共有 1048576 个内存块,那么我们就可以如此定义:

struct page mem[1048576];// 数组的下标天然形成了内存块的编号

  这样,数组的下标天然形成了内存块的编号,就不在需要额外的字段定义内存块编号了。从此以后,OS对内存的管理,转换为对数组的增删查改

  则磁盘加载到内存的数据就可以以4kb为单位进行加载。所以 操作系统进行内存管理的基本单位就是 4kb。实际上,当我们写C语言程序的malloc,realloc等函数时,以及OS的写时拷贝计数,都是以4kb为单位的,只不过C语言层面有更进一步的处理动作。

  我们之前说过,页表不仅有虚拟到物理之间的映射,还有一些选项(权限设置等),那么假设页表一行占用10个字节,而地址空间有232数量的地址,如果这些地址全部映射到内存,那么需要232 * 10 byte 大小的页表。这显然是不可能的。

  实际上从虚拟地址到物理地址之间的转换,我们通过虚拟地址来完成,常规OS虚拟地址为32位,而这32位虚拟地址被划分为3部分:页目录、页表、页内偏移

在这里插入图片描述
  我们把页目录与页表内的条目,称为 页表项。而他们之间的关系完全可以用下面这一张图来清晰的解释:

在这里插入图片描述

  虚拟地址的前十位为页目录,页目录的每一个页表项包含每张页表的地址,页目录最大有1024个页表项。前十位比特位查找到对应的页表,页表的页表项内保存的是内存的每一个内存块地址,通过中间10位比特位可以查找到对应的内存块,最大有1024张页表。

  前面我们说了,物理内存的内存块大小为4kb,转换为2进制其实也就是12位比特位,正好对应了最后12位比特位,从特定的内存块进行页内偏移找到对应的需要获取的内容。所以说,页表内保存的是内存的物理地址没错,只不过是页框的物理地址

  所以我们从内存中查询数据时,需要经过的步骤为:页目录 => 页表 => 内存块 => 数据(页内偏移)。也就完成了虚拟到物理的转化。


🚀线程控制

  经过上面学习,我们了解了 Linux中没有真正的线程,有的是轻量级进程。所以原本在Linux中并不存在线程库,只存在进程的系统调用库。为了支持 “线程” ,操作系统为了让用户可以使用线程,所以Linux给我们提供了一个原生线程库。

  原生线程库的作用将轻量级进程进行封装,转换成线程相关的接口语义提供给用户。且原生线程库是必须存在的,但是不属于内核(用户级线程)。

  而原生线程库的位置:

在这里插入图片描述

  创建一个线程,我们使用pthread_create()接口:

在这里插入图片描述
  各个参数含义:

  • thread: 返回线程ID
  • attr: 设置线程的属性,attr为NULL表示使用默认属性
  • start_routine: 是个函数地址,线程启动后要执行的函数
  • arg: 传给线程启动函数的参数
  • 返回值成功返回0;失败返回错误码

  我们立刻编写一个简单的线程demo:

#include <iostream>
#include <pthread.h>
#include <unistd.h>

// 新创建线程执行回调
void* newthreadrun(void* args)
{
    while(true)
    {
        std::cout << "This is a new thread" << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, newthreadrun, nullptr);

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

makefile:

testThread:test.cc
	g++ -o $@ $^ -lpthread -std=c++11 #-lpthread 链接原生线程库

.PHONY:clean
clean:
	rm -f testThread

在这里插入图片描述

  当我们想查看执行流时,使用上图的命令,只能查看到一个执行流,也就是进程,如果我们想要查看线程的情况,使用如下命令:

 ps -aL | head -1 && ps -aL | grep testThread #L表示轻量级进程LWP

在这里插入图片描述
  这样就可查出每个执行流了,我们看到这两个线程的pid相同,这表示他们同属一个进程。而Linux下的线程id是LWP的id。所以从此处看,线程确实只是进程中的一个执行流。

  而要查看线程的tid,我们可以通过直接打印得到,也可以通过 thread_self() 函数调用得到:

在这里插入图片描述
  这两种线程id获取方式是有所不同,但是打印的结果是相同的,因为其过于长,所以下面的测试用例我使用了16进制转换:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

// 进制转换
std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", tid);
    return id;
}

void* newthreadrun(void* args)
{ 
    int cnt = 10;
    while(cnt)
    {
        std::cout << "This is a new thread is running: " << cnt << " pid: " << getpid() << "new thread id: " << ToHex(pthread_self()) << std::endl;
        sleep(1);
        cnt--;
    }

    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, newthreadrun, nullptr);

    int cnt = 10;
    while(cnt--)
    {
        std::cout << "I am main thread is running: "<< cnt << ", pid:"
         << getpid()  << "new thread id: " << ToHex(tid) <<  " " 
         << "main thread id: "<< ToHex(pthread_self()) << std::endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
  这样就拿到了线程的id,并将其转化为16进制。

是新线程先运行还是主线程先运行?

  新线程和主线程部分运行顺序,是不确定的,这个由调度器决定。

pthread_create()函数最后一个参数的含义?

  实际上最后一个参数的类型是(void*),这表示可以传入任意类型,这个参数表示线程的线程名称。比如:

在这里插入图片描述

主线程退出意味着什么?会影响新线程吗?

  实际上,主线程其实就是进程本身,主线程退出也就意味着进程退出,进程退出会回收进程的资源和空间,所以进程退出时,所有资源将不复存在,所以,进程退出时,其他线程资源会被回收。

  这意味着,我们需要保证主线程往往是最后一个退出的。我们知道进程退出,如果子进程还没回收,那么就会产生僵尸,解决方法是wait。同样,线程退出也需要wait方法。而线程的等待方法为 pthread_join()


✈️线程等待

  多线程与多进程一样,需要对其他线程资源进行回收,进程的回收方法为wait,而线程的回收方法为pthread_join():

在这里插入图片描述
  选项参数:

  • thread: 线程ID
  • retval: 输出型参数,它指向一个指针,后者指向线程的返回值
  • 返回值成功返回0;失败返回错误码

需要进行线程等待的原因

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
  • 创建新的线程不会复用刚才退出线程的地址空间

  我们让mian thread除了等待new thread以外不做任何事,new thread创建打印一段话,每隔一秒打印一次,总计五秒,我们观察线程状态:

在这里插入图片描述

  这时候线程就正常退出了。

  thread_join()第二个参数实际上是一个输出型参数,它指向的是创建线程时新线程调用的函数返回值。我们可以将其打印出来:

void* newthreadrun(void* args)
{ 
    std::string threadname = (char*)args;
    int cnt = 5;
    while(cnt)
    {
        std::cout << "New thread name: " << threadname <<" This is a new thread is running: "
            << cnt << " pid: " << getpid() << "new thread id: "
            << ToHex(pthread_self()) << std::endl;
        sleep(1);
        cnt--;
    }

    return (void*)123;
}

int main()
{
    // main thread do northing, wait 5 second recyle new thread
    pthread_t tid;
    pthread_create(&tid, nullptr, newthreadrun, (void*)"thread-1");

    void* ret = nullptr;
    int n = pthread_join(tid, &ret);

    std::cout << "main thread quit,  ret_value:n= " << n << "main thread get threadrun ret: " << (long long)ret << std::endl;
    return 0;
}

在这里插入图片描述

✈️线程资源共享

  同一个进程下的所有线程共享进程的资源,我们定义一个全局变量,让新线程和主线程分别同时打印变量的值,观察是否相同:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

int g_val = 100;

std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", tid);
    return id;
}

void* newthreadrun(void* args)
{ 
    std::string threadname = (char*)args;
    int cnt = 5;
    while(cnt)
    {
        printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
        g_val++;
        sleep(1);
        cnt--;
    }

    return (void*)123;
}

int main()
{
    // main thread do northing, wait 5 second recyle new thread
    pthread_t tid;
    pthread_create(&tid, nullptr, newthreadrun, (void*)"thread-1");

    void* ret = nullptr;

    int cnt = 10;
    while(cnt)
    {
        printf("main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
        sleep(1);
        cnt--;
    }

    int n = pthread_join(tid, &ret);
    std::cout << "main thread quit,  ret_value:n= " << n << "main thread get threadrun ret: " << (long long)ret << std::endl;
    return 0;
}

在这里插入图片描述

✈️线程异常

  如果我们在运行线程的代码,但是线程却出现了异常,会发生什么事情?

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

int g_val = 100;

std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", tid);
    return id;
}

void* newthreadrun(void* args)
{ 
    std::string threadname = (char*)args;
    int cnt = 5;
    while(cnt)
    {
        printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
        g_val++;
        sleep(1);

        // 野指针错误
        int *p = nullptr;
        *p = 100;
        
        cnt--;
    }

    return (void*)123;
}

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

    void* ret = nullptr;
    while(true)
    {
        printf("main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
        sleep(1);
    }

    int n = pthread_join(tid, &ret);
    std::cout << "main thread quit,  ret_value:n= " << n << "main thread get threadrun ret: " << (long long)ret << std::endl;
    return 0;
}

在这里插入图片描述
  从结果我们可以发现,当新线程发生异常并且主线程没有任何问题时,整个进程就会退出。所以我们可以得出结论:多线程中,当一个线程出现异常,那么整个进程都要退出。所以 多线程代码,往往健壮性不好

  我们之前在学习进程的时候,进程退出时我们经常会打印退出码,来表示进程是否是正常退出的,通过自定义信号或者是Linux信号我们可以判断异常的类型。那为什么线程这里我们并没有观察到什么“线程退出码” 呢?很简单,因为只要其中一个线程出问题了,整个进程都会被终止,所以pthread_join()不关心线程异常。

✈️线程退出

  进程退出时可以使用exit(), 或者_exit()来终止进程,线程难不成也是用这两个接口来退出线程吗?显然不行,exit()是直接退出进程,所有的线程都会被终止。在Linux的原生线程库中,给我们提供了线程退出的接口pthread_exit()

在这里插入图片描述
函数参数:

  • retval: retval不要指向一个局部变量
  • 返回值无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

注意
  pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了

#include <iostream>
#include <pthread.h>
#include <cstdlib>
#include <unistd.h>
#include <string>

int g_val = 100;

std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", tid);
    return id;
}

void* newthreadrun(void* args)
{ 
    std::string threadname = (char*)args;
    int cnt = 5;
    while(cnt)
    {
        printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
        g_val++;
        sleep(1);
        cnt--;
    }

    pthread_exit((void*)123);
}

int main()
{
    // main thread do northing, wait 5 second recyle new thread
    pthread_t tid;
    pthread_create(&tid, nullptr, newthreadrun, (void*)"thread-1");

    void* ret = nullptr;
    int n = pthread_join(tid, &ret);
    std::cout << "main thread quit,ret_value:n=" << n << " main thread get threadrun ret: " << (long long)ret << std::endl;
    return 0;
}

在这里插入图片描述
  线程还有另外一种接口方式退出,使用 pthread_cancle():

int pthread_cancel(pthread_t thread);

函数参数

  • 功能取消一个执行中的线程
  • thread: 线程ID
  • 返回值成功返回0;失败返回错误码

  此时我们让新线程仅仅跑两秒则被主线程退出,观察此时的返回值:

#include <iostream>
#include <pthread.h>
#include <cstdlib>
#include <unistd.h>
#include <string>

int g_val = 100;

std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", tid);
    return id;
}

void* newthreadrun(void* args)
{ 
    std::string threadname = (char*)args;
    int cnt = 5;
    while(cnt)
    {
        printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
        g_val++;
        sleep(1);
        cnt--;
    }

    pthread_exit((void*)123);
}

int main()
{
    // main thread do northing, wait 5 second recyle new thread
    pthread_t tid;
    pthread_create(&tid, nullptr, newthreadrun, (void*)"thread-1");
	
	sleep(2);
	pthread_cancel(tid);// 关闭线程

    void* ret = nullptr;
    int n = pthread_join(tid, &ret);
    std::cout << "main thread quit,ret_value:n=" << n << " main thread get threadrun ret: " << (long long)ret << std::endl;
    return 0;
}

在这里插入图片描述

  从上述代码可以观察到,pthread_cancel()有点类似于Linux当中的kill命令。当一个线程被取消了,那么其退出结果就是-1,在Linux内核中定义为 PTHREAD_CANCELED 的宏:

在这里插入图片描述

✈️线程分离

  我们在使用pthread_join()时,main线程会阻塞等待其他线程。为了让主线程在其他线程运行时也可同时运行。

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

  如果我们main线程不关心新线程执行信息,我们可将新线程设置为分离状态,我们可以使用 pthread_detach() 接口使线程分离:

在这里插入图片描述
  如果设置了pthread_detach(), 那么在main线程中使用pthread_join()则会报错:

在这里插入图片描述
  线程分离表示从今往后,main thread再也不关心调用pthread_detach()的线程了,从而多个进程可以实现真正的并发了。但是如果创建的新线程生命周期要比main thread生命周期长的话,当main thread结束,其他线程也会被终止,因为main thread结束,表示进程结束,需要回收进程资源,而其他线程与main thread共享进程资源也会被回收。所以 不论线程是否分离,都要保证main thread最后一个退出

  所以到底什么是线程分离呢?线程分离只是一种工作状态,底层依旧属于同一进程, 只是不需要等待了

✈️理解线程tid

  前面我们把线程的tid打印了出来,并且因为其太长将其转化为了16进制。我们知道,Linux中没有真正线程,只有轻量级进程。

  当程序刚刚启动时,代码还没有执行到pthread_create()时,此时还是进程,而系统中没有线程概念,但是却有轻量级进程概念。那么用户就可以通过接口来管理“线程”,比如pthread_create(), pthread_join()… 这些接口我们是通过库函数来调用的:

在这里插入图片描述

  实际上这个动态库对底层做了封装,所以我们使用的线程库也被称为 用户级线程。而这个线程库属于文件,所以这个库一定是磁盘文件。而这个库文件又是动态库文件,所以它一定会映射到进程的共享空间当中。

  进程的内核空间只会提供一些轻量级进程的接口,至于线程id,线程优先级,线程状态,线程栈等等这些进程是不知道的。那么这些信息既然不是进程来管理,是谁在管理呢?实际上 线程的管理工作由库来完成。如何管理?先描述再组织

  所以在库中,必须要有描述该线程的结构体,其中包含线程id,线程优先级等等属性字段,具体的描述组织方式如下图所示:

在这里插入图片描述
  上图库当中的每个线程块,也有自己的结构体,叫做 TCB(Thread Control Block: 线程控制块 )。而 每个线程控制块的起始地址就被称为线程的tid!

  包括线程的栈结构,也是在动态库内维护的。我们在学习动静态库时学习过,动态库又叫做共享库,也就是说,同一进程下的所有线程都会使用该动态库,那也就意味着,该 动态库可以管理当前进程的所有线程

  在动态库中,我们能够看到 线程的局部存储,我们来举例说明(设置两个线程函数,创建两个线程,两个线程分别调用两个线程函数,函数全部都对同一个全局变量做自减操作):

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>

int g_val = 100;

void* threadrun1(void* args)
{
    std::string res = static_cast<char*>(args);
    while(1)
    {
        g_val -= 1;
        std::cout << res << " is running, " <<  "g_val : " << g_val << std::endl;
        sleep(1);
    }

    return nullptr;
}

void* threadrun2(void* args)
{
    std::string res = static_cast<char*>(args);
    while(1)
    {
        g_val -= 1;
        std::cout << res << " is running, " <<  "g_val : " << g_val << std::endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, nullptr, threadrun1, (void*)"thread-1");
    pthread_create(&tid2, nullptr, threadrun2, (void*)"thread-2");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);

    return 0;
}

在这里插入图片描述

  但是如果我们想要每个线程都私有一份全局变量,我们可以将全局变量前加上 __thread, 这表示该全局变量在所有线程中都私有一份,现在我让其中一个线程停止修改g_val的操作,再次打印观察结果:

在这里插入图片描述
  如果你将前后两次局部变量的地址打印出来,你会发现当使用了__thread修饰之后,两个线程的g_val的地址就变得不同了。线程局部存储这种技术相对来说应用还是比较少一点的。但是 局部存储只能存储内置类型


🚀线程切换

与进程切换相比,线程切换OS要做的工作少很多

  对,不过主因不是因为寄存器,而是因为一个名为Cache的缓存,在进程间切换时,寄存器自然需要缓存一定的信息,而真正导致进程切换开销大的主要还是Cache。Cache名为缓存,更加靠近CPU,程序在执行代码时,很大概率会接着运行当前代码的下一行代码,所以会将代码数据放在缓存里,这样下次再拿数据时可以从更快的缓存里拿数据了。

  而当进程切换的时候,Cache上的内容要全部清空,并且需要重新加载新的进程的数据,这个过程的开销是很大的。

  而线程切换,因为所有线程共享进程资源,所以Cache中的数据不需要做清除并且重新加载,建立映射等等功能。寄存器开销与进程差别并不大。综上所述,线程切换主要因为共享进程资源不需要对cache重新加载数据,所以线程切换OS要做的工作比进程少得多。

在这里插入图片描述
  可以看出,CPU的缓存都是kb级别的,所以cache加载过程是很消耗资源的。


🚀线程优缺点

优点

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

缺点

  • 性能损失

  一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

  • 健壮性降低

  编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

  • 缺乏访问控制

  进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

  • 编程难度提高

  编写与调试一个多线程程序比单线程程序困难得多


🚀Linux线程 vs 进程

✈️资源共享
  • 进程是资源分配的基本单位
  • 线程是调度的基本单位

  进程的多个线程共享同一地址空间, 因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个 全局变量,在各线程中都可以访问到,除此之外, 各线程还 共享 以下进程资源和环境:
  |___ 文件描述符表
  |___ 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  |___ 当前工作目录
  |___ 用户id和组id

进程和线程的关系图:

在这里插入图片描述

✈️线程私有

线程私有重要的两个点:

1、线程的硬件上下文(CPU寄存器的值)(强调调度)
2、线程的独立栈结构(强调常规运行)

  线程并不是所有资源都共享的,每个线程都会拥有自己的独立栈空间,这样一个线程函数内的临时变量就会保存在自己的栈空间内,不会与其他栈空间内的临时变量等资源起冲突。

  • 线程共享进程数据,但也拥有自己的一部分私有数据:
    |___ 线程ID
    |___ 一组寄存器(硬件上下文)
    |___ 栈
    |___ errno
    |___ 信号屏蔽字
    |___ 调度优先级

线程安全

  一个线程出问题了,导致其他线程出了问题,导致进程退出。这关乎着线程安全问题。

  在多线程应用场景中,如果存在一个公共函数被多个线程同时进入,那么该函数就被重入了。


🚀线程传参及返回

  线程传参与返回值不仅仅可以传内置类型,我们 自定义类型也可以传参与做返回值。线程传参和返回值,我们可以传递级别消息,也可以传递其他对象(包括自定义类型对象)。

  下面以类和对象的角度,实现进程传自定义类型,并且打印结果:

#include <iostream>
#include <pthread.h>
#include <cstdlib>
#include <unistd.h>
#include <string>
#include <vector>
#include <cstdio>

const int threadrun = 5; // create five number threads

class Task
{
public:
    Task()
    {}

    void SetData(int x, int y)
    {
        _datax = x;
        _datay = y;
    }

    int Excute()
    {
        return _datax + _datay;
    }

    ~Task()
    {}

private:
    int _datax;
    int _datay;
};

class ThreadData : public Task
{
public:
    ThreadData(int x, int y, const std::string &threadname):_threadname(threadname)
    {
        _t.SetData(x, y); 
    }

    std::string threadname()
    {
        return _threadname;
    }

    int run()
    {
        return _t.Excute();
    }

private:
    std::string _threadname;
    Task _t;
};

class Result
{
public:
    Result(){}
    ~Result(){}

    void SetResult(int result, const std::string &threadname)
    {
        _result = result;
        _threadname = threadname;
    }

    void Print()
    {
        std::cout << _threadname << " : " << _result << std::endl;
    }

private:
    int _result;
    std::string _threadname;
};

void* handlerTask(void* args)
{

    ThreadData *td = static_cast<ThreadData*>(args);

    std::string name = td->threadname();
    Result* res = new Result();
    int result = td->run();

    res->SetResult(result, name);

    std::cout << name << "run result: " << result << std::endl;

    delete td;

    sleep(2);
    return (Result*)res;
}

// 1. create multi threads
// 2. 线程传参和返回值,我们可以传递级别信息,也可以传递其他对象(包括你自己定义的!)
int main() 
{
    std::vector<pthread_t> threads;
    for(int i = 0; i < threadrun; ++i)
    {
        char threadname[64];
        snprintf(threadname, sizeof(threadname), "Thread-%d", i+1); // 向threadname写入当前进程名称
        ThreadData *td = new ThreadData(10, 20, threadname); // 每个线程指向独立的线程信息

        pthread_t tid;
        pthread_create(&tid, nullptr, handlerTask, td);// 传参传入 td 对象
        threads.push_back(tid);
    }

    std::vector<Result*> result_set;
    void* ret = nullptr;
    for(auto & tid : threads)
    {
        pthread_join(tid, &ret);
        result_set.push_back(static_cast<Result*>(ret));
    }

    for(auto & res : result_set)
    {
        res->Print();
        delete res;
    }

    return 0;
}

在这里插入图片描述


🚀C++11多线程

  如果你学过C++11那么,你一定知道C++11也是支持多线程的,那么在编译阶段,makefile就不要带-lpthread来链接原生线程库了,我们以C++11的代码举例:

#include <iostream>
#include <thread>
#include <unistd.h>

// C++11 多线程编程
void threadrun(int num)
{
    while(num)
    {
        std::cout << "I am a new thread, num: " << num << std::endl;
        num--;
        sleep(1);
    }
}

int main()
{
    std::thread t1(threadrun, 10);

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

    return 0;
}

在这里插入图片描述

  我们编译时发现,不能编译通过,原因竟然是查询不到 pthread_create所链接的库??这里也就能说明 C++11的线程库是封装了Linux的原生线程库的

  这里是在Linux下的情况,为什么要封装呢?Windows环境下呢? 实际上C++具有跨平台性,在Linux下C++会封装Linux的原生线程库,如果把上面的代码搬到windows下来,同样可以运行,这时就会去链接windows下的库,从而实现跨平台性。也就是说,C++标准库在windows和Linux下编译是不同的结果

  要知道,可不止C++支持线程,java,python,go等许多语言都支持线程库,那么其他语言是如何支持线程的呢?在Linux环境下,这些语言大多都是对Linux下原生线程库进行的封装,只不过java特殊一些,在虚拟机上运行的。



http://www.kler.cn/a/311365.html

相关文章:

  • Django 的 ModelViewSet 中的 get_queryset 方法自定义查询集
  • AI大模型:重塑软件开发流程的优势、挑战及应对策略
  • 今天给在家介绍一篇基于jsp的旅游网站设计与实现
  • CAN总线数据帧格式详细介绍
  • 电脑提示xinput1_3.dll丢失怎么办?游戏DLL修复方法详解
  • linux详解,基本网络枚举
  • Java-测试-Mockito 入门篇
  • FTP、SFTP安装,整合Springboot教程
  • 基于剪切板的高速翻译工具
  • 【Qt | QAction】Qt 的 QAction 类介绍
  • 电脑键盘功能基础知识汇总
  • Leetcode面试经典150题-130.被围绕的区域
  • MySql-单表以及多表查询详解
  • paddle 分类网络
  • 【Linux】【Vim】Vim 基础
  • Doris相关记录
  • 【计算机基础题目】二叉树的前序中序后续遍历之间相互转换 详细例子
  • 我的demo保卫萝卜中的技术要点
  • O1-preview:智能预测与预取驱动的性能优化处理器设计OPEN AI
  • Semaphore UI --Ansible webui
  • 心觉:成功学就像一把刀,有什么作用关键在于使用者(二)
  • 进入C++
  • Spring WebFlux实践与源码解析
  • leetcode41. 缺失的第一个正数,原地哈希表
  • Vue2篇
  • 无线感知会议系列【2】【智能无感感知 特征,算法,数据集】