【Linux】线程基本概念及相关操作

程序的一个执行路线就叫做线程,线程是一个进程内部的一个控制序列。一个进程至少有一个线程。

我们通过一幅图来了解下一线程和进程的关系

我们通过图片可以清楚的理解为什么说线程是程序的一个执行路线。线程又被称作是轻量级进程。

那么我们可以理解,在一个进程下,多个线程之间的很多资源是共享的,比如地址空间(包括代码段,数据段,如果定义一个函数或者全局变量,在各个线程都可以调用)。除此之外还共享以下资源:文件描述符表、信号处理方式、用户ID和组ID、和当前工作目录。线程也拥有自己的数据,比如:寄存器、栈、线程ID、调度优先级、errno。

线程是执行程序的最小单位,进程是资源竞争的基本单位。

线程的优缺点:

     1. 创建一个新线程的代价要⽐创建一个新进程⼩得多
     2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多 (因为一个进程的多个线程共享地址空间)
     3. 线程占用的资源要比进程少很多
     4. 能充分利用多处理器的可并行数量 
     5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
     6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现 
     7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

线程的缺点:
     1. 一些系统调用及异常都是针对整个线程,因此若一个线程出问题,整个进程都会受到影响
     2. 线程的安全性较低
     3. 因为共享了数据,通信变的方便,但是资源的争抢就会体现出来因此编码难度要更高一点

创建线程:
函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*star t_routine)(void*), void *arg); 
        thread:返回线程id
        attr:设置线程的属性 ,attr为NULL表⽰示使⽤用默认属性 
        star t_routine:是个函数地址,线程启动后要执行的函数,函数退出了,线程也就退出了
        arg:传给线程启动函数的参数
        返回值:成功返回0,失败返回错误码。
传统的一些函数在错诶时会将错误码放在errno中,但该函数出错时不会设全局变量errno,而是将错误码作为返回值返回。

线程ID:

1. 在Linux中,目前的线程实现是Native POSIX Thread Libaray 简称NPTL。在这种实现下,线程又被称为轻量级进程,每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
2. 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了 1:N关系,POSIX标准又要求进程内的所有线程调用 getpid 函数时返回相同的进程ID,如何解决上述问题呢?
3. Linux内核引入了线程组的概念。

我们可以先查看一下task_struct结构:
 

struct task_struct {
 ...
 pid_t pid;
 pid_t tgid;
 ...
 struct task_struct *group_leader;
 ...
 struct list_head thread_group;
 ...
};

多线程的进程可以看成是一个线程组。我们可以看到结构体中有一个pid,看起来这是我们的进程ID,其实不是,这个pid实际上是线程ID;而tgid(Thread Group ID)对应的则是用户态下的进程ID。所以我们用gitpid()函数得到的是tgid,即进程ID。而我们的线程ID,即pid(结构体中),可以通过gittid()函数来获得。
我们可以通过命令ps -eL来查看线程信息(LWP线程ID)

接下来我们看一段代码:
 

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

void *thread_run1(void* arg)
{
    while(1)
    {
        sleep(1);
    }   
}
void *thread_run2(void *arg)
{
    while(1)
    {
        sleep(1);
    }
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1,NULL,thread_run,NULL);
    pthread_create(&tid2,NULL,thread_run2,NULL);
}

运行该程序并用ps -eL命令查看


我们可以看到有一个线程的线程id和进程id一样,这是为什么呢?
线程组内的第⼀个线程,在⽤户态被称为主线程(main thread),在内核中被称为group leader,内核在创建
第⼀个线程时,会将线程组的ID的值设置成第⼀个线程的线程ID,group_leader指针则指向⾃⾝,
既主线程的进程描述符。所以线程组内存在⼀个线程ID等于进程ID,⽽该线程即为线程组的主线
程。
我们都知道,线程ID是pid_t类型的,但是我们创建线程的函数pthread_create会创建一个线程id并把它放到第一个参数指向的地址中。这个线程id是pthread_t类型,和之前讲到的pid_t类型不是一回事,这个线程id使我们用户看到的id,可以通过这个线程id来对线程进行其他库函数操作。该线程id可以通过函数 pthread_t pthread_self(void)来获得,而且pthread_t类型本质是一个地址,可以用%p打印。

线程终止:

如果只终止一个线程而不终止进程,我们有三种方法。
1.在线程函数中return。
2.线程调用函数  void pthread_exit(void** value_ptr);
3.一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

从线程函数return从而退出线程这种方式不适用于主线程。
void pthread_exit(void** value_ptr) 该参数指向一个非局部变量(后面详细说明)。该函数没有返回值,因为线程退出后无法返回到它的调用者。
调用int pthread_cancel(pthread_t thread) 成功返回0,失败返回错误码。给线程发送一个终止信号。调用该函数时,最好用pthread_join函数(下面介绍)来等待线程的退出,否则可能会出现段错误。

线程等待:

如果一个线程退出但其空间没有释放,仍然在进程的地址空间内,这样就会发生内存泄漏问题,创建新的线程不会复用刚才退出的地址空间。类似进程等待函数waitpid。线程等待也有一个函数。
int pthread_join(pthread_t thread,void **value_ptr);
调用该函数的线程将挂起等待,直到id为thread的线程终止。线程以不同方式终止,通过该函数得到的终止状态是不同的。终止状态体现在第二个参数value_ptr中。
1.通过线程函数return来终止线程,value_ptr中存放的值是线程函数返回的值。
2.通过pthread_exit来终止线程,value_ptr中存放的是pthread_exit函数的参数。所以这就是为什么pthread_exit函数中的参数只能是全局数据或者从堆上动态分配的数据,而不能是线程函数栈上的数据,因为线程函数退出后,其局部变量也释放掉。
3.通过pthread_cancel函数来终止另一个线程,value_ptr所指向的单元存放的是常数PTHREAD_CANCELED(-1)。
如果对线程退出状态不关心可以将参数设置为NULL。
我们通过代码来验证:

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

void* thread_run1(void *arg)
{
    int *p=(int*)malloc(sizeof(int));
    *p=100;
    sleep(1);
    printf("thread_run1  id:%p\n",pthread_self());
    return (void*)p;
}
void* thread_run2(void *arg)
{
    while(1)
    {
        sleep(1);
        printf("thread_run2  id:%p\n",pthread_self());
        int *p=(int*)malloc(sizeof(int));
        *p=200;
        pthread_exit((void*)p);
    }
}
void* thread_run3(void *arg)
{
    while(1)
    {
        sleep(1);
        printf("thread_run3  id:%p\n",pthread_self());
    }
    return NULL;
}
int main()
{
    void *ret;
    pthread_t tid1,tid2,tid3;
    pthread_create(&tid1,NULL,thread_run1,NULL);
    pthread_join(tid1,&ret);
    printf("%d\n",*(int*)ret);
    sleep(1);
    pthread_create(&tid2,NULL,thread_run2,NULL);
    pthread_join(tid2,&ret);
    printf("%d\n",*(int*)ret);
    sleep(1);
    pthread_create(&tid3,NULL,thread_run3,NULL);
    sleep(2);
    pthread_cancel(tid3);
    pthread_join(tid3,ret); //注意这里给第二个参数的时候就不要取地址了,因为要保存的是常数-1,否则会发生段错误。
    printf("%d\n",*(int*)ret);
    return 0;
}

程序运行结果如下:

线程分离:

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进⾏pthread_join操作,否则⽆法释放资源,从⽽造成系统泄漏。我们可以将线程设置为分离的,这样在线程退出后会自动释放线程资源。
int pthread_detach(pthread_t thread);
joinable和分离是冲突的,⼀个 线程不能既是joinable⼜是分离的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值