Linux:线程概念及线程控制

Linux线程与进程基本概念
  • 概念
    在传统操作系统中,PCB就是进程,控制程序的运行,线程有TCB,但是在Linux下,因为线程是通过进程的PCB描述实现的,因此Linux下的PCB实际上是一个线程,并且因为这些线程共用同一个虚拟地址空间,因此也把Linux下的线程称为轻量级线程,相较于传统PCB更加轻量化
    即进程是一个运行中的程序,操作系统会创建一个pcb用来描述进程,并且分配资源,通过pcb来调度运行这个程序,而线程是一个进程中的执行流,但是Linux下实现进程中的执行流时,使用了pcb实现,因此说Linux下的线程是一个轻量级进程,因为同一个进程中的线程共用线程分配的资源,而进程是一个线程组,系统在运行程序,分配资源是分配给线程组的,进程就像是一个工厂,是资源分配的基本单位,而线程就像是工厂中的工人,是CPU调度的基本单位;
    线程是在进程内部运行的,即在进程的地址空间内运行;
    如图:
    在这里插入图片描述
    CPU看到的虽然还是PCB,但是比传统的进程更加的轻量化,所有的线程指向同一块虚拟地址空间,虚拟地址在查找页表时,将虚拟地址当成偏移量俩进行查找,在访问期间,如果目标资源不在内存,会触发缺页中断,这时将页面写入,然后建立映射关系(注意:缺页中断会使效率降低),
  • 多进程与多线程的优缺点分析
    多线程优点
    (1)因为共用同一块虚拟地址空间,因此通信更加灵活(能实现进程间通信就能实现线程间通信,并且线程全局、传参也可以)
    (2)线程的创建和销毁成本更低()
    (3)线程的切换调度成本低
    多进程优点
    (1)稳定、健壮性高—适用于对主程序安全稳定性要求更高的场景,例如shell/服务器
    多线程缺点
    线程间缺乏访问控制,某些系统调用以及异常是针对整个进程产生效果的(例如exit)
  • 多进程与多线程进行多任务处理的优势在哪里?
    1、IO密集型程序(在程序中大量进行IO操作,IO操作:IO等待+数据拷贝)
    2、CPU密集型程序(在程序中不断进行数据运算)
    一般线程最好创建CPU核数+1个
  • 线程的独有和共享
    线程共享进程数据,但是也独有自己的一部分数据
    独有的数据有:
    (1)线程ID
    (2)硬件上下文
    (3)
    (4)errno
    (5)信号屏蔽字
    (6)调度优先级(每个线程都是一个PCB)
    因为线程要进行调度,要调度就要有硬件上下文;线程在运行期间有可能产生临时变量,有可能有函数函数调用,所以必须每个线程有一个栈结构,否则会相互干扰。
    栈结构和上下文体现线程可以被调度
    共享的数据有:
    (1)文件描述符表
    (2)每个信号的处理方式(默认、忽略、自定义)
    (3)当前工作目录
    (4)用户id和组id
    注意:单进程就是一个线程执行流的进程
    多进程/多线程执行多任务的并发处理的共同优势:
    (1)IO密集型程序(程序中大部分的工作都是进行IO):可以在一个执行流中发起一个IO,避免了只有一个执行流时,只有一个IO完成才能进入下一个的情况,提高了IO效率,并且压缩了IO等待时间;
    (2)CPU密集型程序(程序中大部分的工作是进行数据运算):在CPU密集型程序中,执行流的创建并不是越多越好,多了反而会提高CPU调度的成本,CPU密集型程序中,线程的创建最好是CPU核心数+1;
线程控制

操作系统并没有给用户直接提供创建一个线程的接口,因此就封装了一套第三方POSIX 线程库用于线程控制,是用户模拟的(用户线程就是在用户态中创建的线程),绝大多数函数的名字时以“pthread_t”开头的。
要使用这些函数库,要引入头文件<pthread.h>,链接这些线程库必须使用编译器命令"-lpthread"选项

  • 创建线程
    pthread_create函数,它的函数原型是:
 #include <pthread.h>
 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

其中参数thread表示返回线程ID;attr表示设置线程的属性,一般使用NULL表示默认属性;start_routine是一个函数地址,是线程的入口函数;arg是线程入口函数start_routine的参数
它的返回值是成功返回0,失败返回错误码
例如:

#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
void* rout(void* arg)
{
    while(1)
    {
        printf("I am thread 1:%d\n",getpid());
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int ret;
    char* ptr="thread 1";
    ret=pthread_create(&tid,NULL,rout,(void*)ptr);
    if(ret!=0)
    {
        printf("pthread_create error :%s\n",(char*)rout);
    }
    while(1)
    {
        printf("I am main thread:%d\n",getpid());
        sleep(1);
    }
    return 0;
}

结果就是:

[Daisy@localhost test_2019_11_2_1]$ ./pthread 
I am main thread:4336
I am thread 1:4336
I am main thread:4336
I am thread 1:4336
I am main thread:4336
I am thread 1:4336
^C

这时有两个线程在跑,一个执行流,一个线程1,两个线程的pid一样,说明在同一个进程中
此时如果要是将代码改为:

#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <time.h>
void* rout(void* arg)
{
    while(1)
    {
        srand((unsigned long)time(NULL));
        printf("I am thread 1:%d\n",getpid());
        int time=rand()%10;
        sleep(1);
        int a=10/0;
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,rout,(void*)"thread 1");
    pthread_create(&tid,NULL,rout,(void*)"thread 2");
    pthread_create(&tid,NULL,rout,(void*)"thread 3");
    pthread_create(&tid,NULL,rout,(void*)"thread 4");
    pthread_create(&tid,NULL,rout,(void*)"thread 5");
    while(1)
    {
        printf("I am main thread:%d\n",getpid());
        sleep(1);
    }
    return 0;
}

此时应该出异常

[Daisy@localhost test_2019_11_2_1]$ ./pthread 
I am main thread:4552
I am thread 1:4552
I am thread 1:4552
I am thread 1:4552
I am thread 1:4552
I am thread 1:4552
I am main thread:4552
浮点数例外(吐核)

此时是某个线程出异常,主线程没有问题,但是一旦出异常,整个进程都退出,也就是一个线程出异常,整个进程都退出,因此线程健壮性不好。
可以使用ps -efL来查看轻量级进程的信息
例如:

[Daisy@localhost ~]$ ps -efL | head -n1 && ps -efL | grep pthread
UID         PID   PPID    LWP  C NLWP STIME TTY          TIME CMD
Daisy      3050   2803   3050  0    6 19:49 pts/1    00:00:00 ./pthread
Daisy      3050   2803   3051  0    6 19:49 pts/1    00:00:00 ./pthread
Daisy      3050   2803   3052  0    6 19:49 pts/1    00:00:00 ./pthread
Daisy      3050   2803   3053  0    6 19:49 pts/1    00:00:00 ./pthread
Daisy      3050   2803   3054  0    6 19:49 pts/1    00:00:00 ./pthread
Daisy      3050   2803   3055  0    6 19:49 pts/1    00:00:00 ./pthread
Daisy      3134   2945   3134  0    1 19:52 pts/2    00:00:00 grep --color=auto pthread

显示了LWP信息,LWP是CPU调度最重要的ID字段,叫做轻量级进程ID,实际上是线程组ID,LWP是不一样的,此时杀掉一个线程就会所有的线程退出;而tid是线程空间在虚拟地址空间中的首地址,tgid是在外边查看到的进程id,实际上是pcb中的;

  • tid
    此时如果执行这个代码打印出tid,例如:
#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <time.h>
void* rout(void* arg)
{
    while(1)
    {
        printf("I am thread 1:%d\n",getpid());
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,rout,(void*)"thread 1");
    while(1)
    {
        printf("I am main thread:%lu\n",tid);
        sleep(1);
    }
    return 0;
}

此时运行后的结果就是:

发现此时打印出的tid是一个很长的数字,并不是进程ID,因为pthread_create是用户级线程库的函数,不是系统调用的函数,可以这样理解,内核的LWP就相当于身份证号,而这个tid相当于自己工作的工号,这里的线程ID与内核的LWP是一一对应的,这个tid就是线程地址空间的地址。

  • 线程等待
    在创建好新线程后,必须让主线程去等待新线程,倘若不等待,已经退出的线程的空间没有释放,仍然在进程地址空间内,创建新的线程不会复用刚才退出线程的地址空间,就会造成类似于僵尸进程的问题,也会造成内存泄漏
    等待线程的函数是pthread_join,它的函数原型是:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

其中参数thread表示线程tid,retval表示获取退出码,它是一个二级指针,线程执行的函数返回值是void*,因此获得这个一级指针要传一个二级指针;返回值是成功返回0,失败返回错误码,调用该函数的线程将挂起等待,直到id为thread的线程终止。
thread线程以不同的方法终止,通过pthread_join得到的状态是不同的,总结
(1)如果thread线程通过return返回,retval所指向的单元中存放的是thread线程函数的返回值
(2)如果thread线程被别的线程调用pthread_cancel异常终止,retval所指向的单元中存放的是常数PTHREAD_CANCELED(代表该线程被取消)
(3)如果thread线程是自己调用pthread_exit终止,retval中存放的是传给pthread_exit的参数
(4)如果thread线程的终止状态不感兴趣,可以传NULL给retval参数
例如:

#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <time.h>
void* rout(void* arg)
{
    while(1)
    {
        printf("I am thread 1:%s\n",(char*)arg);
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,rout,(void*)"thread 1");
    printf("I am main thread:%lu\n",tid);
    pthread_join(tid,NULL);//线程等待,对终止状态不感兴趣
    return 0;
}

此时创建了新线程,主线程在以阻塞方式等待新线程退出,但是新线程没有退出,因此它的结果就是:

[Daisy@localhost test_2019_11_2_1]$ ./pthread 
I am main thread:139972940277504
I am thread 1:thread 1
I am thread 1:thread 1
I am thread 1:thread 1
I am thread 1:thread 1
I am thread 1:thread 1
^C

此时创建完新线程之后,主线程就一直等待新线程退出,新线程在一直运行,线程没有非阻塞式等待。

  • 线程终止
    1、通过return终止
    例如实现代码:
#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <time.h>
void* rout(void* arg)
{
    while(1)
    {
        printf("I am thread 1:%s\n",(char*)arg);
        sleep(1);
        break;
    }
    return (void*)11;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,rout,(void*)"thread 1");
    printf("I am main thread:%p\n",tid);
    void* ret;
    pthread_join(tid,&ret);//线程等待,ret保存线程处理函数的返回值
    printf("ret:%d\n",(long)ret);
    return 0;
}

运行出来的结果是:

[Daisy@localhost test_2019_11_2_1]$ ./pthread 
I am main thread:0x7f773fd9a700
I am thread 1:thread 1
ret:11

这个就获取到了退出码11,进程退出,新线程退出,导致进程退出,也导致主线程退出,根本没有返回的机会,没有调用pthread_join的机会,因此我们默认线程退出是默认线程正常运行,没有出异常(因为出异常主线程也没有办法,主线程已经退出了)只关心结果是否正确,因此新线程退出没有办法拿到退出信号,只能返回退出码
2、pthread_exit函数
它的函数原型是:

#include <pthread.h> 
void pthread_exit(void *retval);

它的用法与return来终止线程的用法一样,例如:

#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <time.h>
void* rout(void* arg)
{
    while(1)
    {
        printf("I am thread 1:%s\n",(char*)arg);
        sleep(1);
        break;
    }
//    return (void*)11;
    pthread_exit((void*)3);//线程终止
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,rout,(void*)"thread 1");
    printf("I am main thread:%p\n",tid);
    void* ret;
    pthread_join(tid,&ret);//线程等待,以阻塞方式等待
    printf("ret:%d\n",(long)ret);
    return 0;
}

此时的运行结果就是:

[Daisy@localhost test_2019_11_2_1]$ ./pthread 
I am main thread:0x7f3df62d7700
I am thread 1:thread 1
ret:3

可以看到等待到的新线程退出码是3,main函数中调用return代表进程退出。
注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者使用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。

  • 线程取消
    pthread_cancel函数,作用是取消一个执行中的线程,它的函数原型是:
#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数thread是线程ID(tid),表示要取消哪个线程,返回值是成功返回0,失败返回错误码,例如:

#include<stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
void* thread_run(void* arg)
{
    int count=5;
    while(count--)
    {
        printf("I am a new thread,tid is %p\n",pthread_self());//pthread_self用来获取当前线程的线程ID
        sleep(1);
    }
    pthread_exit((void*)111);
}

int main()
{
    pthread_t tid;
    int num=1;
    pthread_create(&tid,NULL,thread_run,(void*)&num);
    pthread_cancel(tid);//取消新线程
    printf("main thread get a new thread tid:%p\n",tid);
    void *ret;
    pthread_join(tid,&ret);
    printf("ret:%d\n",(long)ret);
    return 0;
}

它的运行结果就是:

[Daisy@localhost test_2019_11_3_1]$ ./mythread 
main thread get a new thread tid:0x7f42eae86700
I am a new thread,tid is 0x7f42eae86700
ret:-1

退出码是-1,这个是一个宏,就是PTHREAD_CANCELED宏,表示线程是被取消的,将这个pthread_cancel放在新线程的函数内存取消也可以,但是一般不这么做。(注意:pthread_self函数的作用是获取当前线程ID)
例如:

#include<stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
pthread_t main_id;
void* thread_run(void* arg)
{
    int count=5;
    while(count--)
    {
        printf("I am a new thread,tid is %p\n",pthread_self());//pthread_self用来获取当前线程的线程ID
        sleep(1);
       int ret=pthread_cancel(main_id);//能不能让新线程取消主线程
       printf("ret:%d,string:%s\n",ret,strerror(ret));//使用ret获得返回码,使用strerror打印错误信息
    }
    pthread_exit((void*)111);
}

int main()
{
    main_id=pthread_self();//获取主线程ID
    pthread_t tid;
    int num=1;
    pthread_create(&tid,NULL,thread_run,(void*)&num);
    printf("main thread get a new thread tid:%p\n",tid);
    void *ret;
    pthread_join(tid,&ret);
    printf("ret:%d\n",(long)ret);
    return 0;
}

此时运行结果是:

[Daisy@localhost test_2019_11_3_1]$ ./mythread 
main thread get a new thread tid:0x7f2a6e0f7700
I am a new thread,tid is 0x7f2a6e0f7700
ret:0,string:Success
I am a new thread,tid is 0x7f2a6e0f7700
ret:3,string:No such process
I am a new thread,tid is 0x7f2a6e0f7700
ret:3,string:No such process
I am a new thread,tid is 0x7f2a6e0f7700
ret:3,string:No such process
I am a new thread,tid is 0x7f2a6e0f7700
ret:3,string:No such process

在运行时打开另一终端,执行命令得到:

[Daisy@localhost ~]$ while :; do ps -aL | grep mythread; sleep 1;echo "######################";done
######################
######################
######################
  4066   4066 pts/1    00:00:00 mythread
  4066   4067 pts/1    00:00:00 mythread
######################
  4066   4066 pts/1    00:00:00 mythread <defunct>
  4066   4067 pts/1    00:00:00 mythread
######################
  4066   4066 pts/1    00:00:00 mythread <defunct>
  4066   4067 pts/1    00:00:00 mythread
######################
  4066   4066 pts/1    00:00:00 mythread <defunct>
  4066   4067 pts/1    00:00:00 mythread
######################
  4066   4066 pts/1    00:00:00 mythread <defunct>
  4066   4067 pts/1    00:00:00 mythread

可以看出新线程可以取消主线程,但是进程没有退出,主线程不运行了,新线程就变成了类似于僵尸进程的状态,因为没有人调用join,最终交给bash回收,如果主线程调用return就代表进程退出,就会全部退出。

  • 线程分离
    比如将新线程创建出之后,主线程阻塞等待,但是此时主线程想要做其他事情,但是线程没有非阻塞轮询等待,此时就用到了线程分离。
    新线程退出后,资源自动被系统回收,不关心新线程返回结果,可以进行线程分离,函数pthread_detach,它的函数原型是:
#include <pthread.h>
int pthread_detach(pthread_t thread);

参数thread表示线程id(tid)
例如:

#include<stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void* thread_run(void* arg)
{
    pthread_detach(pthread_self());//线程分离
    int count=5;
    while(count--)
    {
        printf("I am a new thread,tid is %p\n",pthread_self());//pthread_self用来获取当前线程的线程ID
        sleep(1);
}
int main()
{
    pthread_t tid;
    int num=1;
    pthread_create(&tid,NULL,thread_run,(void*)&num);
    sleep(1);
    printf("main thread get a new thread tid:%p\n",tid);
   int ret= pthread_join(tid,NULL);
   printf("ret:%d,%s\n",ret,strerror(ret));
    return 0;
}

此时的运行结果就是:

[Daisy@localhost test_2019_11_3_1]$ ./mythread 
I am a new thread,tid is 0x7fbff10e6700
main thread get a new thread tid:0x7fbff10e6700
ret:22,Invalid argument

主线程等待,返回一个退出码22,显示非法参数,此时等待的新线程已经分离,但是如果去掉等待之前的sleep(1),此时运行结果就是:

[Daisy@localhost test_2019_11_3_1]$ ./mythread 
main thread get a new thread tid:0x7f5a5ab33700
I am a new thread,tid is 0x7f5a5ab33700
I am a new thread,tid is 0x7f5a5ab33700
I am a new thread,tid is 0x7f5a5ab33700
I am a new thread,tid is 0x7f5a5ab33700
I am a new thread,tid is 0x7f5a5ab33700
^C

此时发现主线程一直在等待新线程退出,新线程并没有分离,原因就是没有sleep(1),在创建新线程之后,一共有两种执行流,如果主线程先运行,主线程立马等待,此时新线程还没有被分离,主线程一直在等待新线程退出,因此分离还有另一种方式,例如:

#include<stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void* thread_run(void* arg)
{
    int count=5;
    while(count--)
    {
        printf("I am a new thread,tid is %p\n",pthread_self());//pthread_self用来获取当前线程的线程ID
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int num=1;
    pthread_create(&tid,NULL,thread_run,(void*)&num);
    pthread_detach(tid);//由主线程分离新线程
    printf("main thread get a new thread tid:%p\n",tid);
   int ret= pthread_join(tid,NULL);
    printf("ret:%d,%s\n",ret,strerror(ret));
    return 0;
}

在主线程中分离新线程,运行结果就是:

[Daisy@localhost test_2019_11_3_1]$ ./mythread 
main thread get a new thread tid:0x7fbd5df41700
ret:22,Invalid argument

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

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

pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中,该线程ID和前面说的线程ID不是一回事,前面所说的线程ID属于进程调度的范畴,因为线程是轻量级进程,是操作系统调度器的最小单位,因此需要一个数值来唯一表示该进程(可以理解前面的线程ID是LWP,就是操作系统来标志轻量级进程的id)
pthread_create函数的第一个参数指向一个虚拟内存单元,该内存单元的地址就是新创建线程的线程ID,属于NPTL线程库的范畴,线程库的后续操作就是根据该线程ID来操作线程的,线程库NPTL提供了pthread_self函数来获取线程自身的ID,(我们这里理解的线程ID就是pthread_t类型的id)。

对于Linux实现的NPTL实现而言,pthread_t类型的线程ID本质上是进程地址空间上的一个地址

如图:
在这里插入图片描述
主线程是独立的,动态库加载后,要把动态库本身全部信息映射到主线程堆、栈之间的共享区(mmap),mmap的过程与共享内存映射一样,动态库中不仅有代码,还有为维护线程创建的数据结构。
用户级别的动态库本身承担了线程的组织、管理工作,即每一个线程地址空间有struct_pthread(线程结构体),线程局部存储以及线程栈,实现“先描述,再组织”,可见每一个线程都有自己的私有栈;将struct_pthread、线程局部存储以及线程栈打包成一个整体,进行用户级别的线程描述。
源代码(github):
https://github.com/wangbiy/Linux2/commit/c38f0190d25f555abcbaa9f1cfeaae5acbdc17fe

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值