线程简介
1.什么是线程
- 线程是一个进程内部的控制序列。在Linux环境下的线程本质仍是进程,也叫作轻量级进程,为了让进程完成一定的工作,进程必须至少包含一个线程。
- 在CPU眼中,看到的PCB都要比传统的进程更加轻量化。在CPU调度的时候,是以 LWP(light weight process:轻量级进程ID)为标识符表示特定一个执行流。
- 线程是操作系统调度的基本单位,而进程是承担分配系统资源的基本实体。
- 线程是进程的一个执行流,线程在进程的内部运行,本质是在进程地址空间内运行,拥有该进程的一部分资源。
可以通过ps -aL (a--all L--轻量级进程选项) 查看轻量级进程。
线程一旦被创建,几乎所有的资源都是被所有线程共享的,但线程也有自己私有的资源。
- 私有PCB属性。
- 私有上下文结构 。
- 每一个线程都要有自己独立的栈结构 。
线程的优缺点
优点:
- 线程更加轻量,多线程之间共享进程虚拟地址空间,意味着多线程之间共享数据
- 操作系统调度线程更加方便,多线程切换比多进程更快
- 创建一个新线程的代价要比创建一个新进程小得多
- 能充分利用多处理器的可并行数量
- 多线程程序适合大量运算的进程,例如计算密集型程序
缺点:
- 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器,如果计算密集型线程的数量比可用的处理器多,那么可能会有较大性能损失,性能损失只增加了额外的同步和调度开销,而可用资源不变。
- 健壮性降低:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器,如果计算密集型线程的数量比可用的处理器多,那么可能会有较大性能损失,性能损失只增加了额外的同步和调度开销,而可用资源不变。
- 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编写难度提高:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
2.原生线程库
线程接口并不是操作系统给我们直接提供的系统调用 ,因为linux并没有所谓的真正线程,只有轻量级进程,所以linux只能给我们提供轻量级进程的接口,操作系统在用户和系统调用之间提供一个用户级线程库(pthread库)任何linux操作系统都默认携带这个库,我们称为原生线程库。
原生线程库的位置
线程控制
获取线程号(pthrad_self)
include <pthread.h>
pthread_t pthread_self(void);
功能:
获取线程号。
参数:
无
返回值:
调用线程的线程 ID 。
创建线程(pthread_create)
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg );
功能:
创建一个线程。
参数:
thread:线程标识符地址。
attr:线程属性结构体地址,通常设置为 NULL。
start_routine:线程函数的入口地址。
arg:传给线程函数的参数。
返回值:
成功:0
失败:非 0
参考程序:
#include<iostream>
#include<cassert>
#include<pthread.h>
#include<unistd.h>
using namespace std;
void* thread_routine(void* args){
while(true){
cout<<"我是新线程,我正在执行"<<endl<<"-----------"<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
int n=pthread_create(&tid,nullptr,thread_routine,(void*)"thread one");
while(true){
cout<<"我是主线程,我正在运行!"<<endl;
sleep(1);
}
return 0;
}
运行结果:
终止线程(pthread_exit)
#include<pthread.h>
void pthread_exit(void *retval);
功能:
将单个线程退出
参数:value_ptr是函数的返回代码,通常填nullptr
线程等待(pthread_join)
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:
等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。
参数:
thread:被等待的线程号。
retval:用来存储线程退出状态的指针的地址。
返回值:
成功:0
失败:非 0
参考程序:
#include<iostream>
#include<cstdio>
#include<pthread.h>
#include<string>
#include<unistd.h>
using namespace std;
void* start_routine(void* args){
long long ret=(long long)args;
sleep(1);
return (void*)ret; //传出参数
}
int main()
{
pthread_t id;
pthread_create(&id,nullptr,start_routine,(void*)1234);
void* ret=nullptr;
pthread_join(id,&ret); //获取start_routine返回值
cout<<"join return:"<<(long long)ret<<endl;
return 0;
}
线程取消(pthread_cancel)
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能:
杀死(取消)线程
参数:
thread : 目标线程ID。
返回值:
成功:0
失败:出错编号
线程分离(pthread_detach)
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能:
使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。
参数:
thread:线程号。
返回值:
成功:0
失败:非0
设置线程分离后,线程一旦终止就立刻回收它占用的所有资源, 一旦线程处于分离状态,就不能再使用 pthread_join()来获取其终止状态。
对于线程ID,及线程为什么有独立的栈结构的思考。
一个进程的所有信息包含在task_struct数据结构里。而关于线程属性的结构体在哪里呢?
动态库是在程序连接时通过页表映射到虚拟地址空间的共享区。
因为linux没有正在意义上的线程,在调用pthread_create时,底层上使用的是的clone()函数。
创建一个线程时,先在pthread库中创建一个结构体(包含线程的属性),实现对于线程的控制,每一个结构体都会在内核对应一个轻量级进程的执行流。
用户所关心的线程属性在库中,内核提供线程执行流的调度,在linux中用户级线程:内核轻量级进程=1:1
创建线程时,会在库中创建线程控制块(包含线程ID,线程栈结构,线程的局部存储),创建完成后,继续帮我们调用clone(),创建轻量级进程,将创建好的线程回调方法,私有栈参数传递给clode(),所以线程一旦创建完成,便会依赖原生线程库,在新线程调度时,使用的临时数据便会压入clone()函数参数的child_stack指针指向的栈空间,所以线程ID就是在共享线程块的起始地址,这种解决方案被称为用户级线程,主线程的栈结构使用的是地址空间的栈,其他线程使用的是库当中栈结构。