Linux——多线程(一)

一、线程的概念

1.1线程概念

教材中的概念: (有问题?)

线程是进程内部的一个执行分支,线程是CPU调度的基本单位

之前我们讲的进程:

加载到内存中的程序,叫做进程---(修正)---->进程=内核数据结构+进程代码和数据

1.2线程的理解(以Linux系统为例)

正文:代码段(区),我们的代码在进程中,全部都是串行(单进程)调用的

进程新建,(时空)成本较高(一堆数据结构,映射...)

地址空间和地址空间上的虚拟地址,本质其实是一种"资源"(进程的大部分资源需要通过地址空间来访问)

多执行流(并行) :正文代码分好,已、未初始化区公开,创建执行流就多加一个task_struct,指向同一块地址空间

我们把在进程地址空间中这样创建的"进程",称为线程

理解概念:线程是进程内部(进程地址空间)的一个执行分支,线程是CPU调度的基本单位,CPU调度只关注进程PCB,也就是task_struct

Linux为什么要这么设计"线程"?

如果我们要设计线程,OS也要对线程做管理。

怎么管理呢?依旧是那六个字,先描述,再组织

如果像上面这样设计的话,OS会变得非常的复杂

Linux的设计者认为:进程和线程都是执行流,具有极度的相似性,没必要单独设计数据结构和代码,直接复用就行

1.2.1线程与进程

进程 vs 线程

什么是进程?

中间那个圈里面的叫进程

task_struct 是进程中的执行流,这里我们暂时叫它线程

以前我们说的进程:一个内部只有一个线程的进程;

今天我们讲的进程:一个内部至少有一个线程的进程

CPU不用区分task_struct(进、线程都是执行流)

在内核的角度: 进程是承担分配系统资源的基本实体

关于调度的问题:线程<=执行流(轻量级进程)<=进程
Linux所有的调度执行流都叫做:轻量级进程

Linux内核中并没有线程的概念,线程是用进程PCB来模拟的

那么多个执行流如何进行代码划分?如何理解?

OS要不要管理内存呢?自然是要的

大部分OS的内存都是以4KB为大小的内存块组成

在OS的术语里:把4KB的空间or内容叫 页框/页帧

在物理内存中,每一块被分割成4KB大小的数据块,被叫做页框

以上还不是管理,OS要先描述,再组织----------->定义一个结构体

先描述:

cpp 复制代码
#define Kerucl 0x1
#define User 0x2
#define USR 0x4
#define NOUSR 0x8

struct Page
{
    int flag;
    //其他属性...
}

再组织

cpp 复制代码
struct Page mem[1048576];

对内存的管理,变成对此数组的增删查改(操作系统对内存管理的基本单位是4KB)

可执行文件:写好的代码会被编译器处理形成二进制可执行文件放在磁盘中,在运行的时候加载到内存中

编译器在处理源文件生成二进制可执行文件,同样是以4KB为单位的,这4KB的数据块被叫做页帧

1.2.2页表(页目录+页表项)

再来讲讲页表

每个进程的虚拟地址空间都是4GB,也就是2^32个地址,如果每个虚拟地址都在页表中存一个物理地址,那样占的空间也太大了,这还没算映射在物理内存的地址,再加上其它属性和另外一些杂七杂八的,假设每一行就占10B的空间,那么光是页表就占10*2^32 = 40G 的空间,怎么可能一个进程的页表比物理内存都大。

真正的页表是什么呢?

在32位机器上,地址的大小是4G字节,也就是有32个比特位

将32个比特位分为10个比特位,10个比特位,12个比特位,共3组:

32个比特位的最高10位,代表的是页目录的下标;[0,1023]的下标代表了页目录总共有2^10 = 1024个

页目录中存放的是页表项的地址,可以通过下标找到对应的页表项。

32个比特位的中间10位代表的是页表项的下标,最多能够有1024个页表项

页表项里面存的是物理内存中每个4KB内存块(页框)的起始地址

这最后的12位是什么呢?

0x1234+虚拟地址的后12位为对应的数据(页内偏移刚好4KB)

2^12 = 4KB = 1024 * 2 * 2

1.3Linux线程的系统调用

在上面中我们讲过,在Linux内核中是不存在线程这一个概念的,没有TCB数据结构以及管理算法,而我们所说的线程,在Linux系统都是轻量级进程

Linux操作系统中也没有提供创建线程的系统调用。

只提供了轻量级进程的系统调用

无论是宏观操作系统,还是用户(程序员)都只认线程的概念,但Linux内核中没有线程的概念。

那么Linux是怎么做到当用户在创建线程时,就在内核中创建轻量级进程的呢?

用户创建线程时会调用一个线程库,这个线程库里封装了Linux的轻量级进程的系统调用接口,这样一来就能在Linux中创建轻量级进程了。 而且这个线程库时所有Linux操作系统都一定自带的一个库,所以也叫原生线程库

1.4pthread_create(线程创建)

给不同的线程分配不同的区域,本质就是让不同的线程,各自看到全部页表的子集

man pthread_create

参数:

pthread_t *thread: 线程标识符线程tid

const pthread_attr_t *attr: 线程属性,这个阶段我们一般设为nullptr

void *(*start_routine) (void *): 函数指针,创建出的新线程执行的就是此函数的代码

void *arg : 传给线程函数的参数,是上个参数的指向函数的形参

返回值:创建成功返回0,创建失败返回错误码

用pthread_create创建一个新线程

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

void* handler(void *args)
{
    while(true)
    {
        cout<<"i am newthread"<<endl;
        sleep(1);
    }
    
} 

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,handler,nullptr);
    while(true)
    {
        cout<<"i am main thread,running..."<<endl;
        sleep(1);
    }

    return 0;
}

makefile

Crystal 复制代码
mytest:thread.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f mytest

此时编译报错:链接错误,编译器不认识pthread_create函数。我们创建新线程需要通过原生线程库去创建,此时编译器找不到原生线程库。我们需要使用-l去选项指定链接原生线程库pthread

这时候就编译成功了

用命令查了一下只查到了一个进程

用另外的命令查到两个执行流(-L查看轻量级进程)

PID: 这个我们之前讲过进程pid(进程标识符)

LWP:light weight process 轻量级进程

通过上图我们发现,两个执行流他们的PID是相同的,而LWP是不同的

这也证明线程是进程的一部分(其中的一个执行流)

可以看到第一个线程的LWP和PID是一样的,这个线程被叫做主线程 ,其它不一样的叫做新线程

新创建出来的线程称为新线程

main所对应的执行流会自动向下运行,称为主线程

那么OS在进行调度时,用哪个id进行调度呢?LWP

那以前我们讲的单进程、多进程的调度呢?

其实就是每个进程内部只有一个执行流,LWP==PID

二、线程的共有资源、私有资源

再了解一个函数

man pthread_self

无参数,获取线程id(tid)

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
using namespace std;
std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id,sizeof(id),"0x%lx",tid);
    return id;
}
int g_val = 2;
void* threadrun(void *args)
{
    int cnt = 5;
    std::string threadname = (char*)args;
    while(cnt)
    {
        cout<<threadname<<"is running:"<<cnt<<",pid:"<<getpid()<<",mythread id:"<<ToHex(pthread_self())<<",g_val:"<<g_val<<",&g_val"<<&g_val<<endl;
        g_val++;
        sleep(1);
        cnt--;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");
    int cnt = 10;
    while(cnt)
    {
        cout<<"main thread is running...,cnt:"<<cnt<<",pid:"<<getpid()<<",newthread tid"<<ToHex(tid)<<",main thread id:"<<ToHex(pthread_self())
        <<",g_val:"<<g_val<<",&g_val"<<&g_val<<endl;
        sleep(1);
        cnt--;
    }
    return 0;
}

注意不同线程中g_val的值以及地址:新线程、主线程看到的全局变量是同一个;当任何一个线程修改这个全局变量的值时,都会影响另外一个线程使用这个值

在新线程、主线程中,这个全局变量的地址相同,说明他们使用的是同一个全局变量

上面代码说明进程的数据段资源也被线程共享

进程中的绝大部分资源都是被所有线程共享的(代码和全局数据、进程文件描述符表)

那么线程自己有没有私有的资源呢?

PCB属性私有:所有线程都有各自的PCB,PCB中的属性肯定是私有的,属于各自线程

线程的硬件上下文数据(CPU寄存器的值)私有:CPU在调度PCB的时候,采用轮转时间片的方式,当一个线程被换下时,该线程的上下文一定是私有的,防止被其他线程修改而导致恢复上下文的时候出现错误。

线程的独立栈结构

不同线程各自的临时变量一定是私有的,而临时变量存放在栈结构中,所有栈也是私有的

共享的都是同一块虚拟地址空间,为什么非是栈结构私有呢?还有一个原因是原生线程库的实现。

man clone

系统调用clone是用来创建子进程(轻量级线程)的,也就是没有独立的虚拟地址空间,clone中有一个参数:void *stack,这个参数就是用来开这个子进程的栈空间的

所以 我们在调用pthread_create创建新线程的时候底层会调用clone去指定该线程的私有栈结构

三、线程特点

3.1线程优点

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

线程切换需要保存的上下文数据,只比进程少了一点,为什么说线程之间的切换需要操作系统做的工作要少很多?

cache:集成在CPU内的离CPU寄存器远而容量比寄存器大得多的缓存

一行代码执行完,大概率会执行下一行,可能将相关代码搬到cache中缓存起来,后面读代码直接从cache中读取;如果是两个进程之间的切换的话,那么cache缓存的内容会失效,切换B进程之后要清空cache,然后把B进程的代码和数据放里面,线程的切换中cache不用清空

3.2线程缺点

线程的健壮性较差:

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

缺乏访问控制:

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

编程难度提高:

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

性能损失:

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

主线程退出 == 进程退出 == 所有线程都要退出

所以

  1. 往往我们需要main thread最后结束
  2. 线程也要被"wait",要不然会产生类似进程那里的内存泄露的问题

四、线程控制

4.1pthread_create(线程创建)

在上面的1.4中我们已经提到了pthread_create的使用方法,这里做几点小补充

线程退出无非3中结果:

  1. 代码跑完,结果对
  2. 代码跑完,结果不对
  3. 出异常------重点
cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
using namespace std;
int g_val = 2;
void *threadrun(void *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,threadrun,(void*)"thread -1");
    void *ret = nullptr;
    while(true)
    {
        printf("main thread is running,g_val:%d,&g_val:%p\n",g_val,&g_val);
        sleep(1);
    }
    int n = pthread_join(tid,&ret);
    cout << "main thread quit, n=" << n << " main thread get a ret: " << (long long)ret << std::endl;
}

我们怎么没有像进程一样获取线程的退出信号呢?只有自己手动写的退出码。

因为一旦有一个线程出异常,整个进程就都寄了,不考虑线程异常的情况

4.2pthread_join(线程等待)

如何进行线程等待呢?man pthread_join

函数参数:

pthread_t thread : 线程标识符(tid)

void **retval:输出型参数,主线程等待新线程的返回值,&(void)------>void**

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

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

void* threadrun(void *args)
{
    int cnt = 5;
    std::string threadname = (char*)args;
    while(cnt)
    {
        cout<<threadname<<" is running..."<<endl;
        sleep(1);
        cnt--;
    }
    return (void*)123;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");
    void *ret = nullptr;
    int cnt = 10;
    while(cnt)
    {
        cout<<"main thread is running..."<<endl;
        sleep(1);
        cnt--;
    }
    int n = pthread_join(tid,&ret);
    cout << "main thread quit, n=" << n << std::endl;
    return 0;
}

4.3pthread_exit(线程结束)

线程结束

1.线程函数结束

2.pthread_exit()

不能直接exit线程,因为它是终止进程的

参数:void* retval:返回线程结束信息,当前阶段设置成nullptr即可。

cpp 复制代码
void* threadrun(void *args)
{
    int cnt = 5;
    std::string threadname = (char*)args;
    while(cnt)
    {
        cout<<threadname<<" is running..."<<endl;
        sleep(1);
        cnt--;
        if(cnt == 2)
        {
            pthread_exit(nullptr);
        }
    }
    return (void*)123;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");
    void *ret = nullptr;
    int cnt = 10;
    while(cnt)
    {
        cout<<"main thread is running..."<<endl;
        sleep(1);
        cnt--;
    }
    int n = pthread_join(tid,&ret);
    cout << "main thread quit, n=" << n << std::endl;
    return 0;
}

新线程退出,只剩下主线程

3.int pthread_cancel(tid);//线程取消

参数:要取消线程的tid

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

cpp 复制代码
void* threadrun(void *args)
{
    int cnt = 5;
    std::string threadname = (char*)args;
    while(cnt)
    {
        cout<<threadname<<" is running..."<<endl;
        sleep(1);
        cnt--;
    }
    return (void*)123;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");
    void *ret = nullptr;
    int cnt = 10;
    while(cnt)
    {
        cout<<"main thread is running..."<<endl;
        sleep(1);
        cnt--;
        if(cnt == 7)
        {
            break;
        }
    }
    pthread_cancel(tid);
    cout<<"pthread_cancel:"<<tid<<endl;
    int n = pthread_join(tid,&ret);
    cout << "main thread quit, n=" << n << " main thread get a ret: " << (long long)ret << std::endl;
    return 0;
}

4.4pthread_detach(线程分离)

上面线程等待的时候,主线程就需要阻塞式的等待线程的释放,主线程就什么都干不了。能不能像以前的进程那样不需要阻塞等待(将信号设为忽略),等新线程结束后自动释放呢?尤其是不关心线程的返回值的时候,线程等待是一种负担

自然是可以的,只需要将自动释放的线程设置成分离状态(主线程不需要关心该线程状态,它就会自动释放)

man pthread_detach

参数:pthread_t thread: 要分离线程的tid

返回值:分离成功返回0,失败返回错误码

相关推荐
萝卜兽编程6 分钟前
优先级队列
c++·算法
Bruce小鬼8 分钟前
QT文件基本操作
开发语言·qt
2202_7544215413 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
我只会发热20 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
懷淰メ30 分钟前
PyQt飞机大战游戏(附下载地址)
开发语言·python·qt·游戏·pyqt·游戏开发·pyqt5
hummhumm44 分钟前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
宁静@星空1 小时前
006-自定义枚举注解
java·开发语言
运维&陈同学1 小时前
【zookeeper03】消息队列与微服务之zookeeper集群部署
linux·微服务·zookeeper·云原生·消息队列·云计算·java-zookeeper
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
武子康1 小时前
Java-07 深入浅出 MyBatis - 一对多模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据库·sql·mybatis·springboot