从零开始的嵌入式学习day29

一、Linux线程基本概念
linux中,线程又叫做轻量级进程(light-weight process LWP),也有PCB,创建线程使用的底层函数和进程底层一样,都是clone,但没有独立的地址空间;而进程有独立地址空间,拥有PCB。
Linux下:线程是最小的执行单位,调度的基本单位。进程是最小分配资源单位,可看成是只有一个线程的进程。
线程是一个进程内部的控制序列。控制序列可以理解为一个执行流。进程内部是指虚拟地址空间。

1、线程特点:
(1)线程是资源竞争的基本单位。
操作系统有很多资源。进程与进程之间要竞争操作系统资源,当一个进程申请得到一大堆资源。而这些资源又会分配给线程。一个进程内部有多个线程,去竞争进程所获得的资源。所以说线程是资源竞争的基本单位。
(2)线程是程序执行的最小单位
当用户让进程去执行某个任务时,进程又会将任务细化。进程内部有很多线程,让这些线程去执行
(3)线程共享进程数据,但也拥有自己独立的一部分数据: 线程ID ,一组寄存器,栈,errno值,信号。
其中最重要的数据是栈和寄存器。私有栈是为了保存临时变量,便于函数调用等操作。私有寄存器是为了方便线程切换,保存上下文。

2、进程到线程:
进程:承担分配系统资源的实体
线程:共享进程所获得资源

二、Linux内核线程实现原理
创建线程使用的底层函数和进程一样,都是clone。从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的。进程可以蜕变成线程。线程可看做寄存器和栈的集合。
三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。但线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

三、线程相关函数

1.pthread_create函数

创建一个新线程。其作用对应进程中fork() 函数。Linux环境下,所有线程特点,失败均直接返回错误号。

函数原型

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, 
const pthread_attr_t *restrict attr, 
void *(*start_rtn)(void *), 
void *restrict arg);

参数说明:
(1)thread :
pthread_t 类型的指针,线程创建成功的话,会将分配的线程 ID 添入该指针指向的地址。线程后续的操作将该值作为线程的唯一标识。
(2)attr :
pthread_attr_t 类型,通过该参数可以定制线程属性,比如可以指定新建线程栈空间的大小,调度策略等。如果要创建的线程无特殊要求,该值设置成 NULL,标识采用默认属性。
(3)start_routine :
线程需要执行的函数。创建线程是为了让线程执行特定的任务。线程创建成功之后,该线程就会执行 start_routinue 函数,该函数之于线程,就如同 main 函数之于主线程。
(4)arg :
线程执行 start_routine 函数的参数。当执行函数需要传入多个参数时,线程创建者(一般是主线程)和新建线程约定一个结构体,创建者把信息填入该结构体,再把结构体的指针传给新建线程,新建线程只要解析这个结构体,就能获取到需要的所有参数。
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态。

2.pthread_exit函数
单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:

  1)线程只是从启动例程中返回,返回值是线程的退出码。

  2)线程可以被同一进程中的其他线程取消。

  3)线程调用pthread_exit:

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

 rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数访问到这个指针。

void* th(void*arg)
{
    int i =3;
    while(i--)
    {
        printf("th,tid:%ld\n",pthread_self()); 
        sleep(1);
    }
   
    pthread_exit(NULL);
   //return NULL;
}
int	main(int argc, char **argv)
{
    pthread_t tid;
    pthread_create(&tid,NULL,th,NULL);
 
    while(1)sleep(1);
    system("pause");
    return 0;
}
3. pthread_join函数

线程等待:

#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
// 返回:若成功返回0,否则返回错误编号

阻塞等待线程退出,获取线程退出状态。其作用对应进程中 waitpid() 函数。
int pthread_join(pthread_t thread, void **retval); 成功:0;失败:错误号
参数:thread:线程ID (注意:不是指针);retval:存储线程结束状态。
注意:

进程中:main返回值、exit参数–>int;等待子进程结束 wait 函数参数–>int *
线程中:线程主函数返回值、pthread_exit–>void *;等待线程结束 pthread_join 函数参数–>void **
参数 retval 非空用法:
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
(1)如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
(2)如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
(3)如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
(4)如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。

void * th(void* arg)
{
 
    int i = 5;
    while(i--)
    {
        printf("th tid:%ld\n",pthread_self());
        sleep(1);
    }
    return NULL;
}
 
int	main(int argc, char **argv)
{
    pthread_t tid;
    pthread_create(&tid,NULL,th,NULL);
 
    //while(1)sleep(1);
    pthread_join(tid,NULL);
    system("pause");
    return 0;
}
4.pthread_cancel函数

pthread_cancel 的函数原型如下:

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

其中,thread 参数指定了需要取消的目标线程。如果函数成功,它会返回 0;如果失败,则返回错误码。

使用 pthread_cancel 时,需要注意的是,该函数仅仅是发送一个取消请求,目标线程是否立即终止取决于它自身的取消状态和取消类型。默认情况下,线程会立即响应取消请求并退出,但线程可以通过 pthread_setcancelstate 和 pthread_setcanceltype 函数来改变这一行为。

void* th(void*arg)
{
    int i =3;
    while(i)
    {
        printf("th,tid:%ld\n",pthread_self()); 
        sleep(1);
    }
   
    pthread_exit(NULL);
   //return NULL;
}
int	main(int argc, char **argv)
{
    pthread_t tid;
    pthread_create(&tid,NULL,th,NULL);
    int i = 0 ;
    while(1)
    {
        printf("main th, tid %ld\n",pthread_self());
 
        sleep(1);
        i++;
        if(3 == i )
        {
            pthread_cancel(tid);
        }
    }
    //system("pause");
    return 0;
}

5.线程ID:
pthread_create 函数会产生一个 pthread_t 类型的线程 ID,存放在第一个参数指向的空间内。这里的线程 ID 和前面提到的 pid_t 类型的线程 ID 我们该如何去定位或者看待呢?
pid_t 类型的线程 ID :属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来在整个操作系统内唯一标识该线程。
pthread_t 类型的线程 ID :属于NPTL线程库的范畴,线程库的后续操作,就是根据该线程 ID 来操作线程的。对于 Linux 目前使用的 NPTL 实现而言,pthread_t 类型的 ID 本质上是进程地址空间上的一个地址。

pthread_self 函数:
可以获取到线程自身的 ID。其作用对应进程中 getpid() 函数。
pthread_t pthread_self(void); 返回值:成功:0; 失败:无!

线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现。线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)。注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。

void *th1 (void *arg)
{
    while(1)
    {
        printf("发送视频 th1, tid:%ld\n",pthread_self());
        sleep(1);
    }
}
void *th2 (void *arg)
{
    while(1)
    {
        printf("接收控制th2, tid:%ld\n",pthread_self());
        sleep(1);
    }
}
 
int	main(int argc, char **argv)
{
    pthread_t tid1,tid2;
    
    pthread_create(&tid1,NULL,th1,NULL);
    pthread_create(&tid2,NULL,th2,NULL);
 
    while(1)
    {
         printf("main th, tid:%ld\n",pthread_self());
        sleep(1);
    }
    //system("pause");
    return 0;
}

6.线程传参

int a = 0 ;
void* th(void* arg)
{
    sleep(1);
    a+=10;
    return NULL;
}
int	main(int argc, char **argv)
{
    pthread_t tid;
    printf("a is %d\n",a);
    pthread_create(&tid,NULL,th,NULL);
    pthread_join(tid,NULL);
    printf("a is %d\n",a);
    system("pause");
    return 0;
}

 

四、线程的优缺点
1、线程的优点:
(1)创建一个新线程的代价要比创建一个新进程小得多,释放成本也更低。因为创建一个进程就意味着要创建PCB,分配虚拟地址空间,页表,物理内存等系统资源,而创建一个线程只需要创建一个PCB(TCB)即可
(2)与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。进程切换,需要切换对应的虚拟地址空间,更换页表等。过程繁琐。
(3)线程占用的资源要比进程少很多。
(4)能充分利用多处理器的可并行数量。
(5)在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
(6)计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现 I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。进程可以将线程串行执行变成并行执行。最后汇总,提高效率。但是尽量不要创建太多线程,线程切换也是需要成本的。

2、线程的缺点
(1)性能损失:
一个很少被外部事件阻塞的计算密集型线程往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
(2)健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。一个线程挂掉,因为线程共享一块资源。其他线程也会挂掉。进而导致进程退出,资源被回收。
(3)缺乏访问控制:
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。多线程访问临界资源,它的访问控制是由编程者决定。
(4)编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值