linux线程,线程控制与线程相关概念

线程概念

线程这个词或多或少大家都听过,今天我们正式的来谈一下线程;

在我一开始的概念中线程就是进程的一部分,一个进程中有很多个线程,这个想法基本是正确的,但细节部分呢我们需要细细讲解一下;

什么是线程

1.线程是进程执行流中的一部分,就是说线程是进程内部的一个控制序列;

2.线程是操作系统调度的基本单位;

3.在linux中没有真正意义上的线程,也就是操作系统中说的tcb(thread ctrl block),但是其他的操作系统是有的不同的操作系统实现不同(如windows就是在pcb下再次构建了tcb的数据结构);为什么linux下没有真正意义的线程呢?因为线程再操作系统中也是需要被管理的,可是线程的管理一定得创建数据结构,创建复杂的数据结构一定需要增加维护的成本与难度,而线程的管理其实和进程是相似的;所以聪明的linux程序员将线程管理设计为了轻量化的进程,将线程与进程统一管理,减轻了代码的复杂度,便于维护提高效率;(线程粒度细于进程

4.线程其实是进程的一部分,所以线程运行的地方就是在进程的虚拟地址空间中的;因为线程本身也是属于进程的一部分的,只是被加载到了进程队列中运行而已;(进程就像是一个家庭,线程就像是家庭中的每一个人,每个人都有自己的工作,所以需要分开执行,也就是处于进程队列中),进程会分配的资源给线程(家庭中的资源会分配给每个人,比如爸爸要去远的地方工作需要开车,那车子这个资源就会分配给父亲),这个资源包括代码和数据,之前我们理解的进程可以当作是主线程,通过分配自己的代码给它内部的线程,内部的线程拿到数据和代码资源区执行分配给它的工作,从而执行相应的操作;

重谈虚拟地址空间

页表如何映射

计算页表大小

 所以一个页表最大为4mb,并且一个页表的二级页表不一定为1024个,因为页表的映射也不是一次就完成的,而已页表的映射使用完之后还会释放等;所以一个页表大小不会大于4mb;

就是这样的页表完成了我们的映射;那我们的数据和代码都是存储在这个地址空间上的;而函数就是一个现成的地址,所以我们分配给线程代码数据,是不是可以直接将这个函数分给线程呢?这样不就等于把线程需要执行的工作划分给了线程吗?

所以线程划分资源本质上是将地址空间中的资源进行分配

为什么我们要创建线程?线程优点

1.同一进程中线程之间的切换更加轻量化;

在我们的内存中最快的是寄存器,,cpu之间拿寄存器中的数据进行计算,寄存器也需要获取数据,而寄存器不是之间从内存中拿数据的,因为内存相较于寄存器还是太慢了,所以它们之间还有一个cache缓存,这个cache中存放的是当前进程的数据和指令,寄存器可以很快的就从cache中拿到一个进程中的数据(cache命中率会很高,因为都在同一进程,都是热数据);因为同一进程中的线程是共享数据的,所以cache切换时只需要切换task_struct,而进程之前切换所有数据都需要切换(进程切换了,进程间具有独立性,cache中的数据一定都需要被切换,所咦数据会变冷重新去命中数据),这样的切换消耗会大的多;

2.创建和销毁线程的代价要小很多;因为线程的数据已经在内存中了,线程只需要从它所在的进程中获取数据即可;

3.io密集型程序,通过多线程可以提高很大的效率,在进行io的时候进程可以让其他线程进行计算等操作,不需要等待io结束再操作;相比单线程的等待要优化非常多;

4.计算密集型程序,在单核cpu中多线程没有什么提升,想法,线程之间的切换还会降低效率;但是在多核cpu中,多线程可以在多个核上进行计算(计算线程数要小于等于核的数量),也是大大提高了计算的效率的;

线程缺点:

由于线程之前没有独立性,共享进程代码数据,代码的健壮性要低一些,所以需要进行同步于互斥;缺乏访问控制->健壮性低;相应的调试也会更难;

线程数据 

每个线程虽然都是进程的一部分,从进程中获得数据的,但是线程一定需要包含自己的数据;

线程自己的数据:

1.线程对应的上下文数据(寄存器)

2.线程运行时数据(独立的栈空间)

3.线程id

4.信号屏蔽字

5.调度优先级

 6.errno

线程操作

上面讲解了线程的基本内容,下面我们来对线程进行操作来理解线程;

我们需要先了解这些linux中posix标准中的原生线程库中的函数; 

线程创建

pthread_create

这个函数是用来创建子线程的;

第一个参数是一个输出型参数,用来输出创建线程的tid;

第二个参数是用来设置线程的属性的,其实这是一个指向线程属性对象的指针,通过传递我们设置好的对象传递给线程从而改变线程的默认属性,一般我们都传递NULL使用默认属性即可;

第三个参数是一个回调函数,用来提供给线程运行的代码,可以理解为让线程执行此函数;

第四个参数就是一个传递给函数(第三个参数——回调函数)的参数,这个参数既可以是普通的内置类型,也可以是结构体,这样可以很多的数据;

返回值返回0为成功创建,创建失败返回返回错误码,不设置errno;

 下面可以看到我们的代码成功运行了;

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

void *routine(void *data)
{
    for (int i = 0; i < 5; i++)
    {
        usleep(100000);
        cout << "线程1, pid: " << getpid() << endl;
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, nullptr);
    for (int i = 0; i < 7; i++)
    {
        usleep(200000);
        cout << "线程0, pid: " << getpid() << endl;
    }
    return 0;
}

 从上面的现象我们可以清楚的知道线程是一个独立的执行流虽然routine函数和main函数它们两个再同一个程序中且是两个循环但是,这两个循环同时跑起来了,所以证明了线程的独立性;

编译时需要加-lpthread选项

在linux中使用原生线程库进行编程时我们编译选项总是需要带上-lpthread,这个选项在我们前面学习动静态库的时候就很熟悉了,用来连接指定的库;而似乎我们在以往的编程中除开我们自己创建动静态库的情况之外,我们从未出现过主动连接动静态库的情况;

为什么我们不需要主动去连接呢?这是因为编译器自动去帮我们连接了,我们的c,c++语言级别的库也好,linux的系统库也罢,它们库的路径都是已经存储在编译器的配置文件中的,编译器可以自动的找到库(第一步),然后编译器会自动连接这些库(第二步);为什么会自动连接呢?我们可以认为这些系统库和标准库是编译器自己的库,所以编译器会自动的连接;而pthread这个库是posix标准中的原生线程库;它是属于第三方库的,而第三方库即使它被放到系统,标准库的路径之下,它也是不会被自动连接的;所以我们需要带上-lpthread选项去主动连接这个库;

查看线程

我们看到的线程的现象接下来,我们从系统的角度的入手,使用系统的指令来查看我们的线程的体现;

ps -aL

lwp的全称是light weight process轻量级进程; 

线程的等待与tid获取函数

pthread_join

子进程被创建,父进程需要等待进程返回,而线程被创建也需要被等待,但是这里只有主线程和其他线程的区别,主线程需要等待其他所有线程,防止内存泄漏的问题;

 这里的第一个参数是指向被等待线程的tid;

第二个参数是一个输出型参数可以用来接收线程的返回值,这个返回值可以是任意类型的数据(自定义类型也可以);

返回值为0代表等待成功,非0则返回错误值,不设置errno码;

pthread_self

可以获得线程的tid;

这是一个无参函数和getpid的使用方式是一样的;

代码实现 

 知道了这些基本的函数后,我们下面用代码实践来展示现象并解释:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

struct thread_data
{
    string threadName;
    string threadReturn;
};

void *routine(void *data)
{
    thread_data *d1 = static_cast<thread_data *>(data);
    // thread_data *d1 = (thread_data *)(data);
    int count = 3;
    for (int i = 0; i < count; i++)
    {
        printf("tid: %p threadName: %s count: %d\n", pthread_self(), d1->threadName.c_str(), i);
        sleep(1);
    }
    // int a=5/0;//除0错误 这里说明了当进程中的某个线程出现异常时,整个进程都会退出
    // exit(0);//使用exit退出 这里也说明了使用exit会退出整个进程
    d1->threadReturn="return_"+d1->threadName;
    return d1;
}

void initThread(thread_data *data, int num)
{
    data->threadName = "thread_" + to_string(num);
}


int main()
{
    pthread_t tid;
    thread_data *data = new thread_data;
    initThread(data, 1);
    int ret_create = pthread_create(&tid, nullptr, routine, (void *)data);
    void *ret_thread;
    printf("我是主线程tid: %p\n",pthread_self());
    pthread_join(tid, &ret_thread);
    cout << ((thread_data *)ret_thread)->threadReturn << endl;//证明获得了一个类返回值

    delete data;
    return 0;
}

 使用return正常退出的情况:

下面是使用exit和异常退出的情况: 

 

通过代码和现象我们可以知道这些细节:

1. 我们可以使用join获取线程的返回值,线程返回值可以为任意类型的指针,所以可以传递任意值;

2.我们的子线程退出的时候不能使用exit退出这样会导致整个进程都退出,我们可以使用return,pthread_exit(后面讲),使用cacel取消joined(后面讲),这3种方式退出;

3.进程中的任意一个线程出现异常整个进程都会退出

4.线程的tid是一个地址,这个地址是进程堆栈之间的内存区域(通过上面的现象也可清楚的明白)

由此我们可以知道这些函数的大致使用;

线程结构体位置

上面我们通过概念与实现基本的了解了线程,接下来我们通过图像来了解线程的结构体:

其实我们的线程是这样存在在我们的进程中的,因为linux程序员为了减轻代码的维护效率linux中没有真正的线程,而是将线程作为轻量级进程,而用来描述轻量级进程的结构体是存储在用户层的,存储的位置就是共享区的原生线程库,线程库中维护了线程的属性数据,内核的执行流(tcb控制块)通过找到进程中的线程库中的线程结构体从而找到线程代码执行线程; 

所以线程的属性是由线程库来维护的,而tid之所以是共享区之中的代码的原因就是因为tid指的是共享区中线程库中的某个线程结构体所在的首地址;

线程空间的特点

1.线程之间的栈空间是独立的;

这一点非常好理解,因为函数在被调用的时候就会创建自己的栈帧嘛;而线程执行其实就是执行了分给他的函数;所以线程栈空间是独立的;

2.线程之间是没有秘密的;

为什么线程之间独立但是又没有秘密呢?因为线程总是在一个进程中的嘛,栈之间的数据,只需要通过一个指针就可以获得了;

代码示例:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <vector>
#include <string>
using namespace std;

struct threadData
{
    string threadName;
    threadData(int num)
    {
        threadName = "thread" + to_string(num);
    }
    threadData() = default;
};

int *g_index;

void *routine(void *args)
{
    int val = 0;
    threadData *data = (threadData *)args;
    for (int i = 1; i <=3 ; i++)
    {
        printf("%s tid: %p val: %d\n", data->threadName.c_str(), pthread_self(), val);
        val++;
    }
    if(data->threadName=="thread1")
    {
        val=10000;
        g_index=&val;
        sleep(5);
    }
    return (void *)0;
}

int main()
{
    vector<pthread_t> tids;
    for (int i = 0; i < 3; i++)
    {
        pthread_t tid;
        threadData *td = new threadData(i);
        pthread_create(&tid, nullptr, routine, td);
        tids.push_back(tid);
        sleep(1);
    }
    cout<<"这是thread2的val值: "<<*g_index<<endl;
    for(auto t:tids)
    {
        void *retData;
        pthread_join(t,&retData);
    }
    return 0;
}

但是如果我们想要获得某个栈空间的数据时这也是可以轻松做到的:

我们在routine函数中加入一段这样的代码,并在main函数中读取数据;

  routine函数中:

    if(data->threadName=="thread1")
    {
        val=10000;
        g_index=&val;
        sleep(5);
    }
main函数中:
    
       cout<<"这是thread2的val值: "<<*g_index<<endl;

 

线程的变量:__thread选项 

int *g_index;
//int g_val;
__thread int g_val;

void *routine(void *args)
{
    int val = 0;
    threadData *data = (threadData *)args;
    for (int i = 1; i <=3 ; i++)
    {
        //printf("%s tid: %p val: %d\n", data->threadName.c_str(), pthread_self(), val);
        //val++;
        printf("%s g_val: %d\n",data->threadName.c_str(),g_val);
        g_val++;
    }
    // if(data->threadName=="thread1")
    // {
    //     val=10000;
    //     g_index=&val;
    //     sleep(5);
    // }
    return (void *)0;
}

int main()
{
    vector<pthread_t> tids;
    for (int i = 0; i < 3; i++)
    {
        pthread_t tid;
        threadData *td = new threadData(i);
        pthread_create(&tid, nullptr, routine, td);
        tids.push_back(tid);
        sleep(1);
    }
    //cout<<"这是thread2的val值: "<<*g_index<<endl;
    for(auto t:tids)
    {
        void *retData;
        pthread_join(t,&retData);
    }
    return 0;
}

我们线程在使用 g_val全局变量时:

g_val带上__thread编译选项时:

 

__thread是编译选择,不是c,c++的语法是编译器的选项;

特点:

 1.将进程全局数据变为线程全局数据

2.只能给内置类型带上这个选项

C++线程库说明

在我们的C++中是有语言级别的线程库的(C语言没有),C++中的线程库是跨平台的,但是我们在使用C++线程库时,我们还是会发现,我们需要带上编译选项-lpthread所以说明C++的线程库是封装了原生线程库的,而原生线程库在linux中是posix标准的,在windows中又有不同的标准;但是C++的线程库是跨平台的,所以说明C++的线程库不仅封装了linux的posix标准线程库还封装了windows下的线程库;

clone系统调用的封装

我们前面说线程是轻量级的进程,为什么这么说呢?其实我们在创建线程时使用的pthread_create函数和创建子进程的fork函数都是封装了clone的系统调用;

int clone(int (*fn)(void *), void *child_stack
, int flags, void *arg
, ... /* pid_t *ptid, void *tls, pid_t *ctid */);

这个系统系统调用会指定一片栈空间给新开辟的线程,我们不需要懂clone调用的细节,我们只需要知道,linux中其实在底层上线程的接口也是和进程用的一样的调用,所以它们在内核层面上是处于同一级别的执行流的,所以线程被称为轻量级进程;

小提示:

线程如何使用进程替换的调用会将当前的整个进程替换掉

线程终止

前面我们说了线程的3个正常退出方式;我们下面来详细的讲解一下:

pthread_exit

这个函数就是和return一样的作用,返回一个retval给主线程;这里需要注意的是retval最好是堆上的指针,线程终止栈帧也会销毁,会导致栈上的数据被释放,所以返回值一定要是不被释放的数据;

pthread_cancel

这是一个线程终止函数,我们可以通过此函数终止掉tid的线程:

这里终止了就不需要再join了,如果join了会发返回非0值; 

这是gpt给出的提示:

尽管 pthread_cancel 函数可以请求取消另一个线程,但是线程是否真正被取消,以及何时被取消,是由目标线程自身来决定的。目标线程可以选择忽略取消请求,或者在适当的时机响应取消请求并执行清理操作。

 线程分离

pthread_detach

我们的主线程永远是最后退出的,因为需要等待所有创建进程退出,我们常见的服务器一般都是死循环不退出的程序;而当主线程不关系创建的线程的结果时,可以使用detach来断开创建线程与主线程之间的关系;,主线程就不需要等待子线程了;

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include<cstring>
using namespace std;

void* routine(void*args)
{
    cout<<"我是被创建线程"<<endl;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,routine,nullptr);
    void* ret;
    pthread_detach(tid);
    int ret_join=pthread_join(tid,&ret);
    printf("%s\n",strerror(ret_join));
    return 0;
}

当没有detach时:

当创建的线程被detach时

 所以说明线程不能被同时detach和join;

此外线程可以自己detach自己;

以上就是线程的控制与基本概念,线程部分未完待续; 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Web渗透-MySql-Sql注入:联合查询注入

SQL注入&#xff08;SQL Injection&#xff09;是一种网络攻击技术&#xff0c;攻击者通过将恶意的SQL代码插入到应用程序的输入字段&#xff0c;从而欺骗应用程序执行未经授权的操作。这种攻击方式可以导致严重的安全问题&#xff0c;包括&#xff1a; 数据泄露&#xff1a;攻…

HCIP-Datacom-ARST自选题库__BGP多选【22道题】

1.BGP认证可以防止非法路由器与BGP路由器建立邻居&#xff0c;BGP认证可以分为MD5认证和Keychain认证&#xff0c;请问以下哪些BGP报文会携带BCGP Keychain认证信息?(报头携带) open Update Notication Keepalive 2.传统的BGP-4只能管理IPv4单播路由信息&#xff0c;MP-B…

数据库(10)——图形化界面工具DataGrip

以后关于数据库的图片演示就使用DataGrip了 : ) 创建数据库和表 在连接上数据库之后&#xff0c;可以选择Schema创建一个新的数据库。 点击OK后&#xff0c;就已经创建了一个空的表。 要在数据库中建立一张新的表&#xff0c;右键数据库&#xff0c;点击new table 要给新表添…

【软考】下篇 第19章 大数据架构设计理论与实践

目录 大数据处理系统架构特征Lambda架构Lambda架构介绍Lambda架构实现Lambda架构优缺点Lambda架构与其他架构模式对比 Kappa架构Kappa架构介绍Kappa架构实现Kappa架构优缺点 常见Kappa架构变形&#xff08;Kappa、混合分析系统&#xff09;Kappa架构混合分析系统的Kappa架构 La…

数组长度属性的安排与深度学习中的数据类型探索

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、数组长度属性的理解与应用 1. 数组形状信息的获取 2. 数组形状的变换 3. 数组类型的指…

关于本人VIP付费文章说明

郑重声明&#xff1a;我写博客只是为了记录分享经验 自从上次写完数据结构系列后我就一直没有登陆&#xff0c;目前也没打算继续开新内容。今天偶然发现我之前写的文章被设为vip文章&#xff0c;要vip解锁才能看&#xff0c;我很确定当初我发布的时候选择的是公开&#xff0c;…

SpirngMVC框架学习笔记(一):SpringMVC基本介绍

1 SpringMVC 特点&概述 SpringMVC 从易用性&#xff0c;效率上 比曾经流行的 Struts2 更好 SpringMVC 是 WEB 层框架&#xff0c;接管了 Web 层组件, 比如控制器, 视图, 视图解析, 返回给用户的数据格式, 同时支持 MVC 的开发模式/开发架构SpringMVC 通过注解&#xff0c;…

【kubernetes】关于k8s集群的污点、容忍、驱逐以及k8s集群故障排查思路

目录 一、污点(Taint) 1.1污点介绍 1.2污点的组成格式 1.3当前 taint effect 支持如下三个选项&#xff1a; 1.4污点的增删改查 1.4.1验证污点的作用——NoExecute 1.4.2验证污点的作用——NoSchedule 1.4.3 验证污点的作用——PreferNoSchedule 1.5污点的配置与管理…

单链表的相关题目

1.删除链表中给定值val的所有结点 public void removeall(int key) {//由于是删除链表中所有和key值相同的结点,所以可以设置两个ListNode类型的数据,一个在前面,一个在后面.//直到前面的走到链表的最后,这样完成了遍历.//先判断一下这个链表是否为空if(headnull){System.out.…

脑图工具 在学习系统架构中的使用

系统&#xff0c;有人把它比作一个黑盒&#xff0c;有人比作一个树洞。呃&#xff0c;其实二者都隐含的表达了一个意思&#xff0c;盘根错节&#xff0c;一言难尽&#xff0c;欲说还休&#xff0c;说了又像是隔靴搔痒&#xff0c;感觉没说透。 学习&#xff0c;理解和展示一个…

边缘计算网关的用途及其使用方法-天拓四方

在数字化日益深入的今天&#xff0c;边缘计算网关作为一种重要的设备&#xff0c;正在越来越多地被应用于各种场景中。它不仅能够提升数据处理的速度和效率&#xff0c;还能在降低网络延迟的同时确保数据的安全性。本文将详细介绍边缘计算网关的用途及其使用方法&#xff0c;帮…

MFC里的工具栏按钮图标如何使用外部图片?

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

Revit——(2)模型的编辑、轴网和标高

目录 一、关闭缩小的隐藏窗口 二、标高&#xff08;可创建平面&#xff0c;其他标高线复制即可&#xff09; 三、轴网 周围的四个圈和三角表示四个里面&#xff0c;可以移动&#xff0c;不要删除 一、关闭缩小的隐藏窗口 二、标高&#xff08;可创建平面&#xff0c;其他标…

[书生·浦语大模型实战营]——在茴香豆 Web 版中创建自己领域的知识问答助手

茴香豆是一个基于LLM的领域知识助手&#xff0c;可以用于解答群聊中的问题。接下来是创建过程。 1.打开茴香豆Web版&#xff0c;创建自己的领域库。 地址&#xff1a;茴香豆Web版 这里类似于注册账号&#xff0c;你输入知识库的名称以及密码&#xff0c;然后它就会创建一个知识…

Collection(一)[集合体系]

说明&#xff1a;Collection代表单列集合&#xff0c;每个元素&#xff08;数据&#xff09;只包含一个值。 Collection集合体系&#xff1a; Collection<E> 接口 (一&#xff09;List<E> 接口 说明&#xff1a;添加的元素是有序、可重复、有索引。 1. ArrayLi…

Ai终点站,全系统商业闭环矩阵打造,帮电商、实体降70%成本,12款Ai联合深度实战

说白了&#xff0c;你之前5个人的团队&#xff0c;当团队人数不变的情况下&#xff0c;借助于ChatGPT和各种软件的结合&#xff0c;赋能电商直播带货&#xff0c;可以让之前一年销售额2.000万变成2.500万或者是3.000万&#xff0c;这就是这套课程的核心作用: 【1】系统课程从1…

SpringCloud之SSO单点登录-基于Gateway和OAuth2的跨系统统一认证和鉴权详解

单点登录&#xff08;SSO&#xff09;是一种身份验证过程&#xff0c;允许用户通过一次登录访问多个系统。本文将深入解析单点登录的原理&#xff0c;并详细介绍如何在Spring Cloud环境中实现单点登录。通过具体的架构图和代码示例&#xff0c;我们将展示SSO的工作机制和优势&a…

Excel单元格格式无法修改的原因与解决方法

Excel单元格格式无法更改可能由多种原因造成。以下是一些可能的原因及相应的解决方法&#xff1a; 单元格或工作表被保护&#xff1a; 如果单元格或工作表被设置为只读或保护状态&#xff0c;您将无法更改其中的格式。解决方法&#xff1a;取消单元格或工作表的保护。在Excel中…

【接口测试_04课_Jsonpath断言、接口关联及加密处理】

一、Jasonpath的应用 JsonPath工具网站&#xff1a;JSONPath解析器 - 一个工具箱 - 好用的在线工具都在这里&#xff01; 1、JSONPath的手写与获取 手写JSONPath 1、 $ &#xff08;英文美元符号&#xff09;代表外层的{} . &#xff08;英文句号&#xff09;表示当前…

分频器对相位噪声影响

本文我们将分析输入时钟被N分频之后的输出时钟的相位噪声如何变化。首先理想分频器的意思是我们假设分频器不会引入附加相位噪声&#xff0c;并且输入和输出时钟之间没有延时。我们假设每一个输出边沿的位置都完美的与输入边沿相对齐&#xff0c;这样便于分析。由于每N个输入时…