linux --线程(一 ) 线程基本概念及线程管理

线程引言

问题:为什么有了进程的概念后,还要再引入线程呢?什么的系统应该选用多线程?

  • 从多任务操作来看,相比于多进程,多线程是一种非常"节俭"的多任务操作方式。因为在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据(独有和共享机制),并且启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
  • 线程间方便的通信机制。对不同的多个进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。但是,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改(涉及可重入、竞态条件等概念),有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
  • 除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
      1. 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
      2.使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
      3. 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

线程特征

线程的优点

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

线程的缺点

  • 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。换句话说就是线程在调度的时候是有开销的,如果大量的进程,会导致程序在频繁的切换,占用CPU去执行上下文信息,程序计数器等同步和调度开销。

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

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

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

线程异常

  • 单个线程如果出现除零,内存越界,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

线程具体概念

内核线程

  • 操作系统内核支持多线程调度与执行
  • 内核线程使用资源较少,仅包括内核栈和上下文切换时需要的保存寄存器内容的空间

轻量级进程( lightweight process,LWP )

  • 由内核支持的独立调度单元,调度开销小于普通的进程
  • 系统支持多个轻量级进程同时运行,每个都与特定的内核线程相关联

用户线程(我们编写多线程程序时实际写的都是用户线程)

  • 建立在用户空间的多个用户级线程,映射到轻量级进程后调度执行
  • 用户线程在用户空间创建、同步和销毁,开销较低
  • 每个线程具有独特的ID

线程基本概念

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

在这里插入图片描述

线程是什么?从内核角度去分析

线程就是创建出来的执行流,在内核当中创建了一个PCB, 在内核当中创建了-个task struct这样的结构体;
1.回顾创建子进程

  • fork:在内核当中以父进程的PCB为模板拷贝父进程PCB当中的内容创建一个PCB , PCB被内核使用双向链表管理起来
  • vfork:在内核当中以父进程的PCB为模板拷贝父进程PCB当中部分内容创建一个PCB ,子进程的PCB是指向父进程的虚拟地址空间的, PCB被内核使用双向链表管理起来;但是存在调用栈混乱的问题(由于使用同一虚拟内存空间,所有的调用函数都压栈在同一个栈中,势必在调用函数时造成混乱),故规定先调用子进程的调用函数,即让子进程先运行,进而在让父进程在运行

2.再看创建线程

  • pthread_ create函数:在内核当中创建一个PCB , PCB是指向父进程的虚拟地址空间的,内核会给创建出来的线程在虚拟地址空间当中的共享区开辟一段空间,保存线程独有的东西,所以由于线程的独有和共享属性,线程可以实现多任务调度与执行,且不存在调用栈混乱的问题。
  • 注意:fork,vfork都是系统调用,而pthread是c库函数,但是三者底层也都是调用的clone函数
  • 注意:Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。

在这里插入图片描述

进程与线程的比较总结

  • 线程空间不独立,有问题的线程会影响其他线程;进程空间独立,有问题的进程一 般不会影响其他进程
  • 创建进程需要额外的性能开销
  • 线程用于开发细颗粒度并行性,进程用于开发粗颗粒度并行性
  • 线程容易共享数据(全局变量和堆内存地址空间相互独立全局数据也会拷贝出单独一份),进程共享数据必须使用进程间通讯机制(共享内存,管道等)
  • 线程是操作系统调度的最小单位,进程是操作系统分配资源的最小单位
  • 注意:线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系

进程ID和线程ID

在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢?所以Linux内核引入了线程组的概念。

struct task_struct {
...
	pid_t pid;   //线程id
	pid_t tgid;   //线程组id
...
	struct task_struct *group_leader;//指向主线程的指针
...
	struct list_head thread_group;
...
};
  • 多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。
    1.进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;
    2.进程描述符中的tgid(线程组ID),含义是Thread Group ID,该值对应的是用户层面的进程ID。
    在这里插入图片描述

  • 线程组内的第一个线程,在用户态被称为主线程(main thread),在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,既主线程的进程描述符。所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。

  • 至于线程组其他线程的ID则有内核负责分配,其线程组ID总是和主线程的线程组ID一致,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样。

总结

  • 使用ps aux看出来的进程id就是tgid
  • 1.当当前程序只有一个执行流的时候,意味着当前程序只有一个
    线程,我们将执行main函数的线程,称之为主线程
    tgid(线程组id(进程id))等于主线程的id ( pid )
  • 2.当程序有多个执行流的时候.意味着当前程序有多个线程
    对于 主线程:执行main
    pid == tgid
    对于工作线程:新创建出来的执行流
    tgid:相当于进程号(线程组id ) ,是一定不会变的 ,标识当前的线程属于哪一个进程
    pid:相当于线程id,每一个线程id都是不同的

线程管理

线程创建,线程终止,线程等待
Linux系统下的多线程遵循POSIX线程库接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。所以要使用这些函数库,要通过引入头文<pthread.h>,而且链接这些线程函数库时要使用编译器命令的“-lpthread”选项。

线程创建

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*thread_ start)(void*), void *arg);
参数说明	:
thread:指向线程线程标识符的指针,和线程id并不是一回事,pthread_t ,是线程独有空间的首地址,通过这个标识符可以对当前的线程进行操作, 调用pthread_create作为出参返回给调用者
attr:线程属性,pthread_ attr_t 是一个结构体,这个结构体完成对新创建线程属性的设置;如果说,创建线程的时候,该参数被设置为NULL ,则认为采用默认的属性
	属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个		函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆	栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
        线程栈的大小
		线程栈的起始位置
		线程的分离属性
		线程的优先级调度属性
thread_ start:回调函数:线程入口函数地址,这个函数是void*返回值, void*参数
arg:给线程入口函数传递的参数的值
		不能传递临时变量
		可以传递一个堆上开辟的内存到线程入口函数当中去使用,但需要在线程入口函数结束的时候,将对上开辟的内存释放掉(防止内存泄露)
		void*传递任意的类型,包含自定的数据结构,类实例化指针对象
返回值:成功返回0;失败返回错误码常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。

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

typedef struct thread_info  //构造thread_info动态对象,作为线程函数参数传递给线程
{
    int thread_num_;
    thread_info()
    {
        thread_num_ = -1;//初始化
    }
}THREADINFO;

void* thread_start(void* arg)//回调函数,即线程入口函数
{
    THREADINFO* ti = (THREADINFO*)arg;
    while(1)
    {
        printf("i am new thread~~~~: %d\n", ti->thread_num_);
        sleep(1);
    }
    //对于传递进来的对上开辟的内存,可以在线程入口函数结束的时候,释放掉,不会导致程序有内存泄漏的风险
    delete ti;
    return NULL;
}

int main()
{
    pthread_t tid;
    int ret;
    int i = 0;
    for(; i < 4; i++)
    {
        THREADINFO* threadinfo = new thread_info();
        //堆上开辟的
        threadinfo->thread_num_ = i;
        ret = pthread_create(&tid, NULL, thread_start, (void*)threadinfo);//循环创建线程
        if(ret < 0)
        {
            perror("pthread_create");
            return 0; 
        }
    }

    while(1)
    {
        printf("i am main thread~~~\n");
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码(程序计数器及上下文信息)。
错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小

线程ID及进程地址空间布局

1.pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
2.前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
3.pthread_ create函数第一个参数指向一个虚拟内存单元(线程标识符),该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
4.线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:

pthread_t pthread_self(void);

pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
在这里插入图片描述

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit函数

功能:线程终止
void pthread_exit(void *value_ptr);    //谁调用谁退出
参数
	value_ptr:value_ptr不要指向一个局部变量。  
	返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配在虚拟地址空间的堆上的(共享的),不能在线程函数的栈上(独有的)分配,因为当其它线程得到这个返回指针时线程函数已经退出了,空间也就释放了。

另外注意:当主线程调用pthread_ exit退出的时候,进程是不会退出的,但是主线程的状态变成了Z,也就是说主线程变成了一个“僵尸线程”,工作线程的状态还是R/S

pthread_cancel函数

功能:取消一个执行中的线程
int pthread_cancel(pthread_t thread);  //知道线程ID时,可以指定任一一个线程退出
参数
	thread:线程ID(线程标识符)  
	返回值:成功返回0;失败返回错误码

线程等待

为什么需要线程等待?
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间(内存泄露问题)。

功能:等待线程结束
int pthread_join(pthread_t thread, void **value_ptr);
参数
	thread:线程ID
	value_ptr:它指向一个指针,后者指向线程的返回值
	返回值:成功返回0;失败返回错误码
	第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。
	这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。

调用该函数的线程将挂起(阻塞)等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
    在这里插入图片描述
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void* thread_start(void* arg)
{
    (void)arg;  //默认不关心参数
    printf("i am new thread\n");
    sleep(5);
    
    pthread_exit(NULL);//谁调用,谁终止
	
    //pthread_cancel(pthread_self());//将自己终止
    while(1)
    {
        sleep(1);
    }
    return NULL;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, thread_start, NULL); //属性为默认值
    
    sleep(5);
    //pthread_exit(NULL);   //主线程自己终止自己,会使主线程变成了一个“僵尸线程”,工作线程的状态还是R/S
    //pthread_cancel(tid);  //主线程来终止创建出来的线程
    pthread_join(tid, NULL);//等待回收终止线程的资源
    while(1)
    {
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

线程分离

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

换句话说:意味改变线程的joinable(可结合的)属性,变成detach(分离的),从而在线程退出的时候,不需要其他线程来回收该线程的资源,自动释放线程资源。

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

pthread_detach(pthread_self());
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run( void * arg )
{
	pthread_detach(pthread_self());//将默认可结合性属性改为分离属性
	printf("%s\n", (char*)arg);
	return NULL;
}
int main( void )
{
	pthread_t tid;
	if ( pthread_create(&tid, NULL, thread_run, "change arr") != 0 ) {
		printf("create thread error\n");
		return 1;
		}
	sleep(1);//很重要,要让线程先分离,再等待
	if ( pthread_join(tid, NULL ) == 0 ) { //分离属性设置失败就会调用此函数来回收资源
		printf("pthread change detach failed\n");
	}
	 else{
		printf("pthread change detach succesed\n"); //否则,分离成功,自动释放线程资源
	}
	return 0;
}

在这里插入图片描述

总结:线程的分离状态决定一个线程以什么样的方式来终止自己。线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算结束,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。

注意

  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
  • 如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

线程绑定

关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。

设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。

pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

进程方面的文章笔记----->

发布了55 篇原创文章 · 获赞 152 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_44785014/article/details/105385062