线程的创建和终止

Posted 冰冻三尺 非一日之寒

tags:

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

 

拥有线程程序的编译需要加 -pthread

gcc a.c -o a -pthread

 

/*
        #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:给第三个参数传参,不需要传递参数,就传NULL
            返回值:
                成功:0
                失败:错误号,与之前的errno不一样
                    因此不能使用perror,使用strerror
                    
        void pthread_exit(void *retval);
            功能:终止一个线程,在哪个线程中调用,就表示终止哪个线程
            参数:
                retval:作为返回值,可以通过pthread_join()获取
                    不需要返回值,则添NULL

        pthread_t pthread_self(void);
            功能:获取线程id

        int pthread_equal(pthread_t t1, pthread_t t2);
            功能:判断两个线程id是否相等
*/

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
void *callback(void* arg)

    printf("child thread\\n");
    printf("%d\\n", *(int *)arg);
    return NULL; // == pthread_exit(NULL);



int main()

    pthread_t thread;
    // int errno = pthread_create(&thread, NULL, callback, NULL);

    int num = 10;
    int errno = pthread_create(&thread, NULL, callback, (void *)&num);


    if(errno != 0)
    
        printf("%s\\n", strerror(errno));
        exit(-1);
    
    for(int i = 0; i < 5; i++)
    
        printf("%d\\n", i);
    

    sleep(1);

    pthread_exit(NULL);


    return 0;

 

线程的创建,终止和回收

线程

线程概念

线程:轻量级的进程,其本质仍是进程。(在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

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

Linux线程 | 创建 终止 回收 分离

线程的创建,终止和回收

线程的创建,终止和回收

线程的创建,终止和回收

猎豹MFC--进程和线程--创建线程AfxBeginThread() SetDlgItemInt()线程暂停继续终止

2.如何创建线程