Linux--线程控制及线程标识的探索

本文详细探讨了Linux系统中线程的控制,包括线程的创建、等待、终止方法,如pthread_create、pthread_join、pthread_exit等。同时,文章深入解析了线程标识,讲解了线程ID在不同层面的表示,如pthread_t类型的线程ID和pid_t类型的轻量级进程ID。此外,还介绍了线程的分离及其影响。
摘要由CSDN通过智能技术生成

1 线程控制

1.1 线程的创建

1.通过pthread_create函数来创建线程
(1)函数原型:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

(2)参数解析:
①thread为线程id(是输出型参数,表示线程创建之后通过thread参数可以拿到线程的线程id);
attr表示线程的属性,默认可设为NULL;
start_routine为线程处理函数;
arg为线程处理函数的参数(表示对线程编号为thread的线程进行操作)。
②可以看到:线程处理函数的参数及返回值都是void*,因为C语言没有泛型编程,而线程处理的类型不一定相同,所以用void*接收任意类型,达到泛型编程的目的。

注意:在Linux里没有真正的线程,所以需要使用第三方的库,主要采用POSIX线程库,则使用线程相关函数必须包含头文件<pthread.h>,并且编译时必须加上-lpthread,否则就会出现连接错误。
(3)返回值:
成功返回0.失败返回错误码,而且错误代码以返回值形式返回。(所有以pthread开头的相关函数都满足这个特性)

2.使用示例:
例1:

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

void* thread_entry(void* arg)
{                                                                                                                  
    printf("thread\n");
    return NULL;
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,thread_entry,NULL);
    sleep(1);   //主线程必须要等1秒,否则如果主线程先被调度,就会导致主线程结束后,新线程还没有执行。
    return 0;
}

3.通过pthread_self函数查看线程id
示例:查看线程的线程id(通过pthread_self( )函数,可以查看自己的线程id)

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

void* thread_entry(void* arg)
{
    printf("child thread, id=%lu\n",pthread_self());
    return NULL;
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,thread_entry,NULL);
    sleep(1);
    printf("main thread, child thread id:%lu\n",id);
    return 0;
}

运行结果:

  • 可以看到:通过pthread_create函数拿到的线程id与新线程里调用pthread_self函数得到的线程id的值相同
[root@localhost 线程创建]# gcc -o thread2 thread2.c -lpthread
[root@localhost 线程创建]# ./thread2
child thread, id=3078486896
main thread, child thread id:3078486896

3.当创建一个新线程后,会有两个执行流,新线程去执行相应的处理函数,主线程继续往下执行。
4.创建的新线程和主线程谁先执行并不确定,由调度器决定,(因为线程是轻量级进程,父子进程谁先执行并不确定)

1.2 线程等待

1.为什么线程需要等待
(1)需要知道线程退出信息;
(2)需要知道线程运行结果是否正确;
(3)回收线程的资源;
(4)如果不等待就会造成和僵尸进程类似的问题(注意没有僵尸线程);
2.线程等待函数(pthread_join)

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval); 
//thread表示需要等待线程的线程id,retval为thread线程执行其线程处理函数的返回值(retval为输出型参数,为了拿到线程函数的返回值),因为线程处理函数的返回值为void*,所以需要用void**来接收返回值

3.只有当线程正常退出才能被等待
(1)线程正常退出:代码执行完结果正确;代码执行完结果不正确;
(2)因为如果线程异常退出,整个进程就会被终止掉。
4.通过pthread_join( )函数,调用者以阻塞方式等待。调用pthread_join函数的线程会被挂起等待,直到被等待的进程终止才继续执行。
注意:线程等待中没有非阻塞等待方式,而进程等待有阻塞和非阻塞形式。

5.使用示例:

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

void* thread_run(void* arg)
{
    printf("im thread1\n");
    return (void*)5;
}
int main()
{
    pthread_t pid;
    pthread_create(&pid,NULL,thread_run,NULL);

    void* ret;
    pthread_join(pid,&ret);      //将线程处理函数的返回值通过ret保存

    printf("%d\n",(int)ret);
    return 0;
}

运行结果:(可以看到pid线程的返回值为5,主线程等待该线程后,拿到的返回值为5)
这里写图片描述

1.3 线程的终止
1.3.1 线程终止有三种方法

(1)线程处理函数里return,返回值为线程的退出码;
(2)线程调用pthread_exit(主线程调用该函数,我们无法获得主线程的退出信息,主线程表示的是进程的退出必须采用进程的退出方式);
(3)线程可以被同一进程中的其它线程取消掉(通过pthread_cancel函数)。

1.3.2 pthread_exit

1.函数原型:

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

 //在线程处理函数里面调用该函数,用来终止该线程,并且线程的退出码保存在retval里

2.示例:在thread_run函数里面调用pthread_exit函数,函数的参数为线程退出码,这里将线程退出码设为123

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

void* thread_run(void* arg)                                                                                                             
{
    printf("Im thread1\n");
    pthread_exit((void*)123);
}
int main()
{
    pthread_t pid;
    pthread_create(&pid,NULL,thread_run,NULL);
    void* ret;
    pthread_join(pid,&ret);
    printf("thread1 exit code:%d\n",(int)ret);
    return 0;
}

运行结果:(可以看到:通过线程等待,通过ret拿到的线程退出码就是123)
这里写图片描述
3.示例2:主线程调用pthread_exit函数

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

void* thread_run(void* arg)
{
    printf("thread 1\n");
}
int main()
{
    pthread_t pid;
    pthread_create(&pid,NULL,thread_run,NULL);
    sleep(1);
    printf("main thread\n");                                                                                                            
    pthread_exit((void*)456);
    return 0;
}

运行结果:(可以看到主线程调用pthread_exit函数,当参数设为456时,进程的退出码并不是456而是0)
这里写图片描述
总结:主线程的退出必须采用进程的退出方式。

1.3.3 pthread_cancel

pthread_cancel,既可以取消某个线程,也可以自己取消自己

1.函数原型:

#include <pthread.h>
int pthread_cancel(pthread_t thread);

2.示例:

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

void* thread_run(void* arg)
{
    printf("Im thread1\n");
    pthread_exit((void*)123);
}
int main()
{
    pthread_t pid;
    pthread_create(&pid,NULL,thread_run,NULL);
    pthread_cancel(pid);

    void* ret;
    pthread_join(pid,&ret);
    printf("thread1 exit code:%d\n",(int)ret);                                                                                          
    return 0;
}

运行结果:(可以看到:用pthread_cancel后,线程的退出码为-1,因为这是宏PTHREAD_CANCELED的值。
这里写图片描述
如果主线程在取消线程之前sleep几秒,则那个线程就会正常退出,退出码就是其函数返回值
这里写图片描述

1.3.4 在线程处理函数中调用exit、_exit直接导致进程退出

示例:在线程处理函数thread_run函数里调用exit:

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

void* thread_run(void* arg)
{
    printf("Im thread1\n");
    exit(123);                                                                                                                          
}
int main()
{
    pthread_t pid;
    pthread_create(&pid,NULL,thread_run,NULL);
    void* ret;
    pthread_join(pid,&ret);
    printf("thread1 exit code:%d\n",(int)ret);
    return 0;
}

运行结果:(可以看到在thread_run里面调用exit,将不会输出”thread1 exit code:“,因为直接导致进程退出,则主线程也不会下继续执行下面的代码,也就不会打印那句话,并且通过echo $?可以查看进程退出码为123,也就是exit里面的哪个参数。
这里写图片描述

1.4 线程的分离

1.因为线程需要被等待,如果不等待就会造成内存泄露。如果不关注线程的退出信息,可以将线程设为分离状态。
2.相关函数:

pthread_detach(pthread_t thread);

3.使用示例:
例1:

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

void* entry(void* arg)
{
    printf("thread id:%d\n",pthread_self());
    sleep(5);

    return (void*)3;
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,entry,NULL);
   // pthread_detach(id);
    sleep(1);                                                                                                                           

    void* ret;
    pthread_join(id,&ret);
    printf("thread id %d ,return %d\n",id,(int)ret);
    return 0;
}

运行结果:
①如果没有pthread_detach这句,运行结果如下:

[root@localhost 线程终止]# ./threaddt
thread id:-1216820368
thread id -1216820368 ,return 3

②如果加上pthread_detach这句,运行结果如下:(可以看到输出的线程函数的返回值是一个随机数,说明已经无效,不能对一个已经分离的线程进行线程等待)

[root@localhost 线程终止]# ./threaddt
thread id:-1216951440
thread id -1216951440 ,return 134514203

例2:将线程设为分离后,再通过pthread_join等待,通过函数返回值进行进一步验证(被分离后不能再等待)

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

void* entry(void* arg)
{
    pthread_detach(pthread_self());
    printf("new thread\n");
    return NULL;
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,entry,NULL);
    sleep(1);  //保证新线程先执行

    int ret = pthread_join(id,NULL);
    if(ret == 0)
    {   
        //说明等待成功
        printf("wait success\n");
    }   
    else
    {
        printf("wait failed\n");
    }
    return 0;
}             

运行结果:

[root@localhost 线程终止]# ./threaddt2
new thread
wait failed   //输出wait failed,说明等待失败

4.注意点:
(1)设为分离状态的线程不需要等待,也不能被其它线程回收或杀死,当其运行结束后会自动释放自己的资源。
(2)线程被分离,则它的退出信息我们不再关心,退出结果我们也不再关心,资源我们也无需再回收。
(3) 但是当一个被分离的线程出错后也会影响到其它线程,因为无论怎样,在、线程都是属于进程内部的。

5.线程不仅可以被其它线程设为分离状态,也可以自己把自己设为分离状态:

pthread_detach(pthread_self());   //自己将自己设为分离状态

2 探索线程标识

1.在Linux下,线程是通过进程模拟的,每个线程就会有一个PCB;
没有线程之前,每个进程对应一个PCB,有了线程之后就会使得一个进程管理多个PCB,但是为了使得同一个进程里面的线程对应相同的进程id,所以每个PCB里面不仅需要线程id还要有标识进程的id。
2.在Linux下,一个进程就对应多个PCB,一个线程对应一个PCB;Linux内核引入了线程组的概念。则对于多线程的进程就是一个线程组,在这个线程组里面,每个线程的线程id不相同,但是他们的组id相同,这个组id就是进程id。在线程的PCB里面还包含一个指向组长线程的指针,这个组长就是主线程;主线程的线程id与进程id相同。
3.先有进程然后才会有线程。(因为进程是进行资源分配的最小单位)
4.tast_struct结构体

struct task_struct {
        ...
        pid_t pid;    //这里的pid就是线程id
        pid_t tgid;   //tgid表示线程组id,也就是进程id
         ... 
        struct task_struct *group_leader;     //组长线程,也就是主线程
           ... 
        struct list_head thread_group;      //使用一个链表,将同一个线程组里面的线程连接起来,list_head就是这个链表的头结点
        ... 
}; 

5.通过命令查看线程id(通过ps -eLF)

[root@localhost 线程创建]# ps -eLF |head -n 1 &&  ps -eLF | grep thread2 |grep -v grep
UID        PID    PPID    LWP   C  NLWP    SZ   RSS  PSR  STIME  TTY          TIME     CMD
root     11032  3339  11032  0    2    3088     480   0  19:35      pts/0     00:00:00  ./thread2
root     11032  3339  11033  0    2    3088     480   0  19:35      pts/0     00:00:00   ./thread2

分析:
PID表示进程id,PPID表示父进程id,LWP表示轻量级进程id,也就是线程id,后面的CMD表示属于哪一个进程。
可以看到:

  • 对于进程./thread2来说,里面有两个线程,他们的进程id相同,都是11032,但是各自有独立的线程id,其中线程id为11302的为主线程(与进程id相同)。
  • 但是对于同一个进程./thread2,上面打印出来的线程id与通过命令看到的线程id不相同,说明线程有两个线程id。
  • 同一个线程的两个线程id,一个是pthread_t类型,一个是pid_t类型。通过pthread_create函数会产生一个线程id,这个线程id是pthread_t类型的,该线程id是属于POSIX线程库范畴的,在线程库里面对线程进行相关操作都是通过该值进行;而pid_t类型的线程id,也就是LWP,是内核表示线程的唯一标识符,主要是操作系统为了进行线程调度。
  • 通过gettid( )可以获得线程id,也就是可以获得PCB里面的pid的值,等于LWP的值;通过getpid( )可以获得进程id,也就是PCB里面tgid的值; 通过pthread_self( )获得的值就是pthread_t类型的线程id的值。

6.对于线程而言,没有父子之分,每个线程地位都相同,除了主线程会有些特殊之外。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值