source: Monitor

获取指定对象的独占锁。

[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical, __DynamicallyInvokable]
public static extern void Enter(object obj);

src/vm/ecall.cpp

FCFuncStart(gMonitorFuncs)FCFuncElement("Enter", JIT_MonEnter)FCFuncElement("Exit", JIT_MonExit)FCFuncElement("TryEnterTimeout", JIT_MonTryEnter)FCFuncElement("ObjWait", ObjectNative::WaitTimeout)FCFuncElement("ObjPulse", ObjectNative::Pulse)FCFuncElement("ObjPulseAll", ObjectNative::PulseAll)FCFuncElement("ReliableEnter", JIT_MonReliableEnter)
FCFuncEnd()

next -> JIT_MonEnter

clr/src/vm/jithelpers.cpp

HCIMPL2(FC_BOOL_RET, JIT_MonTryEnter_Portable, Object* obj, INT32 timeOut)
{CONTRACTL {SO_TOLERANT;THROWS;DISABLED(GC_TRIGGERS);      // currently disabled because of FORBIDGC in HCIMPL} CONTRACTL_END;#if !defined(_X86_) && !defined(_AMD64_){//aware//adj. 意识到的;知道的;有…方面知识的;懂世故的//n. (Aware)人名;(阿拉伯、索)阿瓦雷AwareLock* awareLock = NULL; SyncBlock* syncBlock = NULL;//同步索引块ObjHeader* objHeader = NULL;//对象头 *:引用LONG state,oldvalue;DWORD tid;// DWORD 正体 : 四位元组 [电子计算机] int spincount = 50;// spin - 旋转 const int MaxSpinCount = 20000 * g_SystemInfo.dwNumberOfProcessors;Thread *pThread = GetThread();if (pThread->IsAbortRequested()) //为中止线程{goto FramedLockHelper;}if ((NULL == obj) || (timeOut < -1))//参数不正确{goto FramedLockHelper;}tid = pThread->GetThreadId();//获取线程idobjHeader = obj->GetHeader();//获取对象头while (true){//获取同步索引块的值//从此次可以看出同步索引块的值影响着lockoldvalue = objHeader->m_SyncBlockValue;if ((oldvalue & (BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX + BIT_SBLK_SPIN_LOCK + SBLK_MASK_LOCK_THREADID + SBLK_MASK_LOCK_RECLEVEL)) ==0)//经过计算结果若为0则表示即没有锁.{       if (tid > SBLK_MASK_LOCK_THREADID)//超过SBLK掩码锁定{goto FramedLockHelper;}LONG newvalue = oldvalue | tid;if (FastInterlockCompareExchangeAcquire((LONG*)&(objHeader->m_SyncBlockValue), newvalue, oldvalue) == oldvalue)//更新同步索引块 的值{pThread->IncLockCount();//实际操作: m_dwLockCount ++;FC_RETURN_BOOL(TRUE);//直接返回}continue;}//如果已存在值,且为hash或同步索引块下标。//?这里应该说明了两个点//  1. 存在同步索引块表 [通过下标获取]//  2. 同步索引块可以作用于hashcode 与 lock锁if (oldvalue & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX){goto HaveHashOrSyncBlockIndex;}if (oldvalue & BIT_SBLK_SPIN_LOCK){if (1 == g_SystemInfo.dwNumberOfProcessors){                goto FramedLockHelper;}}else if (tid == (DWORD) (oldvalue & SBLK_MASK_LOCK_THREADID)){LONG newvalue = oldvalue + SBLK_LOCK_RECLEVEL_INC;if ((newvalue & SBLK_MASK_LOCK_RECLEVEL) == 0){goto FramedLockHelper;}if (FastInterlockCompareExchangeAcquire((LONG*)&(objHeader->m_SyncBlockValue), newvalue, oldvalue) == oldvalue){FC_RETURN_BOOL(TRUE);}}else{// lock is held by someone elseif (0 == timeOut){FC_RETURN_BOOL(FALSE);}else {goto FramedLockHelper;}}// exponential backofffor (int i = 0; i < spincount; i++){YieldProcessor();//无操作}if (spincount > MaxSpinCount){goto FramedLockHelper;}spincount *= 3;} /* while(true) */HaveHashOrSyncBlockIndex:if (oldvalue & BIT_SBLK_IS_HASHCODE){goto FramedLockHelper;}syncBlock = obj->PassiveGetSyncBlock();if (NULL == syncBlock){goto FramedLockHelper;}awareLock = syncBlock->QuickGetMonitor(); ✨state = awareLock->m_MonitorHeld; ✨ if (state == 0){if (FastInterlockCompareExchangeAcquire((LONG*)&(awareLock->m_MonitorHeld), 1, 0) == 0)//进行CAS操作{syncBlock->SetAwareLock(pThread,1);✨pThread->IncLockCount();FC_RETURN_BOOL(TRUE);}else{goto FramedLockHelper;}}else if (awareLock->GetOwningThread() == pThread) /* monitor is held, but it could be a recursive case */{awareLock->m_Recursion++;//循环+1FC_RETURN_BOOL(TRUE);}
FramedLockHelper: ;//?参数检验并返回结果}
#endif // !_X86_ && !_AMD64_BOOL result = FALSE;OBJECTREF objRef = ObjectToOBJECTREF(obj);// The following makes sure that Monitor.TryEnter shows up on thread// abort stack walks (otherwise Monitor.TryEnter called within a CER can// block a thread abort for long periods of time). Setting the __me internal// variable (normally only set for fcalls) will cause the helper frame below// to be able to backtranslate into the method desc for the Monitor.TryEnter// fcall.__me = GetEEFuncEntryPointMacro(JIT_MonTryEnter);// Monitor helpers are used as both hcalls and fcalls, thus we need exact depth.HELPER_METHOD_FRAME_BEGIN_RET_ATTRIB_1(Frame::FRAME_ATTR_EXACT_DEPTH, objRef);if (objRef == NULL)COMPlusThrow(kArgumentNullException);if (timeOut < -1)COMPlusThrow(kArgumentException);result = objRef->TryEnterObjMonitor(timeOut);✨HELPER_METHOD_FRAME_END();FC_RETURN_BOOL(result);
}
HCIMPLEND

跟踪 awareLock = syncBlock->QuickGetMonitor(); ✨

clr/src/vm/syncblk.h

AwareLock* QuickGetMonitor()
{LEAF_CONTRACT;
// Note that the syncblock isn't marked precious, so use caution when
// calling this method.return &m_Monitor;
}

直接返回 &m_Monitor

这个m_Monitor在SyncBlock类中的定义:

protected: AwareLock  m_Monitor;                    // the actual monitor

所以说 就是获取了一个AwareLock的对象

state = awareLock->m_MonitorHeld; ✨

clr/src/vm/syncblk.h

public:volatile LONG   m_MonitorHeld;ULONG           m_Recursion;PTR_Thread      m_HoldingThread;private:LONG            m_TransientPrecious;// This is a backpointer from the syncblock to the synctable entry.  This allows// us to recover the object that holds the syncblock.DWORD           m_dwSyncIndex;CLREvent        m_SemEvent;// Only SyncBlocks can create AwareLocks.  Hence this private constructor.AwareLock(DWORD indx): m_MonitorHeld(0),m_Recursion(0),
#ifndef DACCESS_COMPILE
// PreFAST has trouble with intializing a NULL PTR_Thread.m_HoldingThread(NULL),
#endif // DACCESS_COMPILE          m_TransientPrecious(0),m_dwSyncIndex(indx){LEAF_CONTRACT;}

查看定义只有初始状态为0 所以 m_MonitorHeld应该是用来做CAS的相关变量

syncBlock->SetAwareLock(pThread,1);

clr/src/vm/syncblk.h 查看方法定义:

void SetAwareLock(Thread *holdingThread, DWORD recursionLevel)
{LEAF_CONTRACT;// <NOTE>// DO NOT SET m_MonitorHeld HERE!  THIS IS NOT PROTECTED BY ANY LOCK!!// </NOTE>m_Monitor.m_HoldingThread = PTR_Thread(holdingThread);m_Monitor.m_Recursion = recursionLevel;
}

从源码可以看出SetAwareLock就是给m_Monitor进行赋值,让m_Monitor的线程指向当前线程 且 循环次数为1

awareLock->GetOwningThread()

?应该就是获取m_Monitor的m_HoldingThread

result = objRef->TryEnterObjMonitor(timeOut);

clr/src/vm/object.h

查看Object的TryEnterObjMonitor定义:

BOOL TryEnterObjMonitor(INT32 timeOut = 0)
{WRAPPER_CONTRACT;return GetHeader()->TryEnterObjMonitor(timeOut);
}

调用了请求头的TryEnterObjMonitor

clr/src/vm/syncblk.cpp

查看ObjHeader的TryEnterObjMonitor方法定义:

BOOL ObjHeader::TryEnterObjMonitor(INT32 timeOut)
{WRAPPER_CONTRACT;return GetSyncBlock()->TryEnterMonitor(timeOut);
}

调用了同步索引块的TryEnterMonitor

clr/src/vm/syncblk.h

BOOL TryEnterMonitor(INT32 timeOut = 0)
{TryEnterWRAPPER_CONTRACT;return m_Monitor.TryEnter(timeOut);
}

之前已经知道了m_Monitor就是表示AwareLock

再到AwareLock的TryEnter:

BOOL AwareLock::TryEnter(INT32 timeOut)
{CONTRACTL{INSTANCE_CHECK;THROWS;GC_TRIGGERS;MODE_ANY;INJECT_FAULT(COMPlusThrowOM(););}CONTRACTL_END;if (timeOut != 0){LARGE_INTEGER qpFrequency, qpcStart, qpcEnd;BOOL canUseHighRes = QueryPerformanceCounter(&qpcStart);// try some more busy waitingif (Contention(timeOut))return TRUE;DWORD elapsed = 0;if (canUseHighRes && QueryPerformanceCounter(&qpcEnd) && QueryPerformanceFrequency(&qpFrequency))elapsed = (DWORD)((qpcEnd.QuadPart-qpcStart.QuadPart)/(qpFrequency.QuadPart/1000));if (elapsed >= (DWORD)timeOut)return FALSE;if (timeOut != (INT32)INFINITE)timeOut -= elapsed;}Thread  *pCurThread = GetThread();TESTHOOKCALL(AppDomainCanBeUnloaded(pCurThread->GetDomain()->GetId().m_dwId,FALSE));    if (pCurThread->IsAbortRequested()) {pCurThread->HandleThreadAbort();}retry:for (;;) {// Read existing lock state.LONG state = m_MonitorHeld;if (state == 0) //初始无锁状态{// Common case: lock not held, no waiters. Attempt to acquire lock by// switching lock bit.if (FastInterlockCompareExchange((LONG*)&m_MonitorHeld, 1, 0) == 0){break;}} else {// It's possible to get here with waiters but no lock held, but in this// case a signal is about to be fired which will wake up a waiter. So// for fairness sake we should wait too.// Check first for recursive lock attempts on the same thread.if (m_HoldingThread == pCurThread)//当前线程为锁线程{goto Recursion;}else{goto WouldBlock;}}}// We get here if we successfully acquired the mutex.m_HoldingThread = pCurThread;m_Recursion = 1;pCurThread->IncLockCount();#if defined(_DEBUG) && defined(TRACK_SYNC){// The best place to grab this is from the ECall frameFrame   *pFrame = pCurThread->GetFrame();int      caller = (pFrame && pFrame != FRAME_TOP ? (int) pFrame->GetReturnAddress() : -1);pCurThread->m_pTrackSync->EnterSync(caller, this);}
#endifreturn true;WouldBlock:// Didn't manage to get the mutex, return failure if no timeout, else wait// for at most timeout milliseconds for the mutex.if (!timeOut){return false;}// The precondition for EnterEpilog is that the count of waiters be bumped// to account for this threadfor (;;){// Read existing lock state.volatile LONG state = m_MonitorHeld;if (state == 0){goto retry;}if (FastInterlockCompareExchange((LONG*)&m_MonitorHeld, (state + 2), state) == state){break;}}return EnterEpilog(pCurThread, timeOut);Recursion:// Got the mutex via recursive locking on the same thread._ASSERTE(m_Recursion >= 1);m_Recursion++;
#if defined(_DEBUG) && defined(TRACK_SYNC)// The best place to grab this is from the ECall frameFrame   *pFrame = pCurThread->GetFrame();int      caller = (pFrame && pFrame != FRAME_TOP ? (int) pFrame->GetReturnAddress() : -1);pCurThread->m_pTrackSync->EnterSync(caller, this);
#endifreturn true;
}

再回到ObjHeader的GetSyncBlock()

//获取现有对象的同步块
// get the sync block for an existing object
SyncBlock *ObjHeader::GetSyncBlock()
{CONTRACT(SyncBlock *){INSTANCE_CHECK;THROWS;GC_NOTRIGGER;MODE_ANY;INJECT_FAULT(COMPlusThrowOM(););POSTCONDITION(CheckPointer(RETVAL));}CONTRACT_END;SyncBlock* syncBlock = GetBaseObject()->PassiveGetSyncBlock(); ✨DWORD      indx = 0;BOOL indexHeld = FALSE;if (syncBlock){// Has our backpointer been correctly updated through every GC?_ASSERTE(SyncTableEntry::GetSyncTableEntry()[GetHeaderSyncBlockIndex()].m_Object == GetBaseObject());RETURN syncBlock;}//需要从缓存中获取它//Need to get it from the cache{SyncBlockCache::LockHolder lh(SyncBlockCache::GetSyncBlockCache());//Try one more timesyncBlock = GetBaseObject()->PassiveGetSyncBlock();if (syncBlock)RETURN syncBlock;SyncBlockMemoryHolder syncBlockMemoryHolder(SyncBlockCache::GetSyncBlockCache()->GetNextFreeSyncBlock());syncBlock = syncBlockMemoryHolder;if ((indx = GetHeaderSyncBlockIndex()) == 0){indx = SyncBlockCache::GetSyncBlockCache()->NewSyncBlockSlot(GetBaseObject());}else{//We already have an index, we need to hold the syncblockindexHeld = TRUE;}{//! NewSyncBlockSlot has side-effects that we don't have backout for - thus, that must be the last//! failable operation called.CANNOTTHROWCOMPLUSEXCEPTION();FAULT_FORBID();syncBlockMemoryHolder.SuppressRelease();new (syncBlock) SyncBlock(indx);// after this point, nobody can update the index in the header to give an AD indexEnterSpinLock();{// If there's an appdomain index stored in the header, transfer it to the syncblockADIndex dwAppDomainIndex = GetAppDomainIndex();if (dwAppDomainIndex.m_dwIndex)syncBlock->SetAppDomainIndex(dwAppDomainIndex);// If the thin lock in the header is in use, transfer the information to the syncblockDWORD bits = GetBits();if ((bits & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX) == 0){DWORD lockThreadId = bits & SBLK_MASK_LOCK_THREADID;DWORD recursionLevel = (bits & SBLK_MASK_LOCK_RECLEVEL) >> SBLK_RECLEVEL_SHIFT;if (lockThreadId != 0 || recursionLevel != 0){// recursionLevel can't be non-zero if thread id is 0_ASSERTE(lockThreadId != 0);Thread *pThread = g_pThinLockThreadIdDispenser->IdToThreadWithValidation(lockThreadId);if (pThread == NULL){// The lock is orphaned.pThread = (Thread*) -1;}syncBlock->InitState();syncBlock->SetAwareLock(pThread, recursionLevel + 1);}}else if ((bits & BIT_SBLK_IS_HASHCODE) != 0){DWORD hashCode = bits & MASK_HASHCODE;syncBlock->SetHashCode(hashCode);}}SyncTableEntry::GetSyncTableEntry() [indx].m_SyncBlock = syncBlock;// in order to avoid a race where some thread tries to get the AD index and we've already nuked it,// make sure the syncblock etc is all setup with the AD index prior to replacing the index// in the headerif (GetHeaderSyncBlockIndex() == 0){// We have transferred the AppDomain into the syncblock above.SetIndex(BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | indx);}//If we had already an index, hold the syncblock//for the lifetime of the object.if (indexHeld)syncBlock->SetPrecious();ReleaseSpinLock();// SyncBlockCache::LockHolder goes out of scope here}}RETURN syncBlock;
}

先看 SyncBlock* syncBlock = GetBaseObject()->PassiveGetSyncBlock();

clr/src/vm/syncblk.h

Object *GetBaseObject()
{LEAF_CONTRACT;return (Object *) (this + 1);
}

先返回了Object

继续查看PassiveGetSyncBlock

//检索同步块,但不分配 // retrieve sync block but don't allocateSyncBlock *PassiveGetSyncBlock(){
#ifndef DACCESS_COMPILELEAF_CONTRACT;return g_pSyncTable [GetHeaderSyncBlockIndex()].m_SyncBlock;
#elseDacNotImpl();return NULL;
#endif // !DACCESS_COMPILE}

g_pSyncTable 此处也证实了 同步索引块表的存在

同步索引块后续再来追踪...


confirm

Every Object is preceded by an ObjHeader (at a negative offset).
每个对象前面都有一个ObjHeader(负偏移量)。The
的ObjHeader has an index to a SyncBlock.
ObjHeader有一个指向同步块的索引。This index is 0 for the bulk of all
大多数情况下,这个指数是0instances, which indicates that the object shares a dummy SyncBlock with
实例,它指示对象与一个虚拟同步块共享一个同步块most other objects.
大多数其他对象。The SyncBlock is primarily responsible for object synchronization.
SyncBlock主要负责对象同步。However,
然而,it is also a "kitchen sink" of sparsely allocated instance data.
它也是一个由稀疏分配的实例数据组成的“厨房水槽”。For instance,
例如,the default implementation of Hash() is based on the existence of a SyncTableEntry.
Hash()的默认实现基于SyncTableEntry的存在。And objects exposed to or from COM, or through context boundaries, can store sparse
暴露于COM或来自COM或通过上下文边界的对象可以稀疏存储data here.
这里的数据。SyncTableEntries and SyncBlocks are allocated in non-GC memory.
同步表项和同步块分配在非gc内存中。A weak pointer
一个弱指针from the SyncTableEntry to the instance is used to ensure that the SyncBlock and
从SyncTableEntry到实例,用于确保SyncBlock和SyncTableEntry are reclaimed (recycled) when the instance dies.
SyncTableEntry在实例死后被回收(回收)。The organization of the SyncBlocks isn't intuitive (at least to me).
同步块的组织并不直观(至少对我来说是这样)。Here's
这是the explanation:
解释:Before each Object is an ObjHeader.
每个对象之前都有一个ObjHeader。If the object has a SyncBlock, the
如果对象有同步块,则ObjHeader contains a non-0 index to it.
ObjHeader包含一个非0索引。The index is looked up in the g_pSyncTable of SyncTableEntries.
索引在SyncTableEntries的g_pSyncTable中查找。This means
这意味着the table is consecutive for all outstanding indices.
该表连续列出所有未清偿的指数。Whenever it needs to
无论何时需要grow, it doubles in size and copies all the original entries.
增长,它的大小翻倍,复制所有原始条目。The old table
旧的表is kept until GC time, when it can be safely discarded.
保存到GC时间,在GC时间可以安全地丢弃它。Each SyncTableEntry has a backpointer to the object and a forward pointer to
每个SyncTableEntry都有一个指向该对象的反向指针和一个指向该对象的正向指针the actual SyncBlock.
实际的SyncBlock。The SyncBlock is allocated out of a SyncBlockArray
同步块是从同步块射线中分配的which is essentially just a block of SyncBlocks.
本质上就是一组同步块。The SyncBlockArrays are managed by a SyncBlockCache that handles the actual
SyncBlockArrays由一个SyncBlockCache管理,它处理实际的allocations and frees of the blocks.
分配和释放块。So...
所以…Each allocation and release has to handle free lists in the table of entries
每个分配和发布都必须处理条目表中的空闲列表and the table of blocks.
和积木桌。We burn an extra 4 bytes for the pointer from the SyncTableEntry to the
从SyncTableEntry到SyncBlock.
SyncBlock。The reason for this is that many objects have a SyncTableEntry but no SyncBlock.
原因是许多对象都有SyncTableEntry,但没有SyncBlock。That's because someone (e.g. HashTable) called Hash() on them.
这是因为有人(例如HashTable)对它们调用了Hash()。Incidentally, there's a better write-up of all this stuff in the archives.
顺便说一句,在档案馆里有一个更好的关于这些东西的记录。

相关链接

https://github.com/SSCLI/sscli20_20060311

https://www.codeproject.com/Articles/184046/Spin-Lock-in-C

https://www.codeproject.com/Articles/18371/Fast-critical-sections-with-timeout

转载于:https://www.cnblogs.com/monster17/p/10881323.html

C# Monitor.TryEnter 源码跟踪相关推荐

  1. spring session spring:session:sessions:expires 源码跟踪

    2019独角兽企业重金招聘Python工程师标准>>> spring session spring:session:sessions:expires 源码跟踪 博客分类: sprin ...

  2. CAS 单点登出失效的问题(源码跟踪)

    一.环境说明 服务端:cas-server-3.5.2 客户端:cas-client-3.2.1+spring mvc 说明:服务端与客户端均是走的Https 客户端配置文件: application ...

  3. hibernate 三种查询方式源码跟踪及总结

    1.设置环境(以EClipse和hibernate 3.2.6为例) 1)首先,新建一个java 工程. 2) 将hiberante src导入到java的src目录下,此时多半会报错,不用管它! 3 ...

  4. zygoteinit.java_源码跟踪之启动流程:从ZygoteInit到onCreate

    Instrumentation SDK版本名称: Pie API Level: 28 一.源码调用时序图 1. Activity的启动流程 说明:其中ActivityThread中执行的schedul ...

  5. spring jdbctemplate源码跟踪

    闲着没事,看看源码也是一种乐趣! java操作数据库的基本步骤都是类似的: 1. 建立数据库连接 2. 创建Connection 3. 创建statement或者preparedStateement ...

  6. ConcurrentHashMap源码跟踪记录

    2019独角兽企业重金招聘Python工程师标准>>> concurrentHashMap源码解读 主要理解几个问题1 ConcurrentHashMap如何实现分段锁2 存取数据是 ...

  7. Tomat启动-源码跟踪

    <看透SpringMVC源码分析与实践> Tomcat源码分析--初始化与启动 tomcat环境搭建 源码下载 由于项目使用的tomcat版本时7.47,从apache svn check ...

  8. Launcher: 设置壁纸_源码跟踪

    网上有很多牛人研究 Launcher,说的都不错,但是个人还是觉得在技术方面还是各抒己见的为好,毕竟每个人研究的面不一样,借此,也想为自己做个笔记. 本博客主要是基于 android2.3.7 的源码 ...

  9. struts深入理解之登录示例的源码跟踪

    废话不多,直接上图:(色泽比较重的是追踪的路径) 转载于:https://www.cnblogs.com/davidwang456/p/3166077.html

最新文章

  1. 写论文查论文查参考文献
  2. VMware Fusion下的虚拟机绑定地址
  3. 用PlanAhead进行RTL代码开发与分析
  4. 深入了解数据分析丨《精益数据分析》超详细读书笔记
  5. eclipse mysql5.7_MySQL5.7、Navicate、jdk、Tomcat、eclipse全套配置及安装(win10)-Go语言中文社区...
  6. 探秘 | 平安人寿人工智能研发团队北京研发中心
  7. C++——《算法分析与设计》实验报告——二分搜索算法
  8. 张平文院士:展示计算数学的魅力
  9. 达梦数据库修改字段长度_解决达梦数据库新增大字段报错问题
  10. Android 8.0学习(25)---系统的应用图标适配
  11. 使用Lucene检索文档中的关键字
  12. Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation
  13. Virtualbox安装Ubuntu
  14. 弹出框、遮罩层demo
  15. 2021-08-04 Mysql联表查询
  16. sqool导出oracle数据
  17. mencoder 音视频格式转换
  18. 大学计算机基础知识点
  19. 冒泡排序Java实现以及时间复杂度分析
  20. 阿里云VPC网络内网实例通过SNAT连接外网

热门文章

  1. [JZOJ5542] 董先生的钦点
  2. 强化学习——day31 多臂老虎机MAB的代码实现(Python)
  3. “消费者至上:媒体新时代 ”主题响彻IBC2019
  4. npm install报错解决fatal: Unable to look up github.com (port 9418) npm ERR! exited with error code: 128
  5. Python中带“symmetric_”前缀的方法的特点
  6. 2018年1月iOS招人心得(附面试题)- 答案整理
  7. FSA-Net: Learning Fine-Grained Structure Aggregation for Head Pose Estimation from a Single Image
  8. 基于NLP的书法字体分析、统计及可视化
  9. 实践数据湖iceberg 第十四课 元数据合并(解决元数据随时间增加而元数据膨胀的问题)
  10. UEFI开发与调试---edk2中的应用模块/库模块/驱动模块