Linux 线程概念&线程控制

                                                     

       这些天家里格外的热,同时自己还偷懒了几天。博客一直没来得及更新,一想到为了冲刺实习生,自己还是不能堕落。希望和老铁们一起过一个充实的暑假~今天我们来学习Linux多线程~它和进程有什么区别呢?

目录

线程概念

Windows下线程的概念

Linux下的线程

总结

Linux 线程vs进程

Linux线程控制

线程创建 

pthread_create

pthread_self

查看线程方法

线程等待

pthread_join

注意 

线程终止

pthread_exit

pthread_exit和exit

pthread_cancel

注意

线程分离

pthread_detach 

线程id和LWP

如何理解线程tid?

用户级线程和内核轻量级线程的关系 


线程概念

Windows下线程的概念

一般教材线程的概念:线程是进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更加细和轻量化。 

但是,今天我们讲的线程和上面的不一样,Linux下设计的线程是复用了进程PCB,具体是怎么设计的呢?我们今天来重点学习一下:

Linux下的线程

       实际上,Linux下的并没有真正意义上的线程,而是对进程PCB的复用。每创建一个线程时,就在当前进程内创建一个PCB,和主线程指向同一个地址空间。但是在CPU看来,它只关心该进程的PCB,对于进程内部的"线程",CPU就不去看,线程是被当前进程的主线程进行管理。 

总结

CPU此时看到的PCB <= 之前讲的PCB的概念

一个PCB就是一个需要被调度的执行流!

Linux中没有专门为线程设计TCB,而是用进程的PCB来模拟线程的,这样做有什么好处?

不用维护复杂的进程和线程之间的关系,不用单独为线程设计任何算法,直接使用进程的一套相关的方法。OS只需要聚焦在进程的资源分配上就可以了。

Linux 线程vs进程

区别一

所有的轻量级进程(可能是"线程")都是在进程内部执行(地址空间:表示进程所能看到的大部分资源)

进程:具有独立性,大部分资源是私有的,可以有部分资源共享(管道、ipc资源)

线程:大部分资源是共享的,可以有部分资源是私有的(栈,上下文)

区别二

Linux线程控制

Linux下的线程是用进程模拟的,所以Linux下不会给我们提供直接操作线程的接口,而是给我们提供的,在同一个地址空间内创建PCB的方法,分配资源给指定的接口,这样做对用户使用很不友好,所以就有人实现第三方库来供我们使用!

线程创建 

pthread_create

pthread_t* thread:输出型参数,返回创建线程的id

const pthread_attr_t* arr:线程属性,我们在这里设置成NULL就可以

void *(*start_routine) (void *):创建的线程执行方法,函数指针

void* arg:给回调函数传的参数

pthread_self

返回当前线程的id(注意:和下面的LWP不一样!)

代码测试:

void* thread_run(void* args)
{
    const char* id = (const char*)args;
    while(1)
    {
        printf("我是%s线程: %d\n", id, getpid());
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, thread_run, (void*)"thread 1");

    while(1)
    {
        printf("我是main主线程: %d\n", getpid());
        sleep(1);
    }
    return 0;
}

运行结果:

查看线程方法

[cyq@VM-0-7-centos 线程控制]$ ps -aL

 

 这时候我们就发现了两个线程,它们的PID都是一样的,说明他们属于同一个进程!LWP是线程ID。

线程等待

为什么要有线程等待?

一般而言,线程也是需要被等待的,如果不等待,可能导致类似于"僵尸进程"问题。

pthread_join

pthread_t thread:要等待线程的id

void** retval:输出型参数,用来获取新线程退出的时候,线程函数的返回值。 

返回值:等待成功返回0,等待失败,返回错误码。

举个栗子:

void* thread_run(void* args)
{
    int id = *(int*)args;
    while(1)
    {
        printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
        sleep(5);
        break;
    }
    return (void*)123;
}

#define NUM 1
int main()
{
    pthread_t tid[NUM];
    int i = 0;
    for(i = 0; i < NUM; i++)
    {
        pthread_create(tid+i, NULL, thread_run, (void*)&i);
        sleep(1);
    }

    void* status = NULL;

    for(i = 0; i < NUM; i++)
    {
        pthread_join(tid[i], &status); //阻塞式等待
    }

    printf("ret: %d\n", (int)status);
    sleep(3);
    return 0;
}

运行结果:

这时候我们发现等待可以获取创建线程的返回结果。

注意 

pthread_join:是一种阻塞式等待,必须是阻塞式等待,直到新线程退出才能拿到对应的返回值。

printf("ret: %d\n", (int)status):(int)status,不能写成*(int*)status,linux这里是64位机,8个字节,这样写会越界,而我们只需要一个整数而已。

pthread_join:需要处理代码异常的问题吗??不需要!

线程终止

线程终止有三种方法:

我们只介绍方法2和方法3: 

pthread_exit

void* retval:退出参数 ,给pthread_join的输出参数一个返回值。echo $?不受retval影响。

代码测试:

void* thread_run(void* args)
{
    int id = *(int*)args;
    while(1)
    {
        printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
        sleep(5);
        break;
    }
    pthread_exit((void*)123);
}

#define NUM 1
int main()
{
    pthread_t tid[NUM];
    int i = 0;
    for(i = 0; i < NUM; i++)
    {
        pthread_create(tid+i, NULL, thread_run, (void*)&i);
        sleep(1);
    }

    void* status = NULL;

    for(i = 0; i < NUM; i++)
    {
        pthread_join(tid[i], &status); //阻塞式等待
    }

    printf("ret: %d\n", (int)status);
    sleep(3);
    return 0;
}

运行结果:

我们发现通过进程等待可以拿到pthread_exit的退出结果。 

pthread_exit和exit

依照上面的代码,将pthread_exit改为exit:

测试结果:

 

        我们发现退出结果没有打印,也就是说,在新线程中遇到exit后,整个进程(所有线程)全部退出。所以我们以后要想终止某一个线程就使用pthread_exit,而不是exit。

终止线程的第三个方法:

pthread_cancel

pthread_t thread:要分离线程的id 

返回值:成功就返回0,失败1返回退出码。

函数调用成功,退出码为-1。

这个-1是什么呢?

[cyq@VM-0-7-centos 线程控制]$ grep -ER "PTHREAD_CANCELED" /usr/include/pthread.h 

 实际上就是pthread_exit的退出结果,-1就是宏:PTHREAD_CANCELED。

举个栗子:

void* thread_run(void* args)
{
    int id = *(int*)args;
    while(1)
    {
        printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
        sleep(1);
    }
}

#define NUM 5
int main()
{
    pthread_t tid[NUM];
    int i = 0;
    for(i = 0; i < NUM; i++)
    {
        pthread_create(tid+i, NULL, thread_run, (void*)&i);
        sleep(1);
    }

    printf("wait sub thread...\n");
    int val = pthread_cancel(tid[0]);
    printf("cancel thread...\n");

    void* status = NULL;

    pthread_join(tid[0], &status); //阻塞式等待

    printf("val: %d\n", (int)status);


    sleep(3);
    return 0;
}

我们用一个命令脚本来看一下结果:

[cyq@VM-0-7-centos test]$ while :; do ps -aL | head -1 && ps -aL | grep mytest; sleep 1; echo "###########################"; done

我们让一个线程被pthread_cancel后,通过监视,发现该线程确实退出了。同时join到取消线程的退出结果是-1,说明取消成功了。

注意

pthread_cancel主线程可以取消子线程,但是子线程不能取消主线程;主线程也不能取消自己,否则会造成(该进程会成为僵尸进程)内存泄漏的问题。

线程分离

主线程要等待创建的新线程,否则会造成内存泄漏的问题,如果不想等待呢?有没有办法?

pthread_detach 

pthread_thread:要分离线程的id 

注意:

 举个栗子:

void *thread_run(void *args)
{
    int id = *(int *)args;
    int cnt = 3;
    while (cnt--)
    {
        printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
        sleep(1);
    }
}

int main()
{
    pthread_t tid;


    pthread_create(&tid, NULL, thread_run, (void*)"new thread");
    sleep(3);


    pthread_detach(tid);

    int ret = pthread_join(tid, NULL); //阻塞式等待

    printf("ret: %d\n", ret);

    sleep(3);
    return 0;
}

运行结果:

我们发现pthread_join的等待结果不是0,说明等待线程失败了。同时最后线程都退出时我们发现没有<default>这样的标志,说明没有僵尸进程,所以不存在内存泄漏。 

线程id和LWP

线程id代码打印:

void* thread_run(void* args)
{
    int num = *(int*)args;
    while(1)
    {
        printf("我是[%d]线程, 我的线程ID是: %lu\n", num, pthread_self());
        sleep(1);
    }
}
int main()
{
    pthread_t tid[5];
    printf("我是主线程, 线程id: %lu\n", pthread_self());

    for(int i = 0; i < 5; i++)
    {
        sleep(1);
        pthread_create(tid+i, NULL, thread_run, (void*)&i);
    }
    sleep(3);
    return 0;
}

注意:pthread_self要对应%lu打印。

打印结果:

 我们发现同样是用来标识线程id的,但为什么两者差别这么大呢?

 我们用%x打印线程id:

               

       我们查看到的线程id是pthread库中的线程id,不是linux内核中的LWP,pthread库的线程id是一个内存地址(虚拟地址)! ! 

如何理解线程tid?

实际上tid就是对应地址空间上共享区里的地址。因为用地址来做id,是可以最快找到映射的对应线程库的代码和数据。 

用户级线程和内核轻量级线程的关系 

 大部分的操作系统都是这样设计的,不排除特别情况。

看到这里,支持博主一下吧~

                               

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值