Linux——线程详解(一)

news2024/11/18 5:39:15

索引

  • 初识线程
      • 1.inux下的线程
      • 2.再谈进程
      • 3.理解页表
      • 4. 再次理解虚拟到物理的转化
  • 线程的控制
      • 1.线程的创建
      • 2.线程异常
      • 3.验证`pthread_join` 的第二个参数
      • 4.线程的退出方式
      • 5. 线程的公有和私有
      • 6.pthread_t 与线程独立栈
      • 7.线程的局部性存储
      • 8.线程分离

初识线程

1.inux下的线程

在这里插入图片描述
之前了解到,当一个进程被创建的时候,进程的task_struct被创建,进程的数据和代码通过页表的映射加载到物理内存。CPU选择一个进程调度就是将进程task_struct的地址load到寄存器当中,这样CPU就能很快找到这个地址,并且也可以将页表的起始地址也load到寄存器中,通过页表就能完成虚拟地址到物理地址的映射,由于task_struct和页表的上下关系都有,所以CPU内部是能快速的找到进程的所有数据的。
由于我们再创建一个进程,那么又是重复完成上述的一系列工作,成本非常高。

在这里插入图片描述
如果此时我们只创建PCB,并且是这几个PCB指向同一个地址空间,共用一张页表并且将进程的代码和数据通过函数划分成几部分,让各个PCB执行自己的部分代码和数据,各个PCB各自使用部分页表来完成映射,所以各个PCB完成的都是一部分 ——这就是Linux下粗粒度的线程。
所以就可以引出线程的几个基本概念:

1.线程是在进程的地址空间内运行的,是进程内部的一个执行流
2.线程执行粒度比进程更细,因为其执行的代码变得更小了,数据变得更少了,CPU内有一大堆寄存器,调度的时候地址空间不用切换了,页表不用切换了,要切的只是当前进程产生的临时上下文,寄存器上的一些核心数据结构不用切换了,所以调度的成本更低
3.线程是CPU内调度的基本单位。

上述说的只是在Linux的线程。
对于其他操作系统而言,由于线程的一些特性,导致线程:进程一定是 n:1的。进程需要管理,线程当然也需要管理,线程的描述是tcb,进程是pcb,但是如果单独实现线程的描述,那么其和进程之间的耦合关系就会变得很复杂。
对于Linux而言:
没有线程,没有线程在概念上的划分,只有一个叫做执行流
Linux的线程是用进程模拟的,PCB模拟的。(这是很多教材的说法)。
因此在linux下是有TCB的,但不是单独设计的,其直接复用了PCB。
所以Linux下提供了一些接口来进行线程的相关操作,但是系统调用接口太麻烦了,而是所有的Linux必须自带的一套原生线程库,在用户层对线程进行相关动作。

这样对于CPU而言有区别吗?没有任何区别,都是调度一个task_struct,只是调度的粒度更小,调度的成本更低,这样本来串行化执行的代码,可以并发或并行的同时执行代码,同时推进,这就线程!!!

2.再谈进程

曾经: 进程 = 内核数据结构 + 进程对应的代码和数据
现在:进程 = 内核视角:承担分配系统资源的基本实体(进程的基座属性)
意义:向系统申请资源的基本单位!!

在这里插入图片描述
之前的进程是内部只有一个执行流的单执行流的进程,但是现在可以是内部有多个执行流的进程——多执行流的进程。

总结

CPU视角,task_struct <= 传统的进程PCB
;没有真正意义上的线程,而是用进程的task_struct模拟实现的,linux下的“进程” <= 其他操作系统的进程概念。
linux下的线程也叫做轻量级进程!线程是调度的基本单位!
下面写一段线程代码:

#include <iostream>
#include <string>

#include <pthread.h>
#include <unistd.h>
using namespace std;
void *callback1(void *args)
{
    string name = (char *)args;
    while (true)
    {
        cout << name << ": " << getpid() << endl;
        sleep(1);
    }
}
void *callback2(void *args)
{
    string name = (char *)args;
    while (true)
    {
        cout << name << ": " << getpid() << endl;
        sleep(1);
    }
}
int main()
{

    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1, nullptr, callback1, (void *)"thread 1");
    pthread_create(&tid2, nullptr, callback2, (void *)"thread 2");
    while (true)
    {
        cout << "我是主线程 我正在运行代码" << endl;
        sleep(1);
    }
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    return 0;
}

在这里插入图片描述
验证了linux下线程就是轻量级进程。
总结

优点:
1.创建一个线程的代价要比一个新进程的小得多
2.线程的切换不需要切换页表和地址空间,需要做的工作比进程的少
3.线程占用的资源比进程小
4.线程可以充分利用多处理器的并行数量
5.在等待慢速I/O操作时,程序可执行其他任务
6.计算密集型应用,为了能够在多处理器系统上运行,将计算分解到多个线程执行
缺点:
1.性能损失,一个很少被阻塞的计算密集型往往无法与其他线程共用一个处理器,并且一旦线程的数量比处理器的数量多,那么就可能会造成较大的性能损失,这里的损失指的是增加了额外的同步和调度开销,而可用的资源不变
2.健壮性降低,进程有独立的地址空间和页表,线程往往会和其他线程共享变量
3.缺乏访问控制,线程时调度的基本单位,在一个线程中调用某些OS会对整个进程造成影响
4.编写苦难较高,调试较难

3.理解页表

先看一个例子:
char*msg = "hello world; *msg = 'z'
上述一行代码是对的吗?
上述的代码能编译过,但是运行时会报错。
因为上述的msg指向的是字符串常量,其存在于只读常量区,是只读的,不能被修改,当发现被修改时,就会报异常。
在这里插入图片描述
字符常量区位于代码区和已初始化数据区,该代码基于页表的映射此时在页表中的权限是只读的,当程序企图修改时,OS会通过页表检测到权限不符,就会报错,其实内存任何时候都是可以被修改的,只是有没有修改的权限罢了。

4. 再次理解虚拟到物理的转化

在这里插入图片描述

这样做有什么好处呢?

1.将进程虚拟地址管理和内存管理通过页表+page进行解耦。当我们要访问某个数据时,通过页表的映射,发现page = null,此时OS就必须从内存重新加载了。在解释一下,页表只关心page在还是不在,如果不在,就交给操作系统的内存管理,将数据重新从磁盘加载到内存。
2.因为将页表拆开了,可以实现页表的按需创建,节省空间
**解释:**页表的最终大小是2^32 / 2^12 = 1M 假设一个条目是20个字节,所以页表最大也就是20M

线程的控制

1.线程的创建

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
作用:创建线程
thread:线程id
attr:属性(不考虑)
void *(*start_routine) (void *):线程执行时所对应的回调方法
arg:传入回调方法中的参数
返回值:创建成功返回0
失败:返回错误码

#include <pthread.h>
pthread_t pthread_self(void);
作用:谁调用该函数就获取该线程的线程ID

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
作用:等待线程
因为线程本质上就是第一个轻量级进程,所以也是要等待的。否则会造成类似于进程那般的内存泄露问题。
thread:线程id
retval: 输出型参数,获取线程的返回值

#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static void PrintId(const char *name, const pthread_t &tid)
{
    printf("%s 正在运行,thread id: 0x%x\n", name, tid);
}
void *callback1(void *args)
{
    int cnt = 5;
    const char *name = static_cast<const char *>(args);
    while (true)
    {
        cout << "线程正在运行...." << endl;
        PrintId(name, pthread_self());
        sleep(1);
        if (!cnt--)
            break;
    }
    cout << "线程退出了..." << endl;
    return nullptr;
}
void *callback2(void *args)
{
    int cnt = 5;
    const char *name = static_cast<const char *>(args);
    while (true)
    {
        cout << "线程正在运行...." << endl;
        PrintId(name, pthread_self());
        sleep(1);
        if (!cnt--)
            break;
    }
    cout << "线程退出了..." << endl;
    return nullptr;
}
int main()
{

    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1, nullptr, callback1, (void *)"thread 1");
    pthread_create(&tid2, nullptr, callback2, (void *)"thread 2");
    int cnt = 10;
    while (true)
    {
        PrintId("main thread", pthread_self());

        sleep(1);
        if (!cnt--)
            break;
    }
    cout << "进程也退出了!!!" << endl;
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);

    return 0;
}

在这里插入图片描述

2.线程异常

3.验证pthread_join 的第二个参数

参数是一个输出型参数,获取新线程的退出码
整体代码与上述相似,只写出更改的代码和运行结果的部分截图

return (void *)10;
 pthread_join(tid1, &retval);
    cout << "retval: " << ((long long)retval) << endl;

在这里插入图片描述

4.线程的退出方式

void *callback1(void *args)
{
    int *ptr = nullptr;
    *ptr = 4;
    int cnt = 3;

    const char *name = static_cast<const char *>(args);
    while (true)
    {
        cout << "线程正在运行...." << endl;
        PrintId(name, pthread_self());
        sleep(1);
        if (!cnt--)
            break;
    }
    cout << "线程退出了..." << endl;
    return (void *)10;
}

如果此时线程一的回调函数如上所示
在这里插入图片描述
进程会直接退出,线程发生段错误,操作系统会发送信号给线程,而进行线程的信号是共享的,所以线程异常 = 进程异常
这也说明了线程的健壮性比较低
所以线程终止只考虑正常终止的情况。

#include <pthread.h>
void pthread_exit(void *retval);
线程终止函数,与上述代码的return 作用一样

#include <pthread.h>
void pthread_exit(void *retval);
给线程发送取消请求,如果线程是被取消的,退出结果是-1
-1实际上就是PTHREAD_CANCELED;表示线程的退出信息此时是被取消的。

int main()
{

    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1, nullptr, callback1, (void *)"thread 1");
    pthread_create(&tid2, nullptr, callback2, (void *)"thread 2");
    sleep(2);
    pthread_cancel(tid1);
    int cnt = 5;
    while (true)
    {
        PrintId("main thread", pthread_self());

        sleep(1);
        if (!cnt--)
            break;
    }
    cout << "进程也退出了!!!" << endl;
    void *retval = nullptr;
    pthread_join(tid1, &retval);
    cout << "retval: " << ((long long)retval) << endl;
    pthread_join(tid2, nullptr);

    return 0;
}

在这里插入图片描述
总结线程的退出方式:

1.return
value_ptr(pthread_join的第二个参数)存放的是thread线程的返回值
2.pthread_exit()
value_ptr(pthread_join的第二个参数)存放的是传给pthread_exit的参数
3.pthread_cancel();
value_ptr(pthread_join的第二个参数)存放的是常数:PTHREAD_ CANCELED
4.如果对线程的终止状态不感兴趣,可以穿nullptr给value_ptr

5. 线程的公有和私有

多线程进程,线程共享同一地址空间,同时线程还共享

文件描述符
每种信号的处理方式
当前工作目录
用户id和组id

当然,线程也有一部分自己的数据

线程ID
一组寄存器

errno
信号屏蔽字
调度优先级

线程私有寄存器说明线程是可被调度的,可以进行线程切换,验证了线程是调度的基本单位。
私有栈说明线程是可以运行起来的,各自进行出栈和压栈

在这里插入图片描述

6.pthread_t 与线程独立栈

在这里插入图片描述
可以看到我们的用户级线程使用第三方线程库 libpthread.so
在这里插入图片描述
无论是第三方第三方线程库还是可执行程序,都要从磁盘加载到内存,然后通过页表建立地址空间与内存的映射。需要注意的是无论是自己的代码,还是库的代码,又或是系统的代码,都是在进程的地址空间中进行的。

在这里插入图片描述
对于用户而言:其需要的是线程
但是对于LinuxOS而言,其只能提供轻量级进程。
所以libpthread.so起到了一个过渡的作用,其通过封装相关系统调用,使得用户看似拿到了线程,也正是在libpthread.so这一层开始有线程的概念。

所以线程的全部实现,并没有体现在OS中,而是OS提供执行流,具体的线程结构由库来进行管理。
库要创建多个线程,因此库要管理线程。
伪代码:
struct thread_info
{
pthread_t tid;
void *stack; //私有栈
};

大致如下
在这里插入图片描述

所以pthread_t对应的用户级线程结构体的起始地址
并且各自线程的私有栈也是在共享区中的,主线程用的是独立栈结构,也就是地址空间中的栈,新线程用的是库提供的栈结构。

7.线程的局部性存储

#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int global_val = 100;
void *startRounte(void *args)
{
    while (true)
    {
        cout << "thread " << pthread_self() << "  global_val: " << global_val
             << "&global_val: " << &global_val << "Inc: " << global_val++ << endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1, nullptr, startRounte, (void *)"pthread 1");
    pthread_create(&tid2, nullptr, startRounte, (void *)"pthread 1");
    pthread_create(&tid3, nullptr, startRounte, (void *)"pthread 1");

    while (true)
    {
        cout << "thread " << pthread_self() << "  global_val: " << global_val
             << "&global_val: " << &global_val << "Inc: " << global_val++ << endl;
        sleep(1);
    }
    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);

    return 0;
}

在这里插入图片描述
如上所示,此时的变量是全局变量,线程可以共享变量,各自打印的变量地址都是一样的
__thread int global_val = 100;
如果将变量的定义改成如上所示。

在这里插入图片描述
此时三个线程各自私有数据,这叫做线程的局部性存储,可以理解为一旦加了__thread,此时每个线程各自将变量拷贝了一份。

8.线程分离

默认情况下:新创建的线程都是可等待的,线程退出后,需要主线程对其pthread_join,否则无法释放资源吗,从而造成资源的泄露。
但是如果不担心线程的分离,pthread_join反而是一种负担,因为一直要阻塞式的等待线程,无法执行主线程的代码。

#include <pthread.h>
int pthread_detach(pthread_t thread);
线程既可以分离,也可以是其他线程对目标线程分离。但是建议用主线程对支线程进行分离,并且join和线程分离是冲突的,线程分离了就不能等待。

__thread int global_val = 100;
void *startRounte(void *args)
{
    pthread_detach(pthread_self());
    while (true)
    {
        cout << "thread " << pthread_self() << "  global_val: " << global_val
             << "&global_val: " << &global_val << "  Inc: " << global_val++ << endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1, nullptr, startRounte, (void *)"pthread 1");
    pthread_create(&tid2, nullptr, startRounte, (void *)"pthread 1");
    pthread_create(&tid3, nullptr, startRounte, (void *)"pthread 1");
    // sleep(1);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);

    return 0;
}

上述是一个错误代码,因为线程已经分离了,但是又在后面join了,此时应该会报错,但是运行之后发现
在这里插入图片描述
运行的结果非常好,这是因为线程是缺乏访问控制的,有可能主线程先调度,此时其直接阻塞式等待了,压根没有意识到线程分离了,为了避免这个情况,我们应该在主线程上进行线程分离。

__thread int global_val = 100;
void *startRounte(void *args)
{
    while (true)
    {
        cout << "thread " << pthread_self() << "  global_val: " << global_val
             << "&global_val: " << &global_val << "  Inc: " << global_val++ << endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1, nullptr, startRounte, (void *)"pthread 1");
    pthread_create(&tid2, nullptr, startRounte, (void *)"pthread 1");
    pthread_create(&tid3, nullptr, startRounte, (void *)"pthread 1");
    // sleep(1);
    pthread_detach(tid1);
    pthread_detach(tid2);
    pthread_detach(tid3);

    int n = pthread_join(tid1, nullptr);
    cout << n << " : " << strerror(n) << endl;
    n = pthread_join(tid2, nullptr);
    cout << n << " : " << strerror(n) << endl;
    n = pthread_join(tid3, nullptr);
    cout << n << " : " << strerror(n) << endl;

    return 0;
}

在这里插入图片描述
此时就可以显示出非法。

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

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

相关文章

通过RTSP协议接入RTSP流媒体服务器EasyNVR视频监控汇聚平台的设备显示离线是什么原因?

EasyNVR安防视频云服务是基于RTSP/Onvif协议接入的视频平台&#xff0c;可支持将接入的视频流进行全平台、全终端的分发&#xff0c;分发的视频流包括RTSP、RTMP、HTTP-FLV、WS-FLV、HLS、WebRTC等。平台丰富灵活的视频能力&#xff0c;可应用在智慧校园、智慧工厂、智慧水利等…

028:vue上传解析excel文件,列表中输出内容

第028个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

静态路由 网络实验

静态路由 网络实验 拓扑图初步配置R1 ip 配置R2 ip 配置R3 ip 配置查看当前的路由表信息查看路由表信息配置静态路由测试 拓扑图 需求&#xff1a;实现 ip 192.168.1.1 到 192.168.2.1 的通信。 初步配置 R1 ip 配置 system-view sysname R1 undo info-center enable # 忽略…

超图聚类论文阅读1:Kumar算法

超图聚类论文阅读1&#xff1a;Kumar算法 《超图中模块化的新度量&#xff1a;有效聚类的理论见解和启示》 《A New Measure of Modularity in Hypergraphs: Theoretical Insights and Implications for Effective Clustering》 COMPLEX NETWORKS 2020, SCI 3区 具体实现源码见…

【SWT】 Button 处理 Checkbox 按钮的选中与反选事件

介绍&#xff1a; 在使用 Java SWT&#xff08;Standard Widget Toolkit&#xff09;创建图形用户界面时&#xff0c;经常需要处理按钮的选中和反选事件。本文将介绍如何通过添加 SelectionListener 监听器来实现按钮选中与反选事件的处理&#xff0c;并相应地修改相关变量的值…

2023国赛数学建模B题思路分析 - 多波束测线问题

# 1 赛题 B 题 多波束测线问题 单波束测深是利用声波在水中的传播特性来测量水体深度的技术。声波在均匀介质中作匀 速直线传播&#xff0c; 在不同界面上产生反射&#xff0c; 利用这一原理&#xff0c;从测量船换能器垂直向海底发射声波信 号&#xff0c;并记录从声波发射到…

【MySQL系列】MySQL的事务管理的学习(一)_ 事务概念 | 事务操作方式 | 事务隔离级别

「前言」文章内容大致是MySQL事务管理。 「归属专栏」MySQL 「主页链接」个人主页 「笔者」枫叶先生(fy) 目录 一、事务概念二、事务的版本支持三、事务提交方式四、事务常见的操作方式4.1 事务正常操作4.2 事务异常验证 五、事务隔离级别5.1 查看与设置隔离性5.2 读未提交&…

flutter报错-cmdline-tools component is missing

安装完androidsdk和android studio后&#xff0c;打开控制台&#xff0c;出现错误 解决办法 找到自己安装android sdk的位置&#xff0c;然后安装上&#xff0c;并将下面的勾选上 再次运行 flutter doctor 不报错&#xff0c;出现以下画面 Doctor summary (to see all det…

视频融合平台EasyCVR综合管理平台加密机授权报错invalid character是什么原因

视频融合平台EasyCVR综合管理平台具备视频融合汇聚能力&#xff0c;作为安防视频监控综合管理平台&#xff0c;它支持多协议接入、多格式视频流分发&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包…

Java版 招投标系统简介 招投标系统源码 java招投标系统 招投标系统功能设计

项目说明 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&#xff0c;以及…

【pytorch】数据加载dataset和dataloader的使用

1、dataset加载数据集 dataset_tranform torchvision.transforms.Compose([torchvision.transforms.ToTensor(),])train_set torchvision.datasets.CIFAR10(root"./train_dataset",trainTrue,transformdataset_tranform,downloadTrue) test_set torchvision.data…

高德地图,绘制矢量图形并获取经纬度

效果如图 我用的是AMapLoader这个地图插件,会省去很多配置的步骤,非常方便 首先下载插件,然后在局部引入 import AMapLoader from "amap/amap-jsapi-loader";然后在methods里面使用 // 打开地图弹窗mapShow() {this.innerVisible true;this.$nextTick(() > {…

祝贺!Databend Cloud 入驻 AWS 云市场

关于 Databend Cloud Databend Cloud 是基于开源云原生数仓项目 Databend 打造的一款易用、低成本、高性能的新一代大数据分析平台&#xff0c;提供一站式 SaaS 服务&#xff0c;免运维、开箱即用。 Databend Cloud 架构如下&#xff1a; 存储层完全面向对象存储而设计。 计算…

2023年海外推广怎么做?

答案是&#xff1a;2023海外推广可以选择谷歌SEO谷歌Ads双向运营。 理解当地文化 成功的海外推广首先是建立在对当地文化的深入了解和尊重的基础上。 本土化策略 为了更好地与当地用户互动&#xff0c;你的品牌、产品或服务需要与他们的文化和生活方式紧密相连。 例如&…

Linux/Windows中根据端口号关闭进程及关闭Java进程

目录 Linux 根据端口号关闭进程 关闭Java服务进程 Windows 根据端口号关闭进程 Linux 根据端口号关闭进程 第一步&#xff1a;根据端口号查询进程PID&#xff0c;可使用如下命令 netstat -anp | grep 8088&#xff08;以8088端口号为例&#xff09; 第二步&#xff1a;…

【大数据之Kafka】九、Kafka Broker之文件存储及高效读写数据

1 文件存储 1.1 文件存储机制 Topic是逻辑上的概念&#xff0c;而partition是物理上的概念&#xff0c;每个partition对应于一个log文件&#xff0c;该log文件中存储的是Producer生产的数据。 Producer生产的数据会被不断追加到该log文件末端&#xff0c;为防止log文件过大导致…

【网络编程】深入了解UDP协议:快速数据传输的利器

(꒪ꇴ꒪ )&#xff0c;Hello我是祐言QAQ我的博客主页&#xff1a;C/C语言&#xff0c;数据结构&#xff0c;Linux基础&#xff0c;ARM开发板&#xff0c;网络编程等领域UP&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff0c;让我们成为一个强大的攻城狮&#xff0…

MILP(混合整数线性规划)

线性规划定义 线性规划问题需要满足以下三个条件&#xff1a; 1.每一个问题用一组决策变量表示某一方案 2.约束条件可以用一组线性等式或者线性不等式来表示 3.目标函数为由决策变量及其有关的价值系数构成线性函数 ILP与MILP定义 整数线性规划中如果所有的变量被限制为&a…

闭包的详细认识与实例

参考https://www.bilibili.com/video/BV1sY4y1U7BT/?spm_id_from333.337.search-card.all.click&vd_source2a0404a7c8f40ef37a32eed32030aa18 一、什么叫闭包 1、问题引出&#xff1a; 不准用全局变量&#xff0c;也不准在调用代码块使用变量&#xff0c;实现计数…

以气象行业为例,浅谈在ToB/ToG行业中如何做好UI设计

商业气象公司是典型的TOB/TOG性质的公司&#xff0c;客户包括农业、能源、航空航天、交通运输、建筑工程等行业&#xff0c;它们需要准确的气象数据、预报和分析来支持业务决策和运营管理。商业气象公司通常会提供各种气象服务&#xff0c;如气象数据采集与分析、预报产品、风险…