百度推广费用怎么算,东莞网络优化专业乐云seo,网站开发资源,专业网站建设公司兴田德润信任高目录
前置知识
线程的概念
Linux中对线程的理解 重新定义进程与线程
重谈地址空间
线程的优缺点
线程的优点
线程的缺点
线程异常
线程的用途
Linux线程 VS 进程
线程控制
创建线程
线程等待
线程终止
线程ID的深入理解 前置知识 我们知道一个进程有属于自己的P…目录
前置知识
线程的概念
Linux中对线程的理解 重新定义进程与线程
重谈地址空间
线程的优缺点
线程的优点
线程的缺点
线程异常
线程的用途
Linux线程 VS 进程
线程控制
创建线程
线程等待
线程终止
线程ID的深入理解 前置知识 我们知道一个进程有属于自己的PCB(task_struct)地址空间页表OS会为进程在物理内存中申请资源(空间)通过页表与地址空间产生映射关系。在执行进程时会通过地址空间使用OS为进程申请的资源。
因此我们可以说 地址空间时进程的资源窗口 在创建子进程时也会为子进程创建属于它的PCB地址空间页表与物理内存的资源。子进程会将父进程的部分属性拷贝下来。 线程的概念
在一些教材中对于线程是这样定义的 线程是进程内的一个执行分支。线程的执行粒度要比进程要细。 这里对线程的定义是通过线程的特点定义的并不能很好的解释什么是线程。
这里可以这样解释
Linux中对线程的理解 我们知道在之前我们创建的进程都是一个PCB(task_struct)的这里我们再创建多个特殊的 进程 : 这个 进程 有属于自己的PCB,但是与它的 父进程共享同一个地址空间同一个页表同一块物理内存。即将只属于一个PCB的栈堆等资源划分一部分给另一个新创建的PCB). 由于父进程与新创建的“子进程”他们共享一个地址空间可能会导致他们可以使用同一个资源。 这里我们可以说这些进程的执行粒度要比原来进程(一个task_struct)的执行粒度要小。 原因原来进程由自己一个就可以执行完全代码使用空间。而现在却需要多个进程去执行代码使用空间。这些进程是原来进程的一个执行分支执行流。因此我们说现在这些 进程的执行粒度小于原来进程的。 这里为了和原来的进程进行区分我们把新创建的进程叫做线程。 Linux中实现线程的方案 1.在Linux中线程在进程内部执行即线程在进程的地址空间内运行。 任何执行流要执行都要有资源在上面我们说过地址空间是进程的资源窗口。这里线程采用了共用同一个地址空间将地址空间分成若干份分配给线程)。所以说新城在地址空间中运行。 2.在Linux中线程的执行粒度要比进程要细。 线程执行了进程的一部分代码共享同一份资源。 注不同的操作系统对线程的实现方案可能是不一样的但实现原理都是一样的。 同时我们知道操作系统用PCB对进程描述与管理会通过进程的PCB进行调度但是对于多线程来说一个进程中会有多个PCB那么该如何调度的呢 这里我们要知道在OS中是通过CPU进行调度的对于CPU来说它并没有进程与线程的概念它只有调度执行流的概念即task_struct只要有代码与数据让CPU去执行就可以了)。 重新定义进程与线程 什么叫做线程 我们认为线程是操作系统调度的基本单位。 什么是进程 在内核角度进程是操作系统分配资源的基本实体执行流也是资源所以线程是进程内部的执行流资源 对于以前的进程可以这样理解操作系统是以进程为基本单位进行资源分配在进程内部就有一个执行流。 Linux中对线程的描述与管理 这里我们知道在操作系统里一个进程可能有1个或多个线程所以进程和线程的关系是1N的。在操作系统用PCB(task_struct)来描述和管理进程所以线程也可以采用同样的方式来进行管理在有些操作系统中Windows用struct tcbthread control block来进行描述与管理。但是我们知道对于进程的管理就已经很复杂了如果再进一步细分struct tcb会更加的复杂。所以在Linux中并没有使用struct tcb来对线程进行描述与管理由于线程是进程的一个执行流在大体上并没有太大的区别于是采用了复用进程的PCBtask_struct来对线程进行描述与管理。
因此可以在进程中只有一个PCB时把它当作进程的多个PCB时当作线程的。甚至可以不区分这些直接把他们当作执行流去看待。 由于Linux是采取复用进程的结构体去管理线程的所以Linux没有真正意义上的线程而是用进程的内核数据结构 模拟线程的。 这里以CPU的角度去看 线程执行流进程 所以在Linux中执行流也叫做轻量级进程。 重谈地址空间
这里我们知道进程是OS分配资源的基本单位线程是调度的基本单位。在进程里多线程会共享同一块地址空间它们会把 地址空间里的资源划分给每一个线程那么是如何划分的呢这里我们就不得不再谈地址空间了。 虚拟地址是如何转换到物理地址以32位虚拟地址为例
在Linux中将32位的虚拟地址分为了三部分32101012。同时页表并不是直接记录在一张表里的。因为地址空间一共有4GB个一个按照10字节算页表最大为40GB明显太大不能存储下来更别说其他的了。页表是这样的分为两级第一级页表有1024个存放第二级页表地址第二级页表有1024个存放了物理内存中页框的起始地址。
在Linux中这样转换的 这里我们知道一个变量的地址是变量的起始地址那么从虚拟地址转化成物理地址我们只能找到一个物理地址但是如果是int那么它是用4个字节存储的那么他是如何读取的呢
这里我们知道每一个变量都有一个类型在读取数据是就识别出了它的类型在它找到物理地址时会加上类型大小对应的偏移量进行读取。 在了解地址空间后我们知道线程的资源分配全部都是由地址空间来的而所有的代码于数据都是地址空间通过页表的映射在物理内存中找到的。 所以线程分配资源的本质就是分配地址空间。 线程的优缺点
线程比进程要更轻量化 创建和释放更加轻量化 创建线程只需要创建一个PCB而进程不但要创建PCB还要创建地址空间页表将地址空间通过页表于物理地址进行映射等。 切换更加轻量化 在线程切换时只需要改变CPU寄存器中对应线程的的上下文内容于数据但是效率主要来自于一个存储常用数据cache切换线程时并不会改变该cache而进程会将其清空从头慢慢开始。 我们知道进程有时间片线程也有时间片。 在OS中并不会为创建的新线程重新赋予时间片而是瓜分同一进程的时间片时间片也是资源会分配给每个线程。 那OS是如何区分是进程切换还是线程进行切换的呢 task_struct是可以标识这里在进程中的线程是有主次之分的在进程刚开始的时候的task_struct时主线程其他创建的是副线程。在主线程的task_struct里记录着进程的时间片于给线程的时间片在其他副线程中记录着线程的时间片当线程的时间片结束主线程中记录进程的时间片会减去线程的时间这是如果为0则进行进程的切换否则进行线程的切换。 线程的优点 创建一个新线程的代价要比创建一个新进程小得多 与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多 线程占用的资源要比进程少很多 能充分利用多处理器的可并行数量 在等待慢速I/O操作结束的同时程序可执行其他的计算任务 计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现不是线程越多越合适一般有多少个CPU就创建多少个线程I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。 线程的缺点 性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。 健壮性降低 编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。 缺乏访问控制 进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。 编程难度提高 编写与调试一个多线程程序比单线程程序困难得多 线程异常 单个线程如果出现除零野指针问题导致线程崩溃进程也会随着崩溃 线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该 进程内的所有线程也就随即退出 线程的用途 合理的使用多线程能提高CPU密集型程序的执行效率 合理的使用多线程能提高IO密集型程序的用户体验如生活中我们一边写代码一边下载开发工具就是 多线程运行的一种表现 Linux线程 VS 进程 进程和线程 进程是资源分配的基本单位 线程是调度的基本单位 线程共享进程数据但也拥有自己的一部分数据: 线程ID 一组寄存器 (线程的上下文)栈 errno 信号屏蔽字 调度优先级 进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程 中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境: 文件描述符表 即一个线程打开一个文件所有线程都打开了每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数) 当前工作目录 用户id和组id 线程控制
创建线程
在Linux中复用了进程的PCB来对线程进行描述于管理因此在Linux中没有明确的线程的概念只有轻量级进程的概念。因此在Linux中不会直接提供线程的系统调用只会给我们提供轻量级的系统调用!
但由于我们用户需要线程的接口去创建线程。
因此一些大佬们专门创建了一个应用层的pthread线程库对轻量级进程接口进行封装为用户提供直接创建线程的接口。pthread线程库是一个第三方库几乎所以的Linux平台都默认自带这个库 功能创建一个新的线程 参数 thread:输出型参数创建线程成功返回线程ID。 attr:设置线程的属性attr为nullptr表示使用默认属性 start_routine:是个函数指针返回值为void* 参数也为void*),线程启动后要执行的函数里面的内容arg:传给线程启动函数的参数为start_routine指向函数的参数。不需要参数为nullptr 返回值成功返回0失败返回错误码 #includeiostream
#includeunistd.h
#includepthread.husing namespace std;void* thread(void* args)
{while(true){coutnew thread:endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid,nullptr,thread,nullptr);while(true){coutmain threadendl;sleep(1);}return 0;
}编译
g -o thread thread.cc -stdc11 这里我们发现在编译时报错。
这里是因为pthread_create是第三方库的接口不是系统调用。g在编译时只会链接自己的C/C库这里因为Linux已经将pthread库加载到了指定路径下因此我们只需要告诉g链接那个库就可以了。
我们要这样编译
g -o thread thread.cc -l pthread -stdc11 这里我们还可以通过指令查看执行流
ps -aL LWP(Light Weight Process)就是轻量级进程的ID用来标识轻量级进程可以看到两个轻量级进程的PID相等所以他们属于同一个进程。这里有一个线程的LWP和PID相等这里我们把这个线程叫做主线程另一个就是新创建的线程。之前说的OS调度一个进程可以认为单进程单执行流它调度的基本单位看的是pid也可以是LWP因为它两相等。现在我们认为线程调度时OS看的是LWP。LWP和PID的关系 PID是对进程标识相同进程具有相同的PID。LWP标识轻量级进程它们的标识不同。OS真正调用时使用的时LWP 注在同一进程里所有线程的PID都相等。 验证线程共享全局变量
#includeiostream
#includeunistd.h
#includepthread.husing namespace std;int val100;void* thread(void* args)
{while(true){printf(new thread: getpid:%d val%d val%p\n,getpid(),val,val);sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid,nullptr,thread,nullptr);while(true){printf(main thread: getpid:%d val%d val%p\n,getpid(),val,val);val;sleep(1);}return 0;
} 验证一个线程出异常整个进程崩溃
#includeiostream
#includeunistd.h
#includepthread.husing namespace std;int val100;void* thread(void* args)
{while(true){sleep(5);int a1;a/0;}
}int main()
{pthread_t tid;pthread_create(tid,nullptr,thread,nullptr);while(true){printf(main thread: getpid:%d val%d val%p\n,getpid(),val,val);val;sleep(1);}return 0;
}获取线程ID
常见的获取线程ID的方式有两种 通过创建线程pthread_create函数的第一个输出型参数获得。通过调用pthread_self函数获得。 这里pthread_self函数那个线程调用就获取那个线程的ID类似于调用getpid()函数。 代码主线程调用pthread_create函数通过输出型参数获取并打印在创建的新线程中调用pthread_self函数获得线程ID。
#includeiostream
#includeunistd.h
#includepthread.husing namespace std;void* thread(void* args)
{while(true){printf(new thread: new thread tid%p\n,pthread_self());//将线程ID以16进制打印出来sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid,nullptr,thread,nullptr);while(true){printf(main thread: create new thread tid%p\n,tid);//将线程ID以16进制打印出来sleep(1);}return 0;
}用这里我们用pthread_self函数获取的线程ID于内核的LWP的值不同pthread_self函数获得的是用户级原生线程库的线程ID而LWP是内核的轻量级进程ID他们之间是1:1。
线程等待
这里创建完线程我们并不知道是那个线程先进行。但是我们可以确定的是主线程最后退出因为新线程是在主线程中创建的主线程要对创建的新线程进行管理所以最后退出。 因此一个线程被创建出来这个线程如进程一般需要主线程进行等待如果主线先退出会出现类似于进程中的僵尸进程的情况造成内存泄漏。这里线程退出类似于进程的退出它的空间并没有被完全的释放。 功能 指定主线程等待那个线程 参数 thread:等待线程ID用户级别的retval:接收线程退出时的返回值若不使用可以为nullptr。 返回值 成功返回0失败返回错误码。 #includeiostream
#includeunistd.h
#includepthread.husing namespace std;void* thread(void* args)
{int cnt5;while(cnt--){coutnew thread: cntendl;sleep(1);}return (void*)101;
}int main()
{pthread_t tid;pthread_create(tid,nullptr,thread,nullptr);void* retval;pthread_join(tid,retval);主线程等待新线程退出通过retval获取线程的返回值coutmain thread quit... retval:(long long int)retvalendl;return 0;
} 注主线程等待的时候默认是阻塞等待。
线程终止
1.return返回
在线程中使用return代表当前进程退出但是在main函数中使用代表整个进程退出也就是说只要主线程退出那么整个资源就会被释放而其他线程会因为没有资源自然而然的也退出了。
2.pthread_exit函数 功能终止调用该函数的线程。 参数retval线程退出时的退出码信息。 说明 该函数无返回值跟进程一样线程结束时无法返回它的调用者自身。pthread_exit或者return返回指针所指向的内存单元必须在全部变量或是用malloc分配的不能在线程函数的只能上分配因为当其他线程得到这个返回指针时指针指向的空间已经被释放了。exit函数的作用时终止整个进程任何一个线程调用exit函数就代表着整个进程终止。 #includeiostream
#includeunistd.h
#includepthread.husing namespace std;void* thread(void* args)
{int cnt5;while(cnt--){coutnew thread: cntendl;pthread_exit((void*)101);//终止线程。这里的参数可以理解为线程的退出码可以被pthread_join的第二个参数接收sleep(1);}return (void*)101;
}int main()
{pthread_t tid;pthread_create(tid,nullptr,thread,nullptr);void* retval;pthread_join(tid,retval);主线程等待新线程退出通过retval获取线程的返回值coutmain thread quit... retval:(long long int)retvalendl;return 0;
} 3.pthread_cancel 功能取消指定线程类似于kill。被取消的线程的错误码返回值为-1。 参数取消线程的ID。 #includeiostream
#includeunistd.h
#includepthread.husing namespace std;void* thread(void* args)
{int cnt5;while(cnt--){coutnew thread: cntendl;sleep(1);}return (void*)101;
}int main()
{pthread_t tid;pthread_create(tid,nullptr,thread,nullptr);sleep(1);//保证线程已创建成功pthread_cancel(tid);//取消线程void* retval;pthread_join(tid,retval);主线程等待新线程退出通过retval获取线程的返回值coutmain thread quit... retval:(long long int)retvalendl;return 0;
}线程ID的深入理解
pthread_create函数会产生一个线程这里和进程一样也需要一个属性来标识线程这里我们叫做线程的ID存放在第一个参数指向的地址中这里线程的ID于内核中的LWP不是一回事的。内核中LWP属于进程调度的范畴因为线程是轻量级进程是操作系统调度器的最小单位。Linux不提供真正的线程只提供轻量级进程也就意味着操作系统只需要对内核执行流LWP进行管理而供用户使用的线程接口等其他数据应该由线程库自己管理。因此管理线程时的“先描述再组织”就应该在线程库里进行。这里线程库是一个动态库加载到地址空间的共享区中每个线程都有自己私有的栈其中主线程采用的栈是进程地址空间中原生的栈而其余线程采用的栈就是在共享区中开辟的。除此之外每个线程都有自己的struct pthread当中包含了对应线程的各种属性每个线程还有自己的线程局部存储当中包含了对应线程被切换时的上下文数据。每一个新线程在共享区都有这样一块区域对其进行描述因此我们要找到一个用户级线程只需要找到该线程内存块的起始地址然后就可以获取到该线程的各种信息。 上面我们所用的各种线程函数本质都是在库内部对线程属性进行的各种操作最后将要执行的代码交给对应的内核级LWP去执行就行了也就是说线程数据的管理本质是在共享区的。pthread_t到底是什么类型取决于实现但是对于Linux目前实现的NPTL线程库来说线程ID本质就是进程地址空间共享区上的一个虚拟地址同一个进程中所有的虚拟地址都是不同的因此可以用它来唯一区分每一个线程。所谓线程ID可以理解为每个新线程在库当中的内存位置的起始地址线程控制块的起始地址。
#includeiostream
#includeunistd.h
#includepthread.husing namespace std;void* thread(void* args)
{int cnt5;while(cnt--){printf(new thread tid:%p\n,pthread_self());sleep(1);}return (void*)101;
}int main()
{pthread_t tid;pthread_create(tid,nullptr,thread,nullptr);while(1){printf(main thread tid:%p\n,pthread_self());sleep(1);}return 0;
}