继上篇《Spark源码分析之Job的调度模型与运行反馈》之后,我们继续来看第二阶段--Stage划分。

Stage划分的大体流程如下图所示:

前面提到,对于JobSubmitted事件,我们通过调用DAGScheduler的handleJobSubmitted()方法来处理。那么我们先来看下代码:

  1. // 处理Job提交的函数
  2. private[scheduler] def handleJobSubmitted(jobId: Int,
  3. finalRDD: RDD[_],
  4. func: (TaskContext, Iterator[_]) => _,
  5. partitions: Array[Int],
  6. callSite: CallSite,
  7. listener: JobListener,
  8. properties: Properties) {
  9. var finalStage: ResultStage = null
  10. // 利用最后一个RDD(finalRDD),创建最后的stage对象:finalStage
  11. try {
  12. // New stage creation may throw an exception if, for example, jobs are run on a
  13. // HadoopRDD whose underlying HDFS files have been deleted.
  14. // 根据最后一个RDD获取最后的stage
  15. finalStage = newResultStage(finalRDD, func, partitions, jobId, callSite)
  16. } catch {
  17. case e: Exception =>
  18. logWarning("Creating new stage failed due to exception - job: " + jobId, e)
  19. listener.jobFailed(e)
  20. return
  21. }
  22. // 创建一个ActiveJob对象
  23. val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
  24. // 清除RDD分区位置缓存
  25. // private val cacheLocs = new HashMap[Int, IndexedSeq[Seq[TaskLocation]]]
  26. clearCacheLocs()
  27. // 调用logInfo()方法记录日志信息
  28. logInfo("Got job %s (%s) with %d output partitions".format(
  29. job.jobId, callSite.shortForm, partitions.length))
  30. logInfo("Final stage: " + finalStage + " (" + finalStage.name + ")")
  31. logInfo("Parents of final stage: " + finalStage.parents)
  32. logInfo("Missing parents: " + getMissingParentStages(finalStage))
  33. val jobSubmissionTime = clock.getTimeMillis()
  34. // 将jobId-->ActiveJob的对应关系添加到HashMap类型的数据结构jobIdToActiveJob中去
  35. jobIdToActiveJob(jobId) = job
  36. // 将ActiveJob添加到HashSet类型的数据结构activeJobs中去
  37. activeJobs += job
  38. finalStage.setActiveJob(job)
  39. //2 获取stageIds列表
  40. // jobIdToStageIds存储的是jobId--stageIds的对应关系
  41. // stageIds为HashSet[Int]类型的
  42. // jobIdToStageIds在上面newResultStage过程中已被处理
  43. val stageIds = jobIdToStageIds(jobId).toArray
  44. // stageIdToStage存储的是stageId-->Stage的对应关系
  45. val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))
  46. listenerBus.post(
  47. SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))
  48. // 提交最后一个stage
  49. submitStage(finalStage)
  50. // 提交其他正在等待的stage
  51. submitWaitingStages()
  52. }

这个handleJobSubmitted()方法一共做了这么几件事:

第一,调用newResultStage()方法,生成Stage,包括最后一个Stage:ResultStage和前面的Parent Stage:ShuffleMapStage;

第二,创建一个ActiveJob对象job;

第三,清除RDD分区位置缓存;

第四,调用logInfo()方法记录日志信息;

第五,维护各种数据对应关系涉及到的数据结构:

(1)将jobId-->ActiveJob的对应关系添加到HashMap类型的数据结构jobIdToActiveJob中去;

(2)将ActiveJob添加到HashSet类型的数据结构activeJobs中去;

第六,提交Stage;

下面,除了提交Stage留在第三阶段外,我们挨个分析第二阶段的每一步。

首先是调用newResultStage()方法,生成Stage,包括最后一个Stage:ResultStage和前面的Parent Stage:ShuffleMapStage。代码如下:

  1. /**
  2. * Create a ResultStage associated with the provided jobId.
  3. * 用提供的jobId创建一个ResultStage
  4. */
  5. private def newResultStage(
  6. rdd: RDD[_],
  7. func: (TaskContext, Iterator[_]) => _,
  8. partitions: Array[Int],
  9. jobId: Int,
  10. callSite: CallSite): ResultStage = {
  11. // 根据fianl RDD获取parent stage及id,这个id为ResultStage的stageId
  12. val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, jobId)
  13. // 创建一个ResultStage,即为整个Job的finalStage
  14. // 参数:id为stage的id,rdd为stage中最后一个rdd,func为在分区上执行的函数操作,
  15. // partitions为rdd中可以执行操作的分区,parentStages为该stage的父stages,jobId为该stage
  16. val stage = new ResultStage(id, rdd, func, partitions, parentStages, jobId, callSite)
  17. // 将stage加入到stageIdToStage中
  18. stageIdToStage(id) = stage
  19. // 更新数据结构jobIdToStageIds
  20. updateJobIdStageIdMaps(jobId, stage)
  21. // 返回stage
  22. stage
  23. }

首先,根据fianl RDD获取parent stages及id,这个id为ResultStage的stageId;

其次,创建一个ResultStage,即为整个Job的finalStage;

然后,将stage加入到数据结构stageIdToStage中;

接着,更新数据结构jobIdToStageIds;

最后,返回这个ResultStage。

我们一步步来看。首先调用getParentStagesAndId()方法,根据fianl RDD获取parent stages及id,这个id为ResultStage的stageId。代码如下:

  1. /**
  2. * Helper function to eliminate some code re-use when creating new stages.
  3. */
  4. private def getParentStagesAndId(rdd: RDD[_], firstJobId: Int): (List[Stage], Int) = {
  5. // 获取parent stages
  6. val parentStages = getParentStages(rdd, firstJobId)
  7. // 获取下一个stageId,为AtomicInteger类型,getAndIncrement()能保证原子操作
  8. val id = nextStageId.getAndIncrement()
  9. // 返回parentStages和id
  10. (parentStages, id)
  11. }

这个id即为下一个stageId,通过AtomicInteger类型的getAndIncrement()获得,能够保证原子性。继续分析getParentStages()方法,通过它来获取final RDD的parent stage。代码如下:

  1. /**
  2. * Get or create the list of parent stages for a given RDD.  The new Stages will be created with
  3. * the provided firstJobId.
  4. */
  5. private def getParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = {
  6. // 用HashSet存储parents stage
  7. val parents = new HashSet[Stage]
  8. // 用HashSet存储已经被访问过的RDD
  9. val visited = new HashSet[RDD[_]]
  10. // We are manually maintaining a stack here to prevent StackOverflowError
  11. // caused by recursively visiting
  12. // 存储需要被处理的RDD。Stack中得RDD都需要被处理
  13. val waitingForVisit = new Stack[RDD[_]]
  14. // 定义一个visit函数,根据传入的RDD,如果之前没有处理过,标记为已处理,循环此RDD的依赖关系dependencies
  15. // 如果是ShuffleDependency,获取其parents;如果不是,则说明为同一stage,并压入Stack:waitingForVisit顶部
  16. def visit(r: RDD[_]) {
  17. if (!visited(r)) {// visited中没有的话
  18. // 将RDD r加入到visited,表示已经处理过了
  19. visited += r
  20. // Kind of ugly: need to register RDDs with the cache here since
  21. // we can't do it in its constructor because # of partitions is unknown
  22. // 循环Rdd r的依赖关系
  23. for (dep <- r.dependencies) {
  24. dep match {
  25. case shufDep: ShuffleDependency[_, _, _] =>
  26. // 如果是ShuffleDependency,获取其parents,添加到parents中去
  27. parents += getShuffleMapStage(shufDep, firstJobId)
  28. case _ =>
  29. // 否则,属于同一个stage,压入Stack顶部,后续再递归处理
  30. waitingForVisit.push(dep.rdd)
  31. }
  32. }
  33. }
  34. }
  35. // 将rdd压入Stack顶部
  36. waitingForVisit.push(rdd)
  37. // 循环waitingForVisit,弹出每个rdd
  38. while (waitingForVisit.nonEmpty) {
  39. // 调用visit()方法,处理每个rdd
  40. visit(waitingForVisit.pop())
  41. }
  42. // 返回得到的parents列表
  43. parents.toList
  44. }

getParentStages()方法在其内部定义了如下数据结构:

parents:用HashSet存储parents stages,即finalRDD的所有parent stages,也就是ShuffleMapStage;

visited:用HashSet存储已经被访问过的RDD,在RDD被处理前先存入该HashSet,保证存储在里面的RDD将不会被重复处理;

waitingForVisit:存储需要被处理的RDD。Stack中得RDD都需要被处理。

getParentStages()方法在其内部还定义了一个visit()方法,传入一个RDD,如果之前没有处理过,标记为已处理,并循环此RDD的依赖关系dependencies,如果是ShuffleDependency,调用getShuffleMapStage()方法获取其parent stage;如果不是,则说明为同一stage,并压入Stack:waitingForVisit顶部,等待后续通过visit()方法处理。所以,getParentStages()方法从finalRDD开始,逐渐往上查找,如果是窄依赖,证明在同一个Stage中,继续往上查找,如果是宽依赖,通过getShuffleMapStage()方法获取其parent stage,就能得到整个Job中所有的parent stages,也就是ShuffleMapStage。

接下来,我们看下getShuffleMapStage()方法的实现。代码如下:

  1. /**
  2. * Get or create a shuffle map stage for the given shuffle dependency's map side.
  3. * 针对给定的shuffle dependency的map端,获取或者创建一个ShuffleMapStage
  4. */
  5. private def getShuffleMapStage(
  6. shuffleDep: ShuffleDependency[_, _, _],
  7. firstJobId: Int): ShuffleMapStage = {
  8. // 从数据结构shuffleToMapStage中根据shuffleId获取,如果有直接返回,否则
  9. // 获取ShuffleDependency中的rdd,调用getAncestorShuffleDependencies()方法,
  10. // 循环每个parent,调用newOrUsedShuffleStage()方法,创建一个新的ShuffleMapStage,
  11. // 并加入到数据结构shuffleToMapStage中去
  12. //
  13. // 它的定义为:private[scheduler] val shuffleToMapStage = new HashMap[Int, ShuffleMapStage]
  14. shuffleToMapStage.get(shuffleDep.shuffleId) match {
  15. case Some(stage) => stage // 有则直接返回
  16. case None => // 没有
  17. // We are going to register ancestor shuffle dependencies
  18. // 调用getAncestorShuffleDependencies()方法,传入ShuffleDependency中的rdd
  19. // 发现还没有在shuffleToMapStage中注册的祖先shuffle dependencies
  20. getAncestorShuffleDependencies(shuffleDep.rdd).foreach { dep =>
  21. // 并循环返回的parents,调用newOrUsedShuffleStage()方法,创建一个新的ShuffleMapStage,
  22. // 并加入到数据结构shuffleToMapStage中去
  23. shuffleToMapStage(dep.shuffleId) = newOrUsedShuffleStage(dep, firstJobId)
  24. }
  25. // Then register current shuffleDep
  26. // 最后注册当前shuffleDep,并加入到数据结构shuffleToMapStage中,返回stage
  27. val stage = newOrUsedShuffleStage(shuffleDep, firstJobId)
  28. shuffleToMapStage(shuffleDep.shuffleId) = stage
  29. stage
  30. }
  31. }

从getShuffleMapStage()方法的注释就能看出,这个方法的主要作用就是针对给定的shuffle dependency的map端,获取或者创建一个ShuffleMapStage。为何是Get or create呢?通过源码得知,getShuffleMapStage()方法首先会根据shuffleDep.shuffleId从数据结构shuffleToMapStage中查找哦是否存在对应的stage,如果存在则直接返回,如果不存在,则调用newOrUsedShuffleStage()方法创建一个Stage并添加到数据结构shuffleToMapStage中,方便后续需要使用此Stage者直接使用。在此之前,会根据入参ShuffleDependency的rdd发现还没有在shuffleToMapStage中注册的祖先shuffle dependencies,然后遍历每个ShuffleDependency,调用newOrUsedShuffleStage()方法为每个ShuffleDependency产生Stage并添加到数据结构shuffleToMapStage中。

下面,我们看下这个getAncestorShuffleDependencies()方法的实现,代码如下:

  1. /** Find ancestor shuffle dependencies that are not registered in shuffleToMapStage yet */
  2. // 根据传入的RDD,发现还没有在shuffleToMapStage中未注册过的祖先shuffle dependencies
  3. private def getAncestorShuffleDependencies(rdd: RDD[_]): Stack[ShuffleDependency[_, _, _]] = {
  4. // 存放parents的栈:Stack
  5. val parents = new Stack[ShuffleDependency[_, _, _]]
  6. // 存放已经处理过的RDD的哈希表:HashSet
  7. val visited = new HashSet[RDD[_]]
  8. // We are manually maintaining a stack here to prevent StackOverflowError
  9. // caused by recursively visiting
  10. // 存放等待调用visit的RDD的栈:Stack
  11. val waitingForVisit = new Stack[RDD[_]]
  12. // 定义方法visit()
  13. def visit(r: RDD[_]) {
  14. if (!visited(r)) {// 如果之前没有处理过
  15. visited += r // 标记为已处理
  16. // 循环RDD的所有依赖
  17. for (dep <- r.dependencies) {
  18. dep match {
  19. case shufDep: ShuffleDependency[_, _, _] => // 如果是ShuffleDependency
  20. // 如果shuffleToMapStage中没有,添加到parents中
  21. if (!shuffleToMapStage.contains(shufDep.shuffleId)) {
  22. parents.push(shufDep)
  23. }
  24. case _ =>
  25. }
  26. // 将该dependence的rdd压入waitingForVisit栈顶部
  27. waitingForVisit.push(dep.rdd)
  28. }
  29. }
  30. }
  31. // 将RDD压入到waitingForVisit顶部
  32. waitingForVisit.push(rdd)
  33. // 循环waitingForVisit,针对每个RDD调用visit()方法
  34. while (waitingForVisit.nonEmpty) {
  35. visit(waitingForVisit.pop())
  36. }
  37. // 返回parents
  38. parents
  39. }

通过代码我们可以发现,它和getParentStages()方法的代码风格非常相似。在其内部也定义了三个数据结构:

parents:存放parents的栈,即Stack,用于存放入参RDD的在shuffleToMapStage中未注册过的祖先shuffle dependencies;

visited:存放已经处理过的RDD的哈希表,即HashSet;

waitingForVisit:存放等待被处理的RDD的栈,即Stack;

定义了一个visit()方法,入参为RDD,针对传入的RDD,如果之前没有处理过则标记为已处理,并循环RDD的所有依赖,如果是如果是ShuffleDependency,并且其依赖的shuffleId在shuffleToMapStage中没有,添加到parents中,否则直接跳过,最后无论为何种Dependency,都将该dependence的rdd压入waitingForVisit栈顶部,等待后续处理。

接下来,我们再看下newOrUsedShuffleStage()方法,其代码如下:

  1. /**
  2. * Create a shuffle map Stage for the given RDD.  The stage will also be associated with the
  3. * provided firstJobId.  If a stage for the shuffleId existed previously so that the shuffleId is
  4. * present in the MapOutputTracker, then the number and location of available outputs are
  5. * recovered from the MapOutputTracker
  6. *
  7. * 为给定的RDD创建一个ShuffleStage
  8. */
  9. private def newOrUsedShuffleStage(
  10. shuffleDep: ShuffleDependency[_, _, _],
  11. firstJobId: Int): ShuffleMapStage = {
  12. // 从shuffleDep中获取RDD
  13. val rdd = shuffleDep.rdd
  14. // 获取RDD的分区个数,即未来的task数目
  15. val numTasks = rdd.partitions.length
  16. // 构造一个ShuffleMapStage实例
  17. val stage = newShuffleMapStage(rdd, numTasks, shuffleDep, firstJobId, rdd.creationSite)
  18. if (mapOutputTracker.containsShuffle(shuffleDep.shuffleId)) {
  19. // 如果mapOutputTracker中存在
  20. // 根据shuffleId从mapOutputTracker中获取序列化的多个MapOutputStatus对象
  21. val serLocs = mapOutputTracker.getSerializedMapOutputStatuses(shuffleDep.shuffleId)
  22. // 反序列化
  23. val locs = MapOutputTracker.deserializeMapStatuses(serLocs)
  24. // 循环
  25. (0 until locs.length).foreach { i =>
  26. if (locs(i) ne null) {
  27. // locs(i) will be null if missing
  28. // 将
  29. stage.addOutputLoc(i, locs(i))
  30. }
  31. }
  32. } else {
  33. // 如果mapOutputTracker中不存在,注册一个
  34. // Kind of ugly: need to register RDDs with the cache and map output tracker here
  35. // since we can't do it in the RDD constructor because # of partitions is unknown
  36. logInfo("Registering RDD " + rdd.id + " (" + rdd.getCreationSite + ")")
  37. // 注册的内容为
  38. // 1、根据shuffleDep获取的shuffleId;
  39. // 2、rdd中分区的个数
  40. mapOutputTracker.registerShuffle(shuffleDep.shuffleId, rdd.partitions.length)
  41. }
  42. stage
  43. }

这个方法的主要完成了以下两件事:

1、构造一个ShuffleMapStage实例stage;

2、判断是否在mapOutputTracker中存在:

(1)如果不存在,调用mapOutputTracker的registerShuffle()方法注册一个,注册的内容为根据shuffleDep获取的shuffleId和rdd中分区的个数;

(2)如果存在,根据shuffleId从mapOutputTracker中获取序列化的多个MapOutputStatus对象,反序列化后循环,逐个添加到stage中。

紧接着,看下newShuffleMapStage()方法,其代码如下:

  1. /**
  2. * Create a ShuffleMapStage as part of the (re)-creation of a shuffle map stage in
  3. * newOrUsedShuffleStage.  The stage will be associated with the provided firstJobId.
  4. * Production of shuffle map stages should always use newOrUsedShuffleStage, not
  5. * newShuffleMapStage directly.
  6. */
  7. private def newShuffleMapStage(
  8. rdd: RDD[_],
  9. numTasks: Int,
  10. shuffleDep: ShuffleDependency[_, _, _],
  11. firstJobId: Int,
  12. callSite: CallSite): ShuffleMapStage = {
  13. // 获得parentStages和下一个stageId
  14. val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, firstJobId)
  15. // 创建一个ShuffleMapStage
  16. val stage: ShuffleMapStage = new ShuffleMapStage(id, rdd, numTasks, parentStages,
  17. firstJobId, callSite, shuffleDep)
  18. // 将stage加入到数据结构stageIdToStage
  19. stageIdToStage(id) = stage
  20. updateJobIdStageIdMaps(firstJobId, stage)
  21. stage
  22. }

可以发现,这个方法也调用了getParentStagesAndId()方法,这样,就形成了一个递归,按照RDD的依赖关系,由后往前,逐渐生成Stage。代码剩余的部分就是创建一个ShuffleMapStage,并将stage加入到数据结构stageIdToStage,以及调用updateJobIdStageIdMaps()方法更新相关数据结构。这个updateJobIdStageIdMaps()方法留待下面分析。

下面,简单看下mapOutputTracker注册的代码。

  1. // 注册shuffle
  2. def registerShuffle(shuffleId: Int, numMaps: Int) {
  3. // 将shuffleId、numMaps大小和MapStatus类型的Array数组的映射关系,放入mapStatuses中
  4. // mapStatuses为TimeStampedHashMap[Int, Array[MapStatus]]类型的数据结构
  5. if (mapStatuses.put(shuffleId, new Array[MapStatus](numMaps)).isDefined) {
  6. throw new IllegalArgumentException("Shuffle ID " + shuffleId + " registered twice")
  7. }
  8. }

很简单,将shuffleId、numMaps大小和MapStatus类型的Array数组的映射关系,放入mapStatuses中,mapStatuses为TimeStampedHashMap[Int, Array[MapStatus]]类型的数据结构。
       经历了这多又长又大篇幅的叙述,现在返回newResultStage()方法,在通过getParentStagesAndId()方法获取parent stages及其result stage的id后,紧接着创建一个ResultStage,并将stage加入到stageIdToStage中,最后在调用updateJobIdStageIdMaps()更新数据结构jobIdToStageIds后,返回stage。

下面,简单看下updateJobIdStageIdMaps()方法。代码如下:

  1. /**
  2. * Registers the given jobId among the jobs that need the given stage and
  3. * all of that stage's ancestors.
  4. */
  5. private def updateJobIdStageIdMaps(jobId: Int, stage: Stage): Unit = {
  6. // 定义一个函数updateJobIdStageIdMapsList()
  7. def updateJobIdStageIdMapsList(stages: List[Stage]) {
  8. if (stages.nonEmpty) {
  9. // 获取列表头元素
  10. val s = stages.head
  11. // 将jobId添加到Stage的jobIds中
  12. s.jobIds += jobId
  13. // 更新jobIdToStageIds,将jobId与stageIds的对应关系添加进去
  14. jobIdToStageIds.getOrElseUpdate(jobId, new HashSet[Int]()) += s.id
  15. val parents: List[Stage] = getParentStages(s.rdd, jobId)
  16. val parentsWithoutThisJobId = parents.filter { ! _.jobIds.contains(jobId) }
  17. updateJobIdStageIdMapsList(parentsWithoutThisJobId ++ stages.tail)
  18. }
  19. }
  20. // 调用函数updateJobIdStageIdMapsList()
  21. updateJobIdStageIdMapsList(List(stage))
  22. }

这个方法的实现比较简单,在其内部定义了一个函数updateJobIdStageIdMapsList(),首选传入result stage,将jobId添加到stage的jobIds中,更新jobIdToStageIds,将jobId与stageIds的对应关系添加进去,然后根据给定stage的RDD获取其parent stages,过滤出不包含此JobId的parents stages,再递归调用updateJobIdStageIdMapsList()方法,直到全部stage都处理完。

至此,第二阶段Stage划分大体流程已分析完毕,有遗漏或不清楚的地方,以后再查缺补漏以及细化及更正错误。

博客原地址:http://blog.csdn.net/lipeng_bigdata/article/details/50674189

Spark源码分析之三:Stage划分的更多相关文章

  1. Spark源码阅读(1): Stage划分

    Spark中job由action动作生成,那么stage是如何划分的呢?一般的解答是根据宽窄依赖划分.那么我们深入源码看看吧 一个action 例如count,会在多次runJob中传递,最终会到一个 ...

  2. spark 源码分析之三 -- LiveListenerBus介绍

    LiveListenerBus 官方说明如下: Asynchronously passes SparkListenerEvents to registered SparkListeners. 即它的功 ...

  3. Spark源码分析之四:Stage提交

    各位看官,上一篇<Spark源码分析之Stage划分>详细讲述了Spark中Stage的划分,下面,我们进入第三个阶段--Stage提交. Stage提交阶段的主要目的就一个,就是将每个S ...

  4. spark 源码分析之十九 -- Stage的提交

    引言 上篇 spark 源码分析之十九 -- DAG的生成和Stage的划分 中,主要介绍了下图中的前两个阶段DAG的构建和Stage的划分. 本篇文章主要剖析,Stage是如何提交的. rdd的依赖 ...

  5. Spark 源码分析系列

    如下,是 spark 源码分析系列的一些文章汇总,持续更新中...... Spark RPC spark 源码分析之五--Spark RPC剖析之创建NettyRpcEnv spark 源码分析之六- ...

  6. Spark源码分析之五:Task调度(一)

    在前四篇博文中,我们分析了Job提交运行总流程的第一阶段Stage划分与提交,它又被细化为三个分阶段: 1.Job的调度模型与运行反馈: 2.Stage划分: 3.Stage提交:对应TaskSet的 ...

  7. spark 源码分析之十九 -- DAG的生成和Stage的划分

    上篇文章 spark 源码分析之十八 -- Spark存储体系剖析 重点剖析了 Spark的存储体系.从本篇文章开始,剖析Spark作业的调度和计算体系. 在说DAG之前,先简单说一下RDD. 对RD ...

  8. Spark源码分析 – Shuffle

    参考详细探究Spark的shuffle实现, 写的很清楚, 当前设计的来龙去脉 Hadoop Hadoop的思路是, 在mapper端每次当memory buffer中的数据快满的时候, 先将memo ...

  9. Spark源码分析 – Dependency

    Dependency 依赖, 用于表示RDD之间的因果关系, 一个dependency表示一个parent rdd, 所以在RDD中使用Seq[Dependency[_]]来表示所有的依赖关系 Dep ...

随机推荐

  1. BZOJ【1607】轻拍牛头

    1607: [Usaco2008 Dec]Patting Heads 轻拍牛头 Time Limit: 3 Sec  Memory Limit: 64 MBSubmit: 1245  Solved: ...

  2. [LeetCode] Sudoku Solver 解数独,递归,回溯

    Write a program to solve a Sudoku puzzle by filling the empty cells. Empty cells are indicated by th ...

  3. shell 执行提示No such file or directory

    问题描述: 项目开发过程中ansible执行脚本失败,单独运行shell脚本提示:No such file or directory,脚本结构执行脚本a 调用另一脚本b, 但查看b脚本路径正确不存在找 ...

  4. AI创投的冰与火之歌:泡沫、跟风、短板和有钱花不出去的沮丧【转】

    转自:http://36kr.com/p/5071386.html 国内的AI行业仍处于野蛮生长阶段.热钱不少,优质项目却不多.创业者拿钱难,投资者有钱却花不出去. 编者按:本文来自微信公众号“刺猬公 ...

  5. 将linlayout布局转为bitmap图片和保存

    1.首先新建路径 File filedirs = new File(Environment.getExternalStorageDirectory(), "/YuLin/"); i ...

  6. Git Base 操作(二)

    1. 撤销修改 (1) 当改乱了工作区(working directory)某个文件的内容,想直接丢弃工作区中的修改时,用命令git checkout -- file. (2) 当不但改乱了工作区某个 ...

  7. BZOJ1801 [Ahoi2009]chess 中国象棋(DP, 计数)

    题目链接 [Ahoi2009]chess 中国象棋 设$f[i][j][k]$为前i行,$j$列放了1个棋子,$k$列放了2个棋子的方案数 分6种情况讨论,依次状态转移. #include <b ...

  8. iOS 动画笔记 (一)

    你也肯定喜欢炫酷的动画! 在APP中,动画就是一个点睛之笔!可以给用户增加一些独特的体验感,估计也有许多的和我一样的,看着那些觉得不错的动画,也就只能流口水的孩子,毕竟可能不知道从哪里下手去写!动画学 ...

  9. Oracle数据库搭建

  10. 串口调试利器--Minicom配置及使用详解.md

    因为现在电脑基本不配备串行接口,所以,usb转串口成为硬件调试时的必然选择.目前知道的,PL2303的驱动是有的,在dev下的名称是ttyUSB*. Minicom,是Linux下应用比较广泛的串口软 ...