嵌入式学习——(Linux高级编程——线程)

news2024/11/26 14:49:40

线程

一、pthread 线程概述

pthread(POSIX threads)是一种用于在程序中实现多线程的编程接口。它与进程一样,可以用于实现并发执行任务,但与进程相比有一些不同的特点。

二、优点

1. 比多进程节省资源:进程在创建时需要分配独立的内存空间(0 - 3G),而线程在启动时只需要较小的空间(8M)。此外,线程之间可以共享进程的资源,如内存、文件描述符等,减少了资源的重复分配和占用。

2. 可以共享变量:线程之间可以直接访问共享的内存区域,这使得它们之间的数据共享更加高效和方便。相比之下,进程之间的数据共享需要通过复杂的进程间通信机制。

三、概念和特征

1. 概念:线程被称为轻量级进程,通常是一个进程中的多个任务。进程是系统中最小的资源分配单位,而线程是系统中最小的执行单位。一个进程可以包含多个线程,默认情况下每个进程至少有一个主线程。

2. 特征:
◦ 共享资源:线程可以共享进程的内存空间和其他资源,这使得线程之间的数据交换更加高效。
◦ 效率高:相比多进程,线程的创建和切换开销较小,可以提高程序的执行效率,通常可以提高约 30% 的效率。
◦ 三方库支持:pthread 提供了一套跨平台的线程编程接口,包括clone等系统调用和 POSIX 标准支持,便于移植。在编写代码时需要包含头文件pthread.h,编译时需要加载-lpthread库,线程函数的实现通常在libpthread.so库中。例如,可以使用gcc 1.c -lpthread命令编译包含线程的程序。

四、缺点

1. 稳定性稍差:与进程相比,线程的稳定性稍微差一些。由于线程共享进程的地址空间,如果一个线程出现错误,可能会影响到其他线程甚至整个进程的稳定性。

2. 调试相对麻烦:使用 GDB 调试多线程程序相对复杂,GDB 只能跟踪其中一条线程分支。可以使用info thread命令查看线程信息,然后使用thread命令切换到特定的线程进行调试。
在实际编程中,需要根据任务的特点和需求来选择使用进程还是线程。如果任务复杂,需要独立的资源和更高的稳定性,可以选择使用进程;如果任务相对简单,需要高效的数据共享和并发执行,可以选择使用线程。

线程与进程区别:

一、资源方面

1. 线程:
◦ 相比进程,线程多了共享资源的特性。线程可以共享所属进程的大部分资源,如内存空间、文件描述符等,这使得线程之间的数据交换和通信更加高效,类似于进程间通信(IPC)但更加便捷。
◦ 同时,线程又具有部分私有资源,其中最主要的是私有栈区。每个线程都有自己独立的栈空间,用于存储函数调用的栈帧、局部变量等,保证了线程在执行过程中的独立性。
2. 进程:
◦ 进程间只有私有资源,没有共享资源。每个进程都有独立的内存空间、文件描述符表、打开的文件列表等资源,进程之间的资源相互隔离,不能直接访问对方的资源。

二、空间方面

1. 进程:
◦ 进程空间独立,不同的进程拥有各自独立的地址空间。这意味着一个进程不能直接访问另一个进程的内存空间,它们之间的通信需要通过特定的机制,如管道、消息队列、共享内存等方式进行。
2. 线程:
◦ 线程可以共享空间,因为它们都在所属进程的地址空间内运行。线程之间可以直接通过共享的内存进行通信,无需像进程间通信那样使用复杂的机制。这种直接通信的方式使得线程之间的数据交换更加快速和高效,但也需要注意同步和互斥问题,以避免数据竞争和不一致性。

线程设计框架概述

POSIX 线程提供了一套用于多线程编程的标准接口。其设计框架主要包括创建多线程、线程空间操作以及线程资源回收等步骤。

创建多线程(pthread_create函数)

1.

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

这个函数用于创建一个新的线程。
◦ thread:是一个输出参数,用于存储新创建线程的 ID。在调用之前需要先定义并传入一个pthread_t类型的变量,函数执行成功后,该变量将被赋予新线程的 ID。
◦ attr:用于指定线程的属性,一般设置为NULL表示使用默认属性。
◦ start_routine:是一个函数指针,指向新线程要执行的函数。这个函数接收一个void*类型的参数,并返回一个void*类型的值。通常被称为回调函数,它定义了线程的执行空间。
◦ arg:是传递给回调函数的参数。可以通过这个参数向新线程传递数据。

2. 注意事项:
◦ 一次pthread_create执行只能创建一个线程。
◦ 每个进程至少有一个线程称为主线程。如果主线程退出,那么所有创建的子线程也会退出。
◦ 主线程必须有子线程同时运行才算多线程程序。
◦ 线程 ID 是线程的唯一标识,由 CPU 维护的一组数字。可以使用pstree命令查看系统中多线程的对应关系。多个子线程可以执行同一回调函数。
◦ ps -eLf命令可以查看线程相关信息(Low Weight Process)。ps -eLo pid,ppid,lwp,stat,comm也可以展示一些线程相关的信息。

获取当前线程 ID(pthread_self函数)

1.

pthread_t pthread_self(void);

这个函数用于获取当前线程的线程 ID。
◦ 功能:获取当前正在执行的线程的 ID。
◦ 参数:无。
◦ 返回值:成功时返回当前线程的 ID,类型为pthread_t(通常是一个无符号长整型,可使用%lu格式输出)。失败时返回 -1。
◦ 另一种获取线程 ID 的方式是通过系统调用syscall(SYS_gettid)。 

创建多线程、打印线程id、验证变量共享:

这段代码创建了两个线程和一个主线程,通过共享全局变量a展示了线程之间的数据共享和执行顺序的不确定性。线程 1 先执行并改变了全局变量a的值,线程 2 在延迟 1 秒后输出改变后的a的值,主线程在创建两个子线程后进入无限循环以保持程序运行。 

线程的退出

一、自行退出(自杀)

 void pthread_exit(void *retval);

这个函数用于子线程自行退出。
◦ 功能:当子线程调用这个函数时,子线程会立即退出。它允许子线程在完成任务后或者出现错误时主动退出执行。
◦ 参数:retval是线程退出时候的返回状态,类似于 “临死遗言”。其他线程可以通过特定的方式获取这个返回值,以了解子线程退出的原因或状态。例如,可以在另一个线程中使用pthread_join函数来获取子线程的退出状态。
◦ 返回值:无。

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

void *th(void* arg)
{
    // 输出子线程的线程 ID
    printf("th tid:%lu\n", pthread_self());

    // 子线程通过调用 pthread_exit(NULL) 退出,等效于 return NULL;
    // 这表示子线程正常结束,没有特定的返回值
    pthread_exit(NULL);
}

int main(int argc, const char *argv[])
{
    // 输出主线程的线程 ID
    printf("main tid:%lu\n", pthread_self());
    pthread_t tid;
    // 创建子线程
    pthread_create(&tid, NULL, th, NULL);

    while (1)
        sleep(1);
    // 主线程进入无限循环,防止主线程过早退出,否则子线程也会跟着退出

    return 0;
}

二、强制退出(他杀)

int pthread_cancel(pthread_t thread);

这个函数用于主线程强制结束子线程。
◦ 功能:主线程可以调用这个函数来请求结束一个特定的子线程。这类似于一种 “他杀” 行为,主线程主动干预子线程的执行,要求其终止。
◦ 参数:thread是要请求结束的子线程的线程 ID(tid)。
◦ 返回值:成功时返回 0;失败时返回 -1。

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

// 线程函数 1,不断输出“接受控制”
void *th1(void* arg)
{
    while (1)
    {
        printf("接受控制\n");
        sleep(1);
    }
    return NULL;
}

// 线程函数 2,不断输出“发送视频”
void *th2(void* arg)
{
    while (1)
    {
        printf("发送视频\n");
        sleep(1);
    }
    return NULL;
}

int main(int argc, const char *argv[])
{
    pthread_t tid1, tid2;
    // 创建第一个线程,执行 th1 函数
    pthread_create(&tid1, NULL, th1, NULL);
    // 创建第二个线程,执行 th2 函数
    pthread_create(&tid2, NULL, th2, NULL);

    int i = 0;
    while (1)
    {
        // 每循环一次,i 加 1
        if (i == 3)
            // 当 i 等于 3 时,强制取消 tid1 对应的线程(即第一个线程)
            pthread_cancel(tid1);

        if (i == 5)
            // 当 i 等于 5 时,强制取消 tid2 对应的线程(即第二个线程)
            pthread_cancel(tid2);

        sleep(1);
        i++;
    }
    return 0;
}

线程的回收

一、线程回收机制

1. 与进程不同,线程没有孤儿线程和僵尸线程的概念。在进程中,如果父进程先于子进程结束,子进程可能成为孤儿进程,由系统的 init 进程收养;如果子进程结束但父进程未及时回收其资源,子进程会成为僵尸进程。而在线程中,主线程结束时,任意生成的子线程都会结束;同时,子线程的结束不会影响主线程的运行。

二、pthread_join函数

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

这个函数用于回收指定线程的资源。
◦ 功能:它可以将指定的线程资源进行回收,并且具有阻塞等待功能。如果指定的线程没有结束,回收线程的调用者(通常是主线程或其他线程)会被阻塞,直到目标线程结束。这样可以确保线程资源被正确回收,避免资源泄漏。
◦ 参数:
◦ thread是要回收的子线程的线程 ID(tid)。
◦ retval是一个二级指针,用于接收要回收的子线程的返回值或状态。通过传递二级指针,可以在函数内部修改指针所指向的内容,从而获取子线程的返回状态。例如,如果子线程通过pthread_exit(值)退出,可以在回收线程中通过retval获取这个退出值。
◦ 返回值:成功时返回 0;失败时返回 -1。

三、子线程的回收策略

1. 如果预估子线程可以在有限范围内结束,则正常使用pthread_join等待回收。这种情况下,回收线程可以确定子线程会在可预期的时间内结束,因此可以通过阻塞等待的方式确保资源被正确回收。
2. 如果预估子线程可能休眠或者阻塞,则可以等待一定时间后强制回收。例如,可以使用pthread_timedjoin_np函数在指定的时间内等待子线程结束,如果超时则强制回收资源。这种策略适用于子线程可能进入长时间休眠或阻塞状态,而回收线程不能无限期等待的情况。
3. 如果子线程已知必须长时间运行,则不再回收其资源。在某些情况下,子线程可能需要一直运行,例如作为守护线程提供持续的服务。这种情况下,可以不回收子线程的资源,让其在后台持续运行。但需要注意,不回收资源可能会导致资源泄漏,因此需要谨慎使用这种策略,并确保子线程在不再需要时能够正确退出。

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

void *th(void* arg)
{
    // 在子线程中动态分配 20 个字节的内存空间
    char *p = (char*)malloc(20);
    // 将字符串"hello"复制到动态分配的内存空间中
    strcpy(p, "hello");
    // 子线程休眠 3 秒,模拟子线程执行一些耗时操作
    sleep(3);
    // 返回动态分配的内存地址,作为子线程的返回值
    return p;
}

int main(int argc, const char *argv[])
{
    pthread_t tid;
    // 创建一个子线程
    pthread_create(&tid, NULL, th, NULL);

    void *ret = NULL;
    // 等待子线程 tid 结束,并获取子线程的返回值,存储在 ret 中
    pthread_join(tid, &ret);

    // 输出子线程返回的字符串内容
    printf("ret = %s\n", (char*)ret);
    // 注意,这里需要强制转换 ret 为 char* 类型才能正确输出字符串

    // 释放子线程动态分配的内存空间
    free(ret);

    return 0;
}

这段代码创建了一个子线程,子线程在堆上动态分配内存并存储一个字符串,然后主线程等待子线程结束并获取其返回值(即动态分配的内存地址),最后输出子线程的字符串内容并释放动态分配的内存空间。通过这种方式,主线程可以获取子线程的执行结果并正确管理资源。

线程的参数、返回值

一、传参数

1. 传整数:
◦ 普通函数传参示例:int add(int a, int b);中a和b是形参,在调用时如add(x, y);中x和y是实参。
◦ 线程传参:pthread_create(&tid, NULL, fun, x);中最后一个参数可以用来向线程函数传递一个整数参数。在线程函数void * fun(void * arg);中,通过将arg强制转换为合适的类型来获取传入的参数。
2. 传字符串:
◦ 栈区字符数组:在函数内部定义的字符数组,如char buf[] = "";,这种方式定义的字符数组在函数执行完毕后其内存空间可能会被回收,不适合在线程间传递。
◦ 字符串常量:char *p = "hello";,字符串常量存储在静态存储区,其地址在程序运行期间一直有效,但由于其内容不可修改,也不太适合作为线程间传递的参数。
◦ 堆区字符串:char *pc = (char *)malloc(128);,通过动态分配内存的方式创建的字符串,可以作为参数传递给线程函数。在主线程中创建子线程时使用pthread_create(&tid, NULL, fun, pc);,在子线程函数fun(void *arg)中,将arg强制转换为char*类型,即可访问传入的字符串。在主线程中需要在合适的时候使用free(pc);释放动态分配的内存。
3. 传结构体:
◦ 定义结构体类型,例如typedef struct { int a; char b[20]; } MyStruct;。
◦ 用结构体定义变量,如MyStruct myStruct;。
◦ 向pthread_create传结构体变量,pthread_create(&tid, NULL, fun, &myStruct);。
◦ 从子线程中获取结构体数据,在子线程函数fun(void *arg)中,将arg强制转换为结构体指针类型,如MyStruct *myStructPtr = (MyStruct *)arg;,然后就可以访问结构体中的成员。
二、返回值
1. pthread_exit(0)可以改为pthread_exit(9);等,这里传递的参数是一个void*类型的指针,可以指向任何数据类型。例如,pthread_exit可以返回一个整数的地址,如int * p = malloc(4); *p = -10; pthread_exit(p);。
2. pthread_join(tid, NULL);可以改为pthread_join(tid,?);,其中第二个参数是一个二级指针,用于接收子线程的返回值。例如,void **retval; pthread_join(tid, retval);。
子线程退出时可以返回一个内存地址,这个地址所在的内存中可以存储任何数据。但是要注意地址的有效性:
• 栈区变量:错误,子线程结束后栈区变量的地址失效,不能作为返回值。
• 全局变量:失去意义,因为主线程可以直接访问全局变量,不需要通过子线程返回。
• 静态变量和堆区变量:可以作为子线程的返回值,因为它们的内存空间在子线程结束后仍然有效。
主线程通过一个地址形式的变量来接受子线程返回的地址变量,就可以将该地址中的数据取到。

设置线程分离属性

int pthread_detach(pthread_t thread);

这个函数用于设置指定线程的分离属性。
1. 功能:设置指定线程为分离状态。当一个线程被设置为分离状态后,一旦该线程结束执行,它的资源会被自动回收,而不需要其他线程通过调用pthread_join来回收资源。这对于那些不需要被其他线程等待或管理的线程非常有用,可以避免资源泄漏和不必要的等待。
2. 参数:thread是要设置分离属性的线程 ID。通常是在创建线程后,将新创建的线程 ID 传入这个函数来设置分离属性。

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

void* th(void* arg)
{
    // 将当前线程设置为分离状态,这样当线程结束时,系统会自动回收资源
    pthread_detach(pthread_self());
    // 输出当前线程的 ID
    printf("tid : %lu\n", pthread_self());

    return NULL;
}

int main(int argc, const char *argv[])
{
    pthread_t tid;
    int i;
    for (i = 0; i < 55000; i++)
    {
        // 创建新线程
        int ret = pthread_create(&tid, NULL, th, NULL);
        if (ret!= 0)
        {
            // 如果创建线程失败,跳出循环
            break;
        }
    }
    // 输出成功创建的线程数量
    printf("i = %d\n", i);

    return 0;
}

在这段代码中,main函数创建了多个线程,每个线程在执行时都会将自身设置为分离状态。这样,当这些线程结束执行时,系统会自动回收它们的资源,无需主线程通过pthread_join来回收。循环的目的是创建尽可能多的线程,直到创建线程失败为止,然后输出成功创建的线程数量。这种方式可以测试系统在一定资源条件下能够创建的线程数量上限。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2061000.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何用Java SpringBoot+Vue搭建太原学院商铺管理系统【实战教程】

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

【中项第三版】系统集成项目管理工程师 | 第 12 章 执行过程组

前言 本章属于10大管理的内容&#xff0c;上午题预计会考8-10分&#xff0c;下午案例分析也会进行考查。学习要以教材为主。 目录 12.1 指导与管理项目工作 12.1.1 主要输入 12.1.2 主要输出 12.2 管理项目知识 12.2.1 主要输入 12.2.2 主要输出 12.3 管理质量 12.3.…

新160个crackme - 037-fireworx.2

运行分析 图标是火炬&#xff0c;估计是Delphi程序&#xff0c;需要破解Name和Serial PE分析 Delphi程序&#xff0c;32位&#xff0c;无壳 静态分析&动态调试 - ida找到关键字符串&#xff0c;双击进入 ida动态调试&#xff0c;LStrCatN是一个拼接函数Serial v10 Name …

兴顺物流管理系统pf

TOC springboot539兴顺物流管理系统pf 第一章 课题背景及研究内容 1.1 课题背景 信息数据从传统到当代&#xff0c;是一直在变革当中&#xff0c;突如其来的互联网让传统的信息管理看到了革命性的曙光&#xff0c;因为传统信息管理从时效性&#xff0c;还是安全性&#xff…

VAuditDemo审计之安装教学

目录 Xampp安装VAditDemo 第一步&#xff1a; 第二步&#xff1a; 第三步&#xff1a; 第四步&#xff1a; 第五步&#xff1a; 第六步&#xff1a; 第七步&#xff1a; 第八步&#xff1a; Xampp安装VAditDemo 第一步&#xff1a; 解压VAuditDemo 先上传好zip文件…

PL3322B 升级为PC3322C

PL3322C 是一款高效率、高集成度、原边调节的电源驱动芯片&#xff0c;PL3322C可去除光耦以及次级控制电路&#xff0c;简化了充电器/适配器等传统的恒流/恒压的设计&#xff0c;高精度的电压和电流调节。PL3322C提供SOT23-6封装&#xff1b;PL3322C集成多种保护功能&#xff0…

ffmpeg6.1集成Plus-OpenGL-Patch滤镜

可参考上一篇文章。ffmpeg6.1集成ffmpeg-gl-transition滤镜-CSDN博客 安装思路大致相同&#xff0c; 因为 Plus-OpenGL-Patch也是基于 ffmpeg 4.x 进行开发的&#xff0c;所以在高版本上安装会有很多报错。 这是我安装后的示例&#xff0c;需要安装教程或者改代码可私信我。 …

工程技术研究杂志工程技术研究杂志社工程技术研究编辑部2024年第12期目录

工程前沿 基于Midas Civil的钢结构人行天桥荷载试验和承载力评价 陈新祥;刘欣;吕美忠; 1-5 基于价值工程理论的基坑支护方案优选 王晓毅;吴军杰; 6-8 建筑工程桩基检测中存在的问题及优化策略研究 张旭;李德君; 9-11 挤密桩参数对黄土地基加固效果的影响研究 杨…

开源介绍 - html2canvas

What is html2canvas &#xff1f; The script allows you to take “screenshots” of webpages or parts of it, directly on the users browser. The screenshot is based on the DOM and as such may not be 100% accurate to the real representation as it does not mak…

Java 入门指南:集合概述

Java集合概述 Java 集合&#xff08;Collections&#xff09;是 Java 中提供的一种容器&#xff0c;用于存储和管理多个对象。与数组不同&#xff0c;集合的长度是可变的&#xff0c;且只能存储对象&#xff08;包括对象的引用&#xff09;&#xff0c;不能存储基本数据类型。…

jmeter中导入java方法并使用

1、首先打开idea&#xff0c;在idea中点击File-New-Project ,创建一个项目&#xff0c;项目名为JmtOne 2、项目创建完成后&#xff0c;直接在main函数中定义两个方法&#xff0c;记住该主函数的包名跟类名&#xff0c;后面会用到&#xff0c;在类中定义了两种拆分字符串的方法&…

FLUX.1 当前支持FLUX.1的Lora模型

首先&#xff0c;我们来梳理一下本次Xlabs-AI团队放出的七款Lora模型&#xff0c;分别是 写实、Furry&#xff08;但是目前该模型还不支持ComfyUI&#xff09;、MJV6、动漫、迪斯尼、风景和艺术风格。每种模型都经过精心训练&#xff0c;能够为 Flux 模型带来卓越的风格转换能…

嵌入式day31

mplayer项目问题分析&#xff1a; 知识短时间内可以获取到 能力的提升一定需要练习 IPC 进程间通信方式 共享内存 //最高效的进程间通信方式 共享内存&#xff1a; 1.是一块 内核预留的空间 2.最高效的通信方式 //避免了用户空间到内核空间的数据拷贝 操作&#xff1a; …

第三方软件测评机构进行安全测试的好处

根据报告显示&#xff0c;2023年全球因网络安全事件造成的损失是8万亿美元&#xff0c;其中70%是由于软件漏洞引起的。这一数据足以表明&#xff0c;企业在软件开发过程中必须重视安全测试。软件安全测试不仅是对软件质量的保障&#xff0c;更是对用户信息和财务安全的保护。 …

QT事件机制理解

事件和信号 从硬件层来看: 事件就是一种中断&#xff0c; 中断的产生形式: 1.用户操控硬件所产生的中断。 2.由系统自身所产生的中断&#xff0c;比如说定时器。 这种中断由系统内核监控&#xff0c;由系统内核接收到中断并向CPU发出的执行请求就叫信号。所以说事件是信号产生…

《Cloud Native Data Center Networking》(云原生数据中心网络设计)读书笔记 -- 06容器网络

本章将回答以下问题 Linux上可用的容器网络组件有哪此?各种选择的限制和性能特征是什么? 命令空间 命名空间是一个 Linux 内核虚拟化组件。该组件类似 Linux 内核提供的网络和服务器虚拟化的组件。命名空间对内核管理的特定的资源进行虚拟化&#xff0c;允许一个虚拟资源有…

Moodle与ONLYOFFICE集成如何实现智能教学管理

陈老老老板&#x1f934; &#x1f9d9;‍♂️本文专栏&#xff1a;生活&#xff08;主要讲一下自己生活相关的内容&#xff09;生活就像海洋,只有意志坚强的人,才能到达彼岸。 &#x1f9d9;‍♂️本文简述&#xff1a;ONLYOFFICE相信大家已经有所了解&#xff0c;本篇讲一下如…

2024年10月湖北省自学考试报名流程(5步)

2024年10月湖北省自学考试报名流程&#xff08;5步&#xff09; #自学考试 #自考报名 #湖北自考 #湖北自考本科 #湖北自考报名 #湖北自考专科 #湖北省自学考试

Linux信号机制探析--信号的产生

&#x1f351;个人主页&#xff1a;Jupiter. &#x1f680; 所属专栏&#xff1a;Linux从入门到进阶 欢迎大家点赞收藏评论&#x1f60a; 目录 &#x1f4da;信号什么是信号&#xff1f;为什么要有信号&#xff1f;查看Linux系统中信号 &#x1f388;信号产生&#x1f4d5;kill…

【分数序列相加】求出一分数序列数列的前20列之和

有一分数序列&#xff1a;1/2,3/2,5/3,8/5,13/8,21/13...求出这个数列的前20列之和 #include<stdio.h>int main(){int t,number20;float a2,b1,s0;for(int i1;i<number;i){ssa/b;ta;aab;bt;}printf("%f\n",s);return 0; } 输出结果&#xff1a; 觉得有帮助…