Linux线程(一)
一、线程相关概念?
1.什么是线程:
线程是一个程序内的一个执行路线。更准确的说是线程是“一个进程内的控制序列“
任意的进程至少有一个执行线程
线程在进程内部执行,本质是在进程地址空间内运行行
在Linux中,在CPU眼中,线程都要比进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源被合理的分配给每个执行流,就形成了线程流
线程是运行在进程中的,主要是用来进行调度和执行(抢占式)
进程主要是进行资源管理(管理内存、文件等)
多线程/多进程的应用场景:a.CPU密集型 b.相应UI界面
2.线程的优点
创建一个新线程的代价要比创建一个新进程小的多,创建一个进程有需要向操作系统申请PCB的内存,而新建一个线程只是和创建线程的进程共用一个虚拟地址空间。
与进程的切换相比,线程之间的切换需要操作系统的工作量少很多
线程占用的资源比进程占用的资源少很多
线程能充分利用多处理器的可并行数量
在等待慢速IO操作结束的同时,线程程序可执行其他的计算任务
计算机密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程上实现,线程大大提高计算效率
IO密集型应用,为了提高性能,将IO操作系统重叠,每个线程可以同时等待不同的IO操作
3.线程的缺点
性能的损失:一个很好被外部阻塞事件的计算密集型线程往往无法与其它线程共享一个处理器,如果计算机密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失是指增加了额外的同步和调度开销,而可用的资源不变
健壮性降低:编写多线程需要考虑更全面,更深入,在一个多线程的程序里,因时间分配上的细微差别或者共享了不应该共享的变量,从而导致不良影响的可能性变大,即线程之间是缺乏保护的。
缺乏访问控制:进程是访问控制的基本粒度,在一个线程内调用某些系统函数会对整个进程造成影响。
编写难度提高:编写与调试多线程程序比单线程程序困难的多
4.线程异常
单个线程如果出现除0,解引用空指针等导致线程崩溃,进程也会崩溃
线程是进程的执行分支,线程出现异常,类似于进程出现异常,触发信号机制,终止进程,由于进程终止,该进程内的所有线程都将被终止。
5.线程的用途
合理的使用多线程能提高CPU密集型程序的执行效率
合理的使用多线程能提高IO密集型程序用户的体验感
注意⚠️线程的数量不是越多越好,而是合理合适
二、进程VS线程
1.线程和进程的比较:
进程
线程
进程是资源分配的基本单位
线程是调度的基本单位
进程拥有自己的数据
线程是运行在一个进程内部,但是线程不仅共享进程的数据,还有线程自己独立的数据(线程ID、一组寄存器、栈、error、信号屏蔽字、调度优先级等)
每个进程都有自己的进程虚拟地址空间
运行在同一个进程下的多个线程共享同一个进程的虚拟地址空间(线程共享进程内的全局变量、函数方法、文件描述符表、每种信号的处理方式(默认处理方式或者自定义处理方式)、当前的工作目录、用户ID和组ID)
2.线程和进程的关系:
三、线程创建
1.线程创建:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
功能:创建一个线程
参数:pthread_t *thread:输出型参数,返回创建出的线程ID
const pthread_attr_t *attr:设置线程的属性,attr为NULL表示使用默认属性
void *(*start_routine) (void *):是个函数地址,表示线程创建成功后让线程去执行的函数(相当于回调函数)
void *arg:传给void *(*start_routine) (void *)的参数
返回值:创建线程成功返回0,失败返回错误码
错误检查:a.传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
b.pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
c.pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
#include
#include
#include
#include
#include
void *rout(void *arg) {
int i;
for( ; ; ) {
printf("I'am thread 1\n");
sleep(1);
}
} i
nt main( void )
{
pthread_t tid;
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
} i
nt i;
for(; ; ) {
printf("I'am main thread\n");
sleep(1);
}
}
2.线程ID和进程ID
在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现条件下,线程又被称为轻量级进程,每一个用户态的线程,在内核中都有一个相对应的调度实体,也有自己的进程描述符(task_struct结构体)
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程后,情况发生了变化,一个用户态的进程下管辖了N个用户态的线程,每个线程作为一个独立调度实体在内核态内都有自己的进程描述符,进程和内核的描述符就变成了1:N的关系,POSIX标准又要求进程内的所有线程调用getpid时返回相同的进程ID,所以为了解决这个问题Linux提出了线程组的概念
线程组:
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};
多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应,进程描述符结构体的内部中的pid,表面上看是进程的ID,其实不然,它对应的是线程ID;进程描述符结构体中的tgid含义是Thread Group ID,该值对应的是用户层面的进程ID
下面看一个现象:
可以看出上面运行的进程是多线程的,进程ID为28543,进程内有2个线程,线程ID分别为28543,28544。
从上面可以看出,a.out进程的ID为28543,下面有一个线程的ID也是28543,这不是巧合,不是巧合,不是巧合,不是巧合!
线程组内的第一个线程,在用户态被称为主线程(main thread),在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID的值设置成第一个线程的线程IDgroup_leader指针则指向自身,既主线程的进程描述符。所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程
/* 线程组ID等于线程ID,group_leader指向自身 */
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group)
至于至于线程组其他线程的ID则有内核负责分配,其线程组ID总是和主线程的线程组ID一致,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样
if ( clone_flags & CLONE_THREAD )
p->tgid = current->tgid;
if ( clone_flags & CLONE_THREAD ) {
P->group_lead = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}
3.线程之间的关系
线程和进程是不一样的,进程有父子进程的概念,但是线程内所有的线程都是平等的关系,即同一个线程组的线程,没有层次关系
4.线程ID和进程虚拟地址空间布局
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID
pthread_t pthread_self(void)
那么pthread_t到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。