[C#]async和await刨根问底

上一篇随笔留下了几个问题没能解决:
· 调用IAsyncStateMachine.MoveNext方法的线程何时发起的?
· lambda的执行为何先于MoveNext方法?
· 后执行的MoveNext方法做了些什么事情?

那么今天就来尝试解决它们吧~
PS: 本文中部分代码来自上一篇随笔,具体来源可参考注释中的章节标题

一、哪里来的线程

通过上一篇随笔的调查我们知道了,async标记的方法的方法体会被编译到一个内部结构体的MoveNext方法中,并且也找到了MoveNext的调用者,再且也证实了有两个调用者是来自于主线程之外的同一个工作线程。
可是这一个线程是何时发起的呢?上一次调查时没能找到答案,这一次就继续从MoveNext方法开始,先找找看Task相关的操作有哪些。

 1 // 三、理解await
 2 bool '<>t__doFinallyBodies';
 3 Exception '<>t__ex';
 4 int CS$0$0000;
 5 TaskAwaiter<string> CS$0$0001;
 6 TaskAwaiter<string> CS$0$0002;
 7 
 8 try
 9 {
10     '<>t__doFinallyBodies' = true;
11     CS$0$0000 = this.'<>1__state';
12     if (CS$0$0000 != 0)
13     {
14         CS$0$0001 = this.'<>4__this'.GetHere().GetAwaiter();
15         if (!CS$0$0001.IsCompleted)
16         {
17             this.'<>1__state' = 0;
18             this.'<>u__$awaiter1' = CS$0$0001;
19             this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$0$0001, ref this);
20             '<>t__doFinallyBodies' = false;
21             return;
22         }
23     }
24     else
25     {
26         CS$0$0001 = this.'<>u__$awaiter1';
27         this.'<>u__$awaiter1' = CS$0$0002;
28         this.'<>1__state' = -1;
29     }
30 
31     Console.WriteLine(CS$0$0001.GetResult());
32 }

注意到14行的GetHere方法返回了一个Task<string>,随后的GetAwaiter返回的是TaskAwaiter<string>。
不过这两个Get方法都没有做什么特别的处理,那么就看看接下来是谁使用了TaskAwaiter<string>实例
于是就来看看19行的AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted里面做了些什么吧。

 1 // System.Runtime.CompilerServices.AsyncVoidMethodBuilder
 2 [__DynamicallyInvokable, SecuritySafeCritical]
 3 public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
 4     ref TAwaiter awaiter, ref TStateMachine stateMachine)
 5     where TAwaiter : ICriticalNotifyCompletion
 6     where TStateMachine : IAsyncStateMachine
 7 {
 8     try
 9     {
10         Action completionAction = this.m_coreState
11             .GetCompletionAction<AsyncVoidMethodBuilder, TStateMachine>(ref this, ref stateMachine);
12         awaiter.UnsafeOnCompleted(completionAction);
13     }
14     catch (Exception exception)
15     {
16         AsyncMethodBuilderCore.ThrowAsync(exception, null);
17     }
18 }

这里主要做了两件事:
一是创建了一个Action,MoveNext方法的信息已经随着stateMachine被封装进去了。
二是把上面这个Action交给Awaiter,让它在await的操作完成后执行这个Action。

先来看看Action的构建细节吧:

 1 // System.Runtime.CompilerServices.AsyncMethodBuilderCore
 2 [SecuritySafeCritical]
 3 internal Action GetCompletionAction<TMethodBuilder, TStateMachine>(ref TMethodBuilder builder, ref TStateMachine stateMachine)
 4     where TMethodBuilder : IAsyncMethodBuilder
 5     where TStateMachine : IAsyncStateMachine
 6 {
 7     Debugger.NotifyOfCrossThreadDependency();
 8     ExecutionContext executionContext = ExecutionContext.FastCapture();
 9     Action action;
10     AsyncMethodBuilderCore.MoveNextRunner moveNextRunner;
11     if (executionContext != null && executionContext.IsPreAllocatedDefault)
12     {
13         action = this.m_defaultContextAction;
14         if (action != null)
15         {
16             return action;
17         }
18         moveNextRunner = new AsyncMethodBuilderCore.MoveNextRunner(executionContext);
19         action = new Action(moveNextRunner.Run);
20         if (AsyncCausalityTracer.LoggingOn)
21         {
22             action = (this.m_defaultContextAction = this.OutputAsyncCausalityEvents<TMethodBuilder>(ref builder, action));
23         }
24         else
25         {
26             this.m_defaultContextAction = action;
27         }
28     }
29     else
30     {
31         moveNextRunner = new AsyncMethodBuilderCore.MoveNextRunner(executionContext);
32         action = new Action(moveNextRunner.Run);
33         if (AsyncCausalityTracer.LoggingOn)
34         {
35             action = this.OutputAsyncCausalityEvents<TMethodBuilder>(ref builder, action);
36         }
37     }
38     if (this.m_stateMachine == null)
39     {
40         builder.PreBoxInitialization<TStateMachine>(ref stateMachine);
41         this.m_stateMachine = stateMachine;
42         this.m_stateMachine.SetStateMachine(this.m_stateMachine);
43     }
44     moveNextRunner.m_stateMachine = this.m_stateMachine;
45     return action;
46 }

这段的分支有点多,行号上的标记是我DEBUG时经过的分支。
可以看到,这个方法里面出现了MoveNext方法的调用者MoveNextRunner,它的Run方法被封装到了返回的Action里。
也就是说,只要这个Action被执行,就会进入Run方法,而Run方法里面有两条分支,简单来说就是:
1.直接调用MoveNext
2.通过InvokeMoveNext调用MoveNext

第40行的赋值不影响Action中的Run,只是在头尾追加了状态记录的操作。
接下来就赶紧找一找执行这个Action的地方吧!
深入UnsafeOnCompleted方法,最终可以找到如下的方法,第一个参数就是要跟踪的对象:

 1 // System.Threading.Tasks.Task
 2 [SecurityCritical]
 3 internal void SetContinuationForAwait(
 4     Action continuationAction,
 5     bool continueOnCapturedContext,
 6     bool flowExecutionContext,
 7     ref StackCrawlMark stackMark)
 8 {
 9     TaskContinuation taskContinuation = null;
10     if (continueOnCapturedContext)
11     {
12         SynchronizationContext currentNoFlow = SynchronizationContext.CurrentNoFlow;
13         if (currentNoFlow != null && currentNoFlow.GetType() != typeof(SynchronizationContext))
14         {
15             taskContinuation = new SynchronizationContextAwaitTaskContinuation(
16                 currentNoFlow, continuationAction, flowExecutionContext, ref stackMark);
17         }
18         else
19         {
20             TaskScheduler internalCurrent = TaskScheduler.InternalCurrent;
21             if (internalCurrent != null && internalCurrent != TaskScheduler.Default)
22             {
23                 taskContinuation = new TaskSchedulerAwaitTaskContinuation(
24                     internalCurrent, continuationAction, flowExecutionContext, ref stackMark);
25             }
26         }
27     }
28     if (taskContinuation == null && flowExecutionContext)
29     {
30         taskContinuation = new AwaitTaskContinuation(continuationAction, true, ref stackMark);
31     }
32     if (taskContinuation != null)
33     {
34         if (!this.AddTaskContinuation(taskContinuation, false))
35         {
36             taskContinuation.Run(this, false);
37             return;
38         }
39     }
40     else if (!this.AddTaskContinuation(continuationAction, false))
41     {
42         AwaitTaskContinuation.UnsafeScheduleAction(continuationAction, this);
43     }
44 }

同样的,行号的标记意味着经过的分支。继续跟进:

 1 // System.Threading.Tasks.AwaitTaskContinuation
 2 [SecurityCritical]
 3 internal static void UnsafeScheduleAction(Action action, Task task)
 4 {
 5     AwaitTaskContinuation awaitTaskContinuation = new AwaitTaskContinuation(action, false);
 6     TplEtwProvider log = TplEtwProvider.Log;
 7     if (log.IsEnabled() && task != null)
 8     {
 9         awaitTaskContinuation.m_continuationId = Task.NewId();
10         log.AwaitTaskContinuationScheduled(
11             (task.ExecutingTaskScheduler ?? TaskScheduler.Default).Id,
12             task.Id,
13             awaitTaskContinuation.m_continuationId);
14     }
15     ThreadPool.UnsafeQueueCustomWorkItem(awaitTaskContinuation, false);
16 }
 1 // System.Threading.ThreadPool
 2 [SecurityCritical]
 3 internal static void UnsafeQueueCustomWorkItem(IThreadPoolWorkItem workItem, bool forceGlobal)
 4 {
 5     ThreadPool.EnsureVMInitialized();
 6     try
 7     {
 8     }
 9     finally
10     {
11         ThreadPoolGlobals.workQueue.Enqueue(workItem, forceGlobal);
12     }
13 }

这里出现了全局线程池,然而没有找到MSDN对ThreadPoolGlobals的解释,这里头的代码又实在太多了。。。暂且模拟一下看看:

1 Console.WriteLine("HERE");
2 var callback = new WaitCallback(state => Println("From ThreadPool"));
3 ThreadPool.QueueUserWorkItem(callback);
4 Console.WriteLine("THERE");

QueueUserWorkItem方法内部调用了ThreadPoolGlobals.workQueue.Enqueue,运行起来效果是这样的:

HERE
THERE
From ThreadPool

再看看线程信息:

Function: CsConsole.Program.Main(), Thread: 0x2E58 主线程
Function: CsConsole.Program.Main(), Thread: 0x2E58 主线程
Function: CsConsole.Program.Main.AnonymousMethod__6(object), Thread: 0x30EC 工作线程

和async的表现简直一模一样是不是~?从调用堆栈也可以看到lambda的执行是源于这个workQueue

到此为止算是搞定第一个问题了。

二、lambda为何先行

先来回忆一下GetHere方法的内容:

// 三、理解await
Task<string> GetHere()
{return Task.Run(() =>{Thread.Sleep(1000);return "HERE";});
}

要追踪的lambda就是在这里构造的,而调用GetHere的地方也只有一个,就是MoveNext方法的try块。
而MoveNext的调用方也都找出来了:

其中Start方法是在主线程中调用的,可以由SampleMethod追溯到。那么以下的调用信息:

Function: Test.Program.Main(string[]), Thread: 0xE88 主线程
Function: Test.Program.GetHere.AnonymousMethod__3(), Thread: 0x37DC 工作线程
Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run(), Thread: 0x37DC 工作线程
Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object), Thread: 0x37DC 工作线程

这个顺序不是有点奇怪吗?lambda怎么能先于MoveNextRunner的两个方法执行?
其实我在这里犯了一个很明显的思维错误。。。Start调用来自主线程,lambda调用来自子线程,于是直觉性地否定了它们之间的关联。。。
很显然,整个过程其实应该是这样的:
1. 主线程:Start方法调用了MoveNext,MoveNext调用了GetHere
2. 主线程:GetHere方法返回了包含lambda信息的Task
3. 主线程:Task经过变换与包装,最终进入了线程池
4. 子线程:通过Task调用了lambda
5. 子线程:通过Runner调用了MoveNext

子线程中的lambda是来源于主线程第一次调用的MoveNext,和之后的Run啊InvokeMoveNext是没有关系的,所以这个顺序也就不奇怪了。
通过DEBUG几个关键点即可以验证这一顺序。第二个也算搞定了。

三、MoveNext干了什么

第二个问题虽然解决了,但是也让第三个问题显得更加重要,既然lambda确实是先于MoveNext,那么MoveNext到底做了些什么?
通过之前的调查,现在知道了:
1. MoveNext在lambda执行之前被Start方法在主线程调用了一次,过程中把lambda封送给了线程池
2. MoveNext在lambda执行之后被InvokeMoveNext又调用了一次,这一次做了什么处理是尚不明了的

回头看本文的第一段代码,前后两次进入同一段代码,但是做了不同的事情,那么显然就是两次走了不同的分支咯。
由于这段代码本身是DEBUG不进去的,所以只能在其内部调用的方法里断点了。我打了如下几个断点:
· Task<TResult>.GetAwaiter
· AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted
· TaskAwaiter<TResult>.GetResult
· Program.SampleMethod
· MoveNextRunner.InvokeMoveNext

来看看执行结果如何吧:

Function: Test.Program.SampleMethod(), Thread: 0x9BC 主线程
Function: System.Threading.Tasks.Task<TResult>.GetAwaiter(), Thread: 0x9BC 主线程
Function: System.Runtime.CompilerServices.AsyncVoidMethodBuilder.AwaitUnsafeOnCompleted<TAwaiter,TStateMachine>(ref TAwaiter, ref TStateMachine), Thread: 0x9BC 主线程
Function: System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object), Thread: 0x3614 工作线程
Function: System.Runtime.CompilerServices.TaskAwaiter<TResult>.GetResult(), Thread: 0x3614 工作线程

需要注意的是,断到InvokeMoveNext里头的时候,只有这一行代码:

((IAsyncStateMachine)stateMachine).MoveNext();

而当我按下F11步入之后,可以猜一猜跳到了哪:

async void SampleMethod()
{Console.WriteLine(await GetHere());
}

而在这个时候GetResult还没执行到。
由此可以整理出try块里的执行过程如下:

 1 try
 2 {
 3     '<>t__doFinallyBodies' = true;
 4     CS$0$0000 = this.'<>1__state';
 5     if (CS$0$0000 != 0)
 6     {
 7         CS$0$0001 = this.'<>4__this'.GetHere().GetAwaiter();
 8         if (!CS$0$0001.IsCompleted)
 9         {
10             this.'<>1__state' = 0;
11             this.'<>u__$awaiter1' = CS$0$0001;
12             this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$0$0001, ref this);
13             '<>t__doFinallyBodies' = false;
14             return;
15         }
16     }
17     else
18     {
19         CS$0$0001 = this.'<>u__$awaiter1';
20         this.'<>u__$awaiter1' = CS$0$0002;
21         this.'<>1__state' = -1;
22     }
23 
24     Console.WriteLine(CS$0$0001.GetResult());
25 }

红字是第一次经过的分支,黄底是第二次经过的分支。
而前面说到的F11进入的区块,实际上就是这里的第24行。
所以现在可以知道,第二次MoveNext做了什么:
执行async方法中await后的代码。

四、水落石出

async和await的轮廓逐渐清晰了~再结合上一篇的一段代码来看看:

// 二、理解async
void MoveNext()
{bool local0;Exception local1;try{local0 = true;Thread.Sleep(1000);Console.WriteLine("HERE");}catch (Exception e){local1 = e;this.'<>1__state' = -2;this.'<>t__builder'.SetException(local1);return;}this.'<>1__state' = -2;this.'<>t__builder'.SetResult()
}

黄底的两句代码原本是在哪的还记得吗?看这里:

// 二、理解async
async void SampleMethod()
{Thread.Sleep(1000);Console.WriteLine("HERE");
}

因为这个async方法中没有出现await调用,所以可以认为仅有的两句代码是出现在await操作之前。
再让SampleMethod变成这样:

async void SampleMethod()
{Console.WriteLine("WHERE");Console.WriteLine(await GetHere());
}

再看看现在的MoveNext方法:

 1 try
 2 {
 3     '<>t__doFinallyBodies' = true;
 4     CS$0$0000 = this.'<>1__state';
 5     if (CS$0$0000 != 0)
 6     {
 7         Console.WriteLine("WHERE");
 8         CS$0$0001 = this.'<>4__this'.GetHere().GetAwaiter();
 9         if (!CS$0$0001.IsCompleted)
10         {
11             this.'<>1__state' = 0;
12             this.'<>u__$awaiter1' = CS$0$0001;
13             this.'<>t__builder'.AwaitUnsafeOnCompleted(ref CS$0$0001, ref this);
14             '<>t__doFinallyBodies' = false;
15             return;
16         }
17     }
18     else
19     {
20         CS$0$0001 = this.'<>u__$awaiter1';
21         this.'<>u__$awaiter1' = CS$0$0002;
22         this.'<>1__state' = -1;
23     }
24 
25     Console.WriteLine(CS$0$0001.GetResult());
26 }

这样就可以很明显的看出来await前后的代码被放到了两个区块里,而这两个区块,也就是之前看到的两次执行MoveNext走过的分支。

最终调查结果如下
1. async方法中的代码会被移交给IAsyncStateMachine的MoveNext方法
2. async方法中await操作前后的代码被分离
3. 主线程直接执行await前的代码,并将await的Task移交给线程池ThreadPoolGlobal
4. 子线程执行完主线程递交来的Task后,再次走入MoveNext方法,执行await后的代码


最后想说的是:
这一阵在办公积金销户提取,整个过程就像是个async方法,把申请提交给管理中心(await前操作)以后就得开始等待(await)他们对申请进行审核(执行Task),这个过程加上周末得整整五天,之后还得去管理中心取款(await后操作),总之就是麻烦死了。。。

转载于:https://www.cnblogs.com/vd630/p/4596203.html

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

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

相关文章

Spring中Bean的定义继承

以下内容引用自http://wiki.jikexueyuan.com/project/spring/bean-definition-inheritance.html&#xff1a; Bean定义继承 bean定义可以包含很多的配置信息&#xff0c;包括构造函数的参数&#xff0c;属性值&#xff0c;容器的具体信息例如初始化方法&#xff0c;静态工厂方法…

由Google Protocol Buffer的小例子引起的g++编译问题

问题 学习 Google Protocol Buffer 的使用和原理时&#xff0c;提供了一个小例子&#xff0c;讲述了protobuf的使用方法。 假如已经有了如下文件&#xff1a; 其中writer.cpp如下&#xff1a;#include "lm.helloworld.pb.h" #include<iostream> #include<…

UVALive 5903 Piece it together(二分图匹配)

给你一个n*m的矩阵&#xff0c;每个点为B或W或.。然后你有一种碎片。碎片可以旋转&#xff0c;问可否用这种碎片精确覆盖矩阵。N,M<500 WB 《碎片 W 题目一看&#xff0c;感觉是精确覆盖&#xff08;最近被覆盖洗脑了&#xff09;&#xff0c;但是仔细分析可以知道&#xf…

springcloud(五):熔断监控Hystrix Dashboard和Turbine

Hystrix-dashboard是一款针对Hystrix进行实时监控的工具&#xff0c;通过Hystrix Dashboard我们可以在直观地看到各Hystrix Command的请求响应时间, 请求成功率等数据。但是只使用Hystrix Dashboard的话, 你只能看到单个应用内的服务信息, 这明显不够. 我们需要一个工具能让我们…

位运算问题

位运算 位运算是把数字用二进制表示之后&#xff0c;对每一位上0或者1的运算。 理解位运算的第一步是理解二进制。二进制是指数字的每一位都是0或者1.比如十进制的2转化为二进制之后就是10。在程序员的圈子里有一个流传了很久的笑话&#xff0c;说世界上有10种人&#xff0c;一…

conda环境管理介绍

我们可以使用conda 来切换不同的环境&#xff0c;主要的用法如下&#xff1a; 1. 创建环境 # 指定python版本为2.7&#xff0c;注意至少需要指定python版本或者要安装的包 # 后一种情况下&#xff0c;自动安装最新python版本conda create -n env_name python2.7# 同时安装必…

unable to execute dex: multiple dex files Cocos2dxAccelerometer

原文转载&#xff1a;http://discuss.cocos2d-x.org/t/conversion-to-dalvik-format-failed-unable-to-execute-dex-multiple-dex-files-define-lorg-cocos2dx-lib-cocos2dxaccelerometer/6652/4 用cocos2dx2.2.3没问题&#xff0c;用了3.1.1出现这个问题。确实够蛋疼。还要有这…

mysql自增_面试官:为什么 MySQL 的自增主键不单调也不连续?

为什么这么设计(Why’s THE Design)是一系列关于计算机领域中程序设计决策的文章&#xff0c;我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点、对具体实现造成的影响。如果你有想要了解的问题&#xff0c;可以在文章下面留言。当我们在…

使用过滤统计信息解决基数预估错误

基数预估是SQL Server里一颗隐藏的宝石。一般而言&#xff0c;基数预估指的是&#xff0c;在查询编译期间&#xff0c;查询优化器尝试找出在执行计划里从各个运算符平均返回的行数。这个估计用来驱动计划本身生成并选择正确的计划运算符——例如像Nested Loop, Merge Join,还是…

C# 委托链、多路广播委托

委托链、多路广播委托&#xff1a;也就是把多个委托链接在一起,我们把链接了多个方法的委托称为委托链或多路广播委托 例&#xff1a; 1 class HelloWorld2 {3 //定义委托类型4 delegate void DelegationChain();5 static void Main(string[] args)6 …

openssl 生成证书_使用证书和私钥导出P12格式个人证书!

【OpenSSL】使用证书和私钥导出P12格式个人证书1, 产生CA证书1.1, 生成ca的私钥openssl genrsa -out cakey.pem 20481.2, 生成ca的自签名证书请求openssl req -new -key cakey.pem -subj "/CNExample Root CA" -out cacsr.pem1.3, 自签名ca的证书openssl x509 -req -…

Faster RCNN minibatch.py解读

minibatch.py 的功能是&#xff1a; Compute minibatch blobs for training a Fast R-CNN network. 与roidb不同的是&#xff0c; minibatch中存储的并不是完整的整张图像图像&#xff0c;而是从图像经过转换后得到的四维blob以及从图像中截取的proposals&#xff0c;以及与之对…

oracle精简版_使用Entity Framework Core访问数据库(Oracle篇)

前言哇。。看看时间 真的很久很久没写博客了 将近一年了。最近一直在忙各种家中事务和公司的新框架 终于抽出时间来更新一波了。本篇主要讲一下关于Entity Framework Core访问oracle数据库的采坑。。强调一下&#xff0c;本篇文章发布之前 关于Entity Framework Core访问oracl…

java String部分源码解析

String类型的成员变量 /** String的属性值 */ private final char value[];/** The offset is the first index of the storage that is used. *//**数组被使用的开始位置**/private final int offset;/** The count is the number of characters in the String. *//**String中…

javascript之闭包理解以及应用场景

1 function fn(){2 var a 0;3 return function (){4 return a;5 } 6 }如上所示&#xff0c;上面第一个return返回的就是一个闭包&#xff0c;那么本质上说闭包就是一个函数。那么返回这个函数有什么用呢&#xff1f;那是因为这个函数可以调用到它外部的a…

faster rcnn学习之rpn、fast rcnn数据准备说明

在上文《 faster-rcnn系列学习之准备数据》,我们已经介绍了imdb与roidb的一些情况&#xff0c;下面我们准备再继续说一下rpn阶段和fast rcnn阶段的数据准备整个处理流程。 由于这两个阶段的数据准备有些重合&#xff0c;所以放在一起说明。 我们并行地从train_rpn与train_fas…

sql server规范

常见的字段类型选择 1.字符类型建议采用varchar/nvarchar数据类型2.金额货币建议采用money数据类型3.科学计数建议采用numeric数据类型4.自增长标识建议采用bigint数据类型 (数据量一大&#xff0c;用int类型就装不下&#xff0c;那以后改造就麻烦了)5.时间类型建议采用为dat…

php 结构体_【开发规范】PHP编码开发规范下篇:PSR-2编码风格规范

之前的一篇文章是对PSR-1的基本介绍接下来是PSR-2 编码风格规范&#xff0c;它是 PSR-1 基本代码规范的继承与扩展。PSR-1 和PSR-2是PHP开发中基本的编码规范&#xff0c;大家其实都可以参考学习下&#xff0c;虽然说每个开发者都有自己熟悉的一套开发规范&#xff0c;但是我觉…

faster rcnn学习之rpn训练全过程

上篇我们讲解了rpn与fast rcnn的数据准备阶段&#xff0c;接下来我们讲解rpn的整个训练过程。最后 讲解rpn训练完毕后rpn的生成。 我们顺着stage1_rpn_train.pt的内容讲解。 name: "VGG_CNN_M_1024" layer {name: input-datatype: Pythontop: datatop: im_infotop: …

Android学习之高德地图的通用功能开发步骤(二)

周一又来了&#xff0c;我就接着上次的开发步骤&#xff08;一&#xff09;来吧&#xff0c;继续把高德地图的相关简单功能分享一下 上次写到了第六步&#xff0c;接着写第七步吧。 第七步&#xff1a;定位 地图选点 路径规划 实时导航 以下是我的这个功能NaviMapActivity的…