Go源码--context包

简介

Context 是go语言比较重要的且也是比较复杂的一个结构体,Context主要有两种功能:

  • 取消信号:包括直接取消(涉及的结构体:cancelCtx ; 涉及函数:WithCancel)和携带截止日期的取消(涉及结构体:timeCtx,cancelCtx;涉及函数:WithTimeout/WithDeadline/WithDeadlineCause);一般用来控制主从协程,当主协程执行取消操作时,当然希望从携程也执行取消操作(或者其他设定的操作),这样主协程可以对从协程的生命周期有控制权。
  • 传递消息:包括跨API边界和进程间传递(涉及结构体:valueCtx;涉及函数:WithValue);主从协程可以共享数据,这样在Context形成的带回溯的树状结构中,通过回溯任何协程可以根据key获取其他协程注册在valueCtx(继承了context)结构体中的value,从而做到在整个主从协程中共享数据。

由于上述涉及的结构体入参 都包含Context 返回值也包含Context,所以继承自Context的结构体 都可以是入参,这样上述函数就可以组合使用。WithValue一般和/WithCancel/WithTimeout/WithDeadline/WithDeadlineCause搭配使用,这样既可以控制从协程生命周期,又可以传递数据。这几个结构体的组合可以产生各种应用场景。我后续会介绍这种组合。下面我们先梳理下几种重要的接口和结构体吧。

重要结构体

Context接口

Context接口是梦开始的地方,拥有最崇高的地位。我们来看下其结构体

type Context interface {
// Deadline 返回代表此上下文完成工作时应取消的时间。Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}  // 返回 chan类型 如果返回的不是关闭的 则阻塞 否则 返回关闭的chan 不阻塞Err() error  // 返回Done不阻塞的原因 如果是 Canceled的则 返回cancel的原因 所以 这个参数不为空 不意味着是运行出错了Value(key any) any   // 返回 key对应的value,可以递归调用。用于查找整个context树上跟key匹配的value
}
emptyCtx 结构体

emptyCtx 采用空值实现了 Context的四个函数

type emptyCtx struct{}
backgroundCtx 结构体
type backgroundCtx struct{ emptyCtx }

其采用组合的方式来包含emptyCtx,从而可以调用其函数值,也就是说实现了Context这个结构体,这就是结构体嵌入的优势。todoCtx结构体 跟backgroundCtx 结构一样。其主要用来作为树形结构的根节点,所以其实现的context函数都是空实现。

cancelCtx 结构体

介绍 这个结构体前我们先来看下 canceler 接口

type canceler interface {cancel(removeFromParent bool, err, cause error) // 取消Done() <-chan struct{}
}

canceler 的实现是 *cancelCtx和 *timerCtx,主要为context类型结构体进行功能补充,可以进行取消操作。

cancelCtx 结构体如下:

// 可以被取消,当被取消,也会取消任何实现了canceler结构体的孩子Context
type cancelCtx struct {Contextmu       sync.Mutex            // 保护下面的属性done     atomic.Value          // done存了chan struct{},被懒惰创建(使用时才创建)children map[canceler]struct{} // 实现了 canceler接口的孩子map,key有用,value没用。主要用来组成树形结构,因为key是cancler类型的// 所以能作为key(除了根节点的树节点)的只有cancelCtx,timeCtx和嵌入这两个任何一个的结构体;err      error                 // cause    error                 // 
}

cancelCtx主要用来主协程控制从协程的生命周期,主协程调用cance()函数后其chan关闭,其子协程也会执行 Done()(关闭的chan不再阻塞)函数取消阻塞来执行后续逻辑。这里分两种情况,一种是只有一个With函数,子协程不产生新的context,也就是大家经常用的,第二种情况是在子协程使用With函数来产生孩子节点,这时children就会发挥作用,会形成带回溯的树形结构。下文会有详细讲解。
跟cancelCtx直接相关的函数主要是 WithCancel。

timerCtx 结构体

timerCtx结构体有定时器和截止时间两个参数。它嵌入了一个cancelCtx结构体来实现Done和Err方法。通过停止计时器(到时间了)然后委托给cancelCtx.cancel来实现cancel,也就是说timerCtx的cancel是通过调用cancelCtx的cancel方法来实现的。我们来看下其结构体:

type timerCtx struct {cancelCtx // 内嵌取消contexttimer *time.Timer  // 定时器 用来定时取消操作deadline time.Time // 返回取消的时间戳
}

可以看到 由于内嵌 取消context其本身可以代表cancelCtx类型,可以断言成功。(这个特性可以将两者组合使用)
跟timerCtx直接相关的函数有 WithTimeout/WithDeadline/WithDeadlineCause三个。要再次强调下 只要是context的实现结构体都可以作为With系列函数的入参,所以不同context变体的组合可以实现各种复杂的主从控制和信息传递操作。以上两个结构体介绍的都是主从控制方面的,现在我们来看下传递参数相关的结构体。

valueCtx 结构体

其结构体如下:

type valueCtx struct {Contextkey, val any
}

这个结构体带来一个key-value对,其为key实现了Vaule函数,主要用来在树形结构中查找key对应的value。其他的方法调用都委托给了嵌入的Context。这样 通过 valueCtx和timerCtx或者cancelCtx的组合就能即传递参数又控制子协程。当然vauleCtx也可以嵌入其他Context来实现参数的传递,例如我们常用的 go http包。
我们来简要绘制一个图来看下如上结构体之间的关系。
在这里插入图片描述
简要介绍了结构体 接下来我们来介绍几种重要的函数,函数都是名如其人。

WithCancel

其英文解释如下:
// WithCancel returns a copy of parent with a new Done channel. The returned context’s Done channel is closed when the returned cancel function is called or when the parent context’s Done channel is closed, whichever happens first.Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.
翻译成中文是:
WithCancel返回带有Done 通道的上下文的副本,其实就是新建一个cancelCtx将其放入children key中。当返回的取消函数调用或者当父上下文通道已经关闭时,返回的上下文通道也会关闭。看哪个先发生吧。
其代码如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := withCancel(parent)  // 创造新cancelCtx 如果入参 ctx是cancelCtx类型 就将 新的cancelCtx放入其children key中return c, func() { c.cancel(true, Canceled, nil) } // 返回新 cancelCtx和 取消函数 
}

其使用例子如下:


func TestWithCancel(t *testing.T) {var wg sync.WaitGroupctx, concel := context.WithCancel(context.Background())wg.Add(1)go doSomthing(ctx, &wg)time.Sleep(3 * time.Second)concel()wg.Wait()
}func doSomthing(ctx context.Context, wg *sync.WaitGroup) {defer wg.Done()for {select {case <-ctx.Done():time.Sleep(2 * time.Second)fmt.Println("playing")returndefault:time.Sleep(1 * time.Second)fmt.Println("I am working")}}}

其输出如下:

I am working
I am working
playing

我们可以看到 当还没有执行cancel时 doSoomthing 中 ctx.Done() 返回的chan是阻塞未关闭状态,当调用cancel()函数时,chan关闭,因为doSomthing()中入参是 context是一个接口,接口是入参,就会携带本身类型和其实现的结构体的指针。所以 ctx中的 chan就是唯一的,当主协程调用cancel关闭chan时,子协程ctx.Done函数 返回关闭的结构体也就不再阻塞。还记得我在介绍cancelCtx结构体时,最后一段的描述吗,这时候因为就一个With函数不会产生新的 cancelCtx。
接下来我们来深入剖析下WithCancel源码。
WithCancel 的withCancel函数源码如下:

func withCancel(parent Context) *cancelCtx {if parent == nil {panic("cannot create context from nil parent")}// 创建一个新的 结构体 作为 符合条件的父节点 child; 树结构c := &cancelCtx{}// 如果符合某条件 将 子节点插入树中c.propagateCancel(parent, c)return c
}

其中 propagateCancel函数源码如下:

// propagateCancel 负责在父上下文被取消时,使子上下文也被取消。它设置了 cancelCtx 的父上下文(可以理解成树上的父子节点,制定一个节点的父节点和子节点)。
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {c.Context = parent // 回溯 指向父节点 删除本子节点用 方便GC 和 通过Value方法回溯// 父节点 主要是context.Background() 节点 没有必要取消 它是根节点(设想下 链表的根节点是不是都是空的,上文中TestWithCancel函数 执行到 // 这边就返回了 因为parent 是 context.Background() 是根节点 且 其Done是空实现)done := parent.Done()  if done == nil {return // parent is never canceled}// // 父节点已经取消 后续就不用再执行了 因为 按照 context 的设计理念 父节点取消 肯定是想子节点也取消select {case <-done:// parent is already canceledchild.cancel(false, parent.Err(), Cause(parent))returndefault:}// 将 parent Context 类型的 转换成 cancelCtx类型 然后构建 树形传递链(一夫多子 一子一父) map可能共享 所以需要枷锁if p, ok := parentCancelCtx(parent); ok {// parent is a *cancelCtx, or derives from one.p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err, p.cause)} else {if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()return}// 略过
}

可以看到 上面有对孩子节点赋值的操作这是什么情况下发生的呢,接下来我们来看一个例子,是cancelCtx的另一个有传播树形链的使用场景。代码如下:

func doSomthing2(ctx context.Context, wg *sync.WaitGroup) {defer wg.Done()for {select {case <-ctx.Done():fmt.Println("playing2")returndefault:time.Sleep(1 * time.Second)fmt.Println("I am working2")}}}func doSomthing(ctx context.Context, wg *sync.WaitGroup) {defer wg.Done()ctx1, cancel1:= context.WithCancel(ctx)  // ctx 当入参 产生新的 ctx1 这时 ctx下文是 ctx1 上文是 Backgroud产生的空contextdefer cancel1() go doSomthing2(ctx1, wg)for {select {case <-ctx.Done():fmt.Println("playing")returndefault:time.Sleep(1 * time.Second)fmt.Println("I am working")}}}func TestWithCancel(t *testing.T) {var wg sync.WaitGroupctx, concel := context.WithCancel(context.Background())wg.Add(2)go doSomthing(ctx, &wg)time.Sleep(3 * time.Second)concel()wg.Wait()
}

运行结果是:

I am working2
I am working
I am working
I am working2
I am working
playing
I am working2
playing2

可以看到 这时候 就形成了树形结构 我们来看下 形成的树:

在这里插入图片描述
如果 在子协程中 和子子协程中 运用With函数 进行嵌套最终可形成一颗很大的树,如图:
在这里插入图片描述
这时候 就形成比较复杂的主从控制方案,如果valueCtx再嵌套上cancelCtx,就可以根据key在树上寻找value(因为指针是双向的,可以根据value函数递归查找,只能往上找不能往下),这样可以做到参数的向下传递。但我们一般常用的是主协程一个With函数,从协程就引用主协程的ctx 也不嵌套,就全局唯一一个ctx,如同第一个例子。
其结构图如下:
在这里插入图片描述

WithValue

WithValue 基于 valueCtx结构体,主要用来父context给子context传递数据,也就是说子context可以获得其父context的内容 反之则不行。
一个例子:

func doSomthing(ctx context.Context, wg *sync.WaitGroup) {defer wg.Done()for {select {case <-ctx.Done():fmt.Println("playing")returndefault:time.Sleep(1 * time.Second)fmt.Println("I am working")fmt.Println(ctx.Value("key"))}}}func TestWithCancel(t *testing.T) {var wg sync.WaitGroupctx:=context.WithValue(context.Background(),"key","value")ctx, concel := context.WithCancel(ctx)wg.Add(1)go doSomthing(ctx, &wg)time.Sleep(3 * time.Second)concel()wg.Wait()
}

运行结果如下:

I am working
value
I am working
value
I am working
value
playing

可以看到 主协程传递的是 key value 可以在子协程通过 Value(key)函数来获取value。通过valuetx嵌入cancelCtx可以得到一颗树形结构,如上图cancelCtx形成的多节点树形结构,由于valueCtx 可以嵌入 cancelCtx所以 可以加入其树形链条中。

其源码如下:

func WithValue(parent Context, key, val any) Context {if parent == nil {panic("cannot create context from nil parent")}if key == nil {panic("nil key")}if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}

源码比较简单,valueCtx本身一般不会用来传播,一般都是嵌入其他的结构体 像cancelCtx,timerCtx 和其他的context,主要用来传递参数。子可以看所有的父参数,父不能看子,因为查找是递归查找。
valueCtx的主要功能是传递参数和查找参数,传递用的是 key,value。查找用的是Value函数 我们来看下Value函数。


func (c *valueCtx) Value(key any) any {if c.key == key {return c.val}return value(c.Context, key)
}// 用于在整个context链条(带回溯的树结构)上 寻找 匹配的值 是一个公共方法
func value(c Context, key any) any {for {switch ctx := c.(type) {case *valueCtx:if key == ctx.key {return ctx.val // 匹配上key 返回}c = ctx.Contextcase *cancelCtx:if key == &cancelCtxKey {return c}c = ctx.Contextcase withoutCancelCtx:if key == &cancelCtxKey {// This implements Cause(ctx) == nil// when ctx is created using WithoutCancel.return nil}c = ctx.ccase *timerCtx:if key == &cancelCtxKey {return &ctx.cancelCtx}c = ctx.Contextcase backgroundCtx, todoCtx:return nildefault:return c.Value(key) // 回溯 递归查找}}
}

通过 上述代码 我们可以看到 其之所以可以共享参数 主要是由于有递归查找的原因 所以 子可以查找父传递的参数 但是反之不行。
另一个用处是在http包内 传递参数,感兴趣的可以查阅相关资料。
人为取消是不是有时候不爽啊,那就加入定时功能吧,所以 下面的几个函数本质上就是在cancelCtx功能的扩展,我们看其结构体timerCtx就包含了cancelCtx,只不过在其基础上加了时间维度。下面来看看涉及的几个结构体。

WithDeadlineCause

WithDeadlineCause 是time相关功能的基础函数,WithDeadline和 WithTimeout都是基于这个函数。我们来看下其源码:


// 定时取消操作
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {if parent == nil {panic("cannot create context from nil parent")}// 获取父context的取消时间,如果比传入时间早 就执行WithCancel取消逻辑if cur, ok := parent.Deadline(); ok && cur.Before(d) {// The current deadline is already sooner than the new one.return WithCancel(parent)}// 否则 就新建一个新timerCtx 并把取消时间赋给deadlinec := &timerCtx{deadline: d,}// 构建 context树形结构 将c, parent父子化c.cancelCtx.propagateCancel(parent, c)// 开始定时 直到达到取消时间 执行取消操作dur := time.Until(d)if dur <= 0 {c.cancel(true, DeadlineExceeded, cause) // deadline has already passedreturn c, func() { c.cancel(false, Canceled, nil) }}c.mu.Lock()defer c.mu.Unlock()// dur时间过后 开始执行取消if c.err == nil {c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded, cause)})}// todoreturn c, func() { c.cancel(true, Canceled, nil) }
}

这个结构体不怎么使用 我们一般使用它两个变种。如下

WithDeadline

变种自WithDeadlineCause
源码如下:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {return WithDeadlineCause(parent, d, nil)
}

只不过去掉了取消原因 因为是定时取消 原因不用说了
我们看一个例子

func doWork(ctx context.Context,wg * sync.WaitGroup) {defer wg.Done()select {case <-time.After(3 * time.Second):// 模拟操作需要3秒完成fmt.Println("操作完成")case <-ctx.Done():// 当上下文取消时会进入这个分支fmt.Println("操作被取消:", ctx.Err())}
}
func TestWithDeadline(t *testing.T) {// 设置截止时间为当前时间后2秒deadline := time.Now().Add(2 * time.Second)ctx, cancel := context.WithDeadline(context.Background(), deadline)defer cancel() // 确保在主函数返回前取消上下文// 模拟操作var wg sync.WaitGroupwg.Add(1)go doWork(ctx,&wg)wg.Wait()
}

运行结果如下:

操作被取消: context deadline exceeded

比较简单 我们来看最后一个

WithTimeout

其源码如下:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout)) 
}

我们看到这个函数 只是 TestWithDeadline一个特例,入参是一个时间戳,只不过后续需要转换成当前时间往后时间戳刻度作为定时时间。
所以上面的例子稍微修改下就可以使用

func doWork(ctx context.Context,wg * sync.WaitGroup) {defer wg.Done()select {case <-time.After(3 * time.Second):// 模拟操作需要3秒完成fmt.Println("操作完成")case <-ctx.Done():// 当上下文取消时会进入这个分支fmt.Println("操作被取消:", ctx.Err())}
}
func TestWithTimeout(t *testing.T) {// 设置截止时间为当前时间后2秒ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel() // 确保在主函数返回前取消上下文// 模拟操作var wg sync.WaitGroupwg.Add(1)go doWork(ctx,&wg)wg.Wait()
}

总结

如此 Context的所有变体和相关函数都介绍完毕了。我们来总结下。

  1. Context接口是内核,cancelCtx内嵌了Context并实现了其部分函数,来执行一些取消操作,而timerCtx内嵌了 cancelCtx从而可以定时取消。
  2. valueCtx内嵌了Context,其本身还有 键值对参数,从而可以用来传递参数。其之所以不内嵌cancelCtx主要是为了提供一个基础组合结构体,用户就可以内嵌各种context变体来传递参数,使用范围更广,而不只是限制在cancel变体。
  3. With函数入参和返回值都有Context所以各种变体可以进行组合。例如cancelCtx和valueCtx组合既可以控制从协程又可以向从协程传递参数。
  4. With函数只有在子协程中也执行,才能构建Context树,否则With参数传递Contest就是全局唯一一个ctx,因为With函数Context入参是接口,所以实际上传递的是Context类型和其实现结构体的指针,所以单纯传递Context不会产生值复制。

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

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

相关文章

密室逃脱——收集版

一、原版修改 1、导入资源 Unity Learn | 3D Beginner: Complete Project | URP 2、设置Scene 删除SampleScene&#xff0c;打开UnityTechnologies-3DBeginnerComplete下的MainScene 3、降低音量 (1) 打开Hierarchy面板上的Audio降低音量 (2) 打开Prefabs文件夹&#xf…

Git安装与使用及整合IDEA使用的详细教程

1. 版本控制软件介绍 版本控制软件提供完备的版本管理功能&#xff0c;用于存储、追踪目录&#xff08;文件夹&#xff09;和文件的修改历史&#xff0c;是软件开发者的必备工具&#xff0c;是软件公司的基础设施。版本控制软件的最高目标&#xff0c;是支持软件公司的配置管理…

管理统计学

第1章 统计学是收集、处理、分析、解释数据并从数据中得出结论的科学。 统计学是处理数据的方法论。 参数 表示总体特征的概括性数字度量&#xff0c;是研究者想要了解的总体的某种特征值。 统计量 是用来描述样本特征的概括性数字度量。 常用统计量包括&#xff1a; &#xff…

ESP8266[ 关于-巴发云MQTT/TCP:arduino 设置回调函数 ] 日志2024/6/29

ESP8266 [ 关于-巴发云MQTT/TCP:arduino 设置回调函数 ] 日志2024/6/29 arduino库:#include <PubSubClient.h> 回调函数 是其库设置好的 可以改名字 这里只写上关键代码 设置客户端为 A 关键代码: A.setCallback(回调名) //MQTT 回调处理mqttmsgg(自定义…

zdppy_api+vue3实现前后端分离的登录功能

实现思路 1、准备zdppy的开发环境 2、使用amauth提供的低代码接口&#xff0c;直接生成login登录接口 3、使用之前开发的登录模板渲染登录界面 4、给登录按钮绑定点击事件 5、给用户名和密码的输入框双向绑定数据 6、使用axios在登录按钮点击的时候&#xff0c;携带用户数据发…

C++ | Leetcode C++题解之第207题课程表

题目&#xff1a; 题解&#xff1a; class Solution { private:vector<vector<int>> edges;vector<int> indeg;public:bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {edges.resize(numCourses);indeg.resize(numCo…

昇思25天学习打卡营第13天|MindNLP ChatGLM-6B StreamChat

学AI还能赢奖品&#xff1f;每天30分钟&#xff0c;25天打通AI任督二脉 (qq.com) MindNLP ChatGLM-6B StreamChat 本案例基于MindNLP和ChatGLM-6B实现一个聊天应用。 1 环境配置 %%capture captured_output # 实验环境已经预装了mindspore2.2.14&#xff0c;如需更换mindspo…

[知识点篇]《计算机组成原理》之数据信息的表示

1、数据表示的作用 &#xff08;1&#xff09;定义&#xff1a;将数据按照某种方式组织&#xff0c;以便机器硬件能直接识别和使用。现代计算机采用二进制进行数据表示。 &#xff08;2&#xff09;数据表示考虑因素&#xff1a; 数据的类型&#xff1a; 数值/非数值、小数、…

读AI新生:破解人机共存密码笔记17不确定性和概率

1. 前向搜索 1.1. 通过前向搜索&#xff0c;通过考虑各种可能的动作序列的结果&#xff0c;来选择动作&#xff0c;是智能系统的基本能力 1.2. 如果一家卡车运输公司想要优化其100辆卡车在美国的运输&#xff0c;那么该公司可能需要考虑的状态数量将是10^700个 1.3. 几乎所有…

解决Install/Remove of the Service Denied报错

1、问题概述&#xff1f; 在Windows系统中安装MySQL5.7.43的时候&#xff0c;运行mysqld install命令提示报错&#xff1a;Install/Remove of the Service Denied 意思是&#xff1a;安装/删除服务被拒绝 问题原因所在&#xff1a;就是你当前的权限不够&#xff0c;以管理员…

Linux【环境 CenOS7】部分软件安装链接整理

优质博文&#xff1a;IT-BLOG-CN 一、开启网络 【问题】&#xff1a; 刚安装完CentOS&#xff0c;当ping www.baidu.com时&#xff0c;ping不通&#xff1b; 【解决】&#xff1a; 进入cd /etc/sysconfig/network-scripts/我这里修改的是ifcfg-ens33文件&#xff0c;将ONBOOT…

p2p、分布式,区块链笔记:试用ZeroTier组网

ZeroTier 是一种用于创建和管理虚拟局域网&#xff08;Virtual Local Area Network&#xff0c;VLAN&#xff09;的软件定义网络&#xff08;SDN&#xff09;解决方案。它可以通过互联网将多个设备安全地连接在一起&#xff0c;就像它们在同一个本地网络上一样。主要开发语言为…

【Python从入门到进阶】59、Pandas库中Series对象的操作(二)

接上篇《58、Pandas库中Series对象的操作(一)》 上一篇我们讲解了Pandas库中Series对象的基本概念、对象创建和操作&#xff0c;本篇我们来继续学习Series对象的运算、函数应用、时间序列操作&#xff0c;以及Series的案例实践。 一、Series对象的运算 1. 数值型数据的算术运…

1、音视频解封装流程---解复用

对于一个视频文件(mp4格式/flv格式)&#xff0c;audio_pkt或者video_pkt是其最基本的数据单元&#xff0c;即视频文件是由独立的视频编码包或者音频编码包组成的。 解复用就是从视频文件中把视频包/音频包单独读取出来保存成独立文件&#xff0c;那么如何得知packet是视频包还是…

【一篇搞懂】操作系统期末大题:进程同步与互斥 PV操作

文章目录 一、前言&#x1f680;&#x1f680;&#x1f680;二、正文&#xff1a;☀️☀️☀️题型一&#xff1a;利用信号量实现前驱关系题型二&#xff1a;利用信号量实现资源同步与互斥 一、前言&#x1f680;&#x1f680;&#x1f680; 本文简介&#xff1a;这是一篇基于b…

无人机远程控制:北斗短报文技术详解

无人机&#xff08;UAV&#xff09;技术的快速发展和应用&#xff0c;使得远程控制成为了一项关键技术。无人机远程控制涉及无线通信、数据处理等多个方面&#xff0c;其中北斗短报文技术以其独特的优势&#xff0c;在无人机远程控制领域发挥着重要作用。本文将详细解析无人机远…

2024-06-26 base SAS programming 学习笔记6(proc report)

proc report可以生成报表&#xff0c;基本格式&#xff1a; proc report data options; (options 可以是windows/WD表示将结果输出至单独的报表窗口&#xff0c;或者nowindows/nowd将结果输出至HTML结果窗口) column variables ;(筛选待输出的变量&#xff0c;变量名与变量名之…

09_计算机网络模型

目录 OSI/RM七层模型 OSI/RM七层模型 各层介绍及硬件设备 传输介质 TCP/IP协议簇 网络层协议 传输层协议 应用层协议 完整URL的组成 IP地址表示与计算 分类地址格式 子网划分和超网聚合 无分类编址 特殊含义的IP地址 IPv6协议 过渡技术 OSI/RM七层模型 OSI/RM七…

区间动态规划——最长回文子序列长度(C++)

把夜熬成粥&#xff0c;然后喝了它。 ——2024年7月1日 书接上回&#xff1a;区间动态规划——最长回文子串&#xff08;C&#xff09;-CSDN博客&#xff0c;大家有想到解决办法吗&#xff1f; 题目描述 给定一个字符串s&#xff08;s仅由数字和英文大小写字母组成&#xff0…

微积分-导数3(微分法则)

常见函数的导数 常量函数的导数 d d x ( c ) 0 \frac{d}{dx}(c) 0 dxd​(c)0 常量函数的图像是一条水平线 y c y c yc&#xff0c;它的斜率为0&#xff0c;所以我们必须有 f ′ ( x ) 0 f(x) 0 f′(x)0。从导数的定义来看&#xff0c;证明也很简单&#xff1a; f ′ …