线程的创建,终止和回收

Posted milaiko

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程的创建,终止和回收相关的知识,希望对你有一定的参考价值。

线程

线程概念

线程:轻量级的进程,其本质仍是进程。(在Linux环境下)
进程: 具有独立的地址空间,拥有pcb
线程:有独立的pcb,但是没有独立的地址空间(共享)

Linux下:
线程:最小的执行单位
进程:最小分配资源单位,可看成只有一个线程的进程。
ps -Lf 去看看firefox

进程控制块 PCB

每个进程在内核中都有一个进程控制块(PCB)来维护进程相关信息,Linux内核的进程控制块是task_struct结构体。

其内部成员有很多,我们大致了解一部分

  • 进程的状态,有就绪、运行、挂起、停止等状态
  • 进程切换时需要保存和恢复的一些cpu寄存器
  • 描述虚拟地址的空间信息
  • 描述控制终端的信息
  • 当前工作目录
  • umask掩码
  • 文件描述符表,包含很多指向file结构体的指针
  • 和信号相关的信息
  • 用户id和组id
  • 会话(session)和进程组
  • 进程可以使用的资源上限。

实际上,虚拟地址只能与MMU相关,而如何通过MMU相关是通过PCB和物理地址相关。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kupEKirV-1626964303995)(https://z3.ax1x.com/2021/05/17/gRhj4f.png)]

从内核看线程和进程是一样的, 都有各自不同的PCB,但是PCB中指向 内存资源的三级页表是不同的。

  1. 轻量级进程,也有PCB,创建线程使用的底层函数和进程一样,都是clone
  2. 从内核里看进程和线程是一样的,都有各自不同的PCB, 但是PCB指向内存资源的三级页表是不同的
  3. 进程可以蜕变成线程
  4. 线程可以看作寄存器和栈的集合
  5. 在linux下, 线程最小的执行单位是线程,进程是最小的资源分配单位

pthread_self()

#include <pthread.h>

pthread_t pthread_self(void);

这个函数是返回自身的线程ID

在linux使用无符号长整型表示pthread_t数据类型,solaris 把 pthread_t表示为无符号整型,FreeBSD和MAC OS是使用pthread的结构体的指针来表示

pthread_create

#include <pthread.h>        

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                    void *(*start_routine) (void *), void *arg);
  • thread 创出参数
  • pthread_attr_t 线程属性, 如果不想看这个值可以传NULL
  • 一个函数指针
  • arg,上一个函数指针的参数,如果不需要参数传NULL
  • 返回值
    • 成功返回0, 失败返回error number,并且content of thread没有定义。

创建线程案例

#include"head.h"
#include<pthread.h>
void *tfn(void *arg){
    printf("pthread : pid = %d, tid = %lu\\n", getpid(), pthread_self());
    return NULL;
}
int main(){
    pthread_t tid;
    // tid = pthread_self();
    printf("main : pid = %d, tid = %lu\\n", getpid(), pthread_self());
    int ret = pthread_create(&tid,NULL, tfn,NULL);
    if(ret !=  0){
        sys_err("pthread create error");
    }
    sleep(1);   //给定足够的时间让线程打印
    return 0;
}

主线程传递给子线程的变量要使用值传递

使用值传递

#include"head.h"

// int是4字节,void* 是8字节,因为先是从8字节到4字节,再是4字节到8字节,没有数据丢失
void *tfn(void* arg){
    long int i = (long int)arg; //根据编译器,有的编译器不允许从8字节到4字节的强转,以防止数据丢失
    sleep(i);
    printf("--I'm %ldth thread, :pid = %d, tid = %lu\\n", i+1, getpid(),  pthread_self());
    return NULL;
}
int main(){
    int i; 
    int ret;
    pthread_t tid;

    for(i=0;i<5;i++){
        ret = pthread_create(&tid, NULL, tfn, (void*)i);        //传入参数
        if(ret!=0){
            sys_err("pthread_create error");
        }
    }
    sleep(i);
    return 0;
}

使用指针传递

#include"head.h"
// int是4字节,void* 是8字节,因为先是从8字节到4字节,再是4字节到8字节,没有数据丢失
void *tfn(void* arg){
    int i = *((int* )arg);  //将之强转为int指针 ,并且取引用。
    sleep(i);
    printf("--I'm %dth thread, :pid = %d, tid = %lu\\n", i+1, getpid(),  pthread_self());
    return NULL;
}
int main(){
    int i; 
    int ret;
    pthread_t tid;

    for(i=0;i<5;i++){
        ret = pthread_create(&tid, NULL, tfn, (void*)&i);        //传入i变量的地址
        if(ret!=0){
            sys_err("pthread_create error");
        }
    }
    sleep(i);
    return 0;
}

打印出来会发现i并不是老老实实从1到5,而是在1——5之间随机的树,其实聪明的同志已经知道了。

那我们解析一下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lqa1YjRv-1626964303997)(https://z3.ax1x.com/2021/05/18/ghp4G8.png)]

这是变量i在栈区的表现,因为是依赖地址去寻找值的。而线程的切换需要是时间,主线程里面对i循环速度会更快,所以当子线程没有获得正确的i值时,i的值已经切换,所以也导致了arg的切换。

嗯?也就是说在for循环sleep一下,就能够得到正确的顺序了?——————u1s1确实,但是实际上不会让你天天sleep把。

主子线程共享全局变量

pthread_exit函数

该函数的功能是退出当前的线程

#include <pthread.h>

void pthread_exit(void *retval);
#include"head.h"
void* func(){
    return NULL;
}

void* func1(){
    pthread_exit(NULL);         //将当前进程退出
    return NULL;
}
                                // int是4字节,void* 是8字节,因为先是从8字节到4字节,再是4字节到8字节,没有数据丢失
void *tfn(void* arg){
    long int i = (long int)arg; //但是不允许void* 转化到 int,因为有数据丢数
    sleep(i);
    if(i == 2){
        // exit(0);                 //表示退出进程
        // return NULL;             //达到目的, 表示返回函数调用者, 如果return Null是直接在tfn能够成功退出进程
        // func();                     //但是换了个函数就不能达到目的
        func1();
    }
    printf("--I'm %ldth thread, :pid = %d, tid = %lu\\n", i+1, getpid(),  pthread_self());
    return NULL;
}
int main(){
    int i; 
    int ret;
    pthread_t tid;

    for(i=0;i<5;i++){
        ret = pthread_create(&tid, NULL, tfn, (void*)i);        //传入参数
        if(ret!=0){
            sys_err("pthread_create error");
        }
    }
    sleep(i);
    return 0;
}

pthread_join函数

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

该函数的功能是回收一个终止的线程

#include"head.h"
struct thrd{
    int var;
    char str[256];
};

void *tfn(void *arg){
    struct thrd *tval;
    tval = (struct thrd* )malloc(sizeof(tval));
    tval->var = 100;
    strcpy(tval->str, "hello thread");

    return (void*)tval;
}

int main(){
    pthread_t tid;
    struct thrd *retval;
    int ret = pthread_create(&tid, NULL, tfn, NULL);
    if(ret != 0){
        sys_err("pthread_create error");
    }
    ret = pthread_join(tid, (void **)&retval); //tid 是要回收的tid, 把tid返回的东西拿到手 
    if(ret != 0){
        sys_err("pthread_join error");
    }
    printf("child thread exit with var = %d, str=%s\\n", retval->var, retval->str);
    pthread_exit(NULL);
}

pthread_cancel

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

功能:从主线程杀死子线程

代码案例:

#include"head.h"
void* tfn(void *arg){
    while (1)
    {
        printf("thread: pid = %d, tid =%lu\\n", getpid(), pthread_self());
        sleep(1);
    }
    return NULL;
}


int main(){
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, tfn, NULL);    
    if(ret!=0){
        fprintf(stderr, "pthread_create error:%s\\n", strerror(ret));
        exit(1);
    }
    printf("main: pid = %d, tid = %lu\\n", getpid(), pthread_self());
    sleep(5);
     
    ret = pthread_cancel(tid);
    if(ret != 0){
        fprintf(stderr, "pthread cancel error:%s\\n", strerror(ret));
        exit(1);
    }
    while(1);
}

注意:如果说子线程没有退出点,那么父线程调用pthread_cancel也无法将其关闭。

pthread_detach函数

功能:实现线程分离

#include <pthread.h>

int pthread_detach(pthread_t thread);

线程detacch和joinable

一个可汇合的线程(joinable)终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join

一个脱离的线程就像守护进程,终止时, 所有相关资源释放。

线程属性—线程分离

线程分离可以使用pthread_detach来实现,但是如果是直接通过设置属性可以在create时就实现线程分离。

  • 初始化线程属性
int pthread_attr_init(pthread_attr_t *attr);
//return 成功:0 失败errno
  • 销毁线程属性所占用的资源
int pthread_attr_destroy(pthread_attr_t *attr);
  • 设置线程属性
#include <pthread.h>
// 设置线程属性 分离或不分离
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 获得线程属性
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

detachstate:PTHREAD_CREATE_DETACHDE (分离线程)  
detachstate:PTHREAD_CREATE_JOINABLE (可汇合线程)
示例代码
#include"head.h"
#include<pthread.h>
void *tfn(void *arg){
    printf("pthread : pid = %d, tid = %lu\\n", getpid(), pthread_self());
    return NULL;
}
int main(){
    pthread_t tid;
    // tid = pthread_self();
    pthread_attr_t  attr;
    int ret = pthread_attr_init(&attr);
    if(ret != 0){
        sys_err("arr init error", ret);
    }
    // pthread_attr_destroy();

    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if(ret != 0){
        sys_err("attr_setdetachstate error", ret);
    }

    ret = pthread_create(&tid,NULL, tfn,NULL);
    if(ret !=  0){
        sys_err("pthread create error", ret);
    }
    // sleep(1);   //给定足够的时间让线程打印  
    ret = pthread_attr_destroy(&attr);
    if(ret != 0){
        fprintf(stderr, "att_destory error", ret);
    }

    ret = pthread_join(tid, NULL);
    if(ret != 0){
        fprintf(stderr, "tid_join error", ret);
    }
    
    printf("main : pid = %d, tid = %lu\\n", getpid(), pthread_self());
    

 
    pthread_exit((void*)0); //这个函数是来退出主线程,而主线程退出不影响子线程,如果说把进程给结束了,才会影响到子线程
    return 0;
}

注意事项:
避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit

以上是关于线程的创建,终止和回收的主要内容,如果未能解决你的问题,请参考以下文章

线程的创建,终止和回收

线程的创建,终止和回收

线程的创建,终止和回收

C++笔记--Linux编程(13)-守护进程-线程

JAVA线程池使用哪一种比较好

第2次