Flink 1.6.0 Windows操作
原文连接 https://ci.apache.org/projects/flink/flink-docs-release-1.6/dev/stream/operators/windows.html
Windows是无限数据流(infinite streams)处理的核心,Windows将一个stream拆分成有限大小的"桶(buckets)",可以在这些桶上做计算操作。
窗口化的Flink程序的一般结构如下,第一个代码段中是分组的流,第二段是非分组的流。区别是分组的stream调用keyBy(...)
和window(...)
,非分组的stream中window(...)
换成了windowAll(...)
分组窗口(Keyed Windows)
stream
.keyBy(...) <- keyed versus non-keyed windows
.window(...) <- required: "assigner"
[.trigger(...)] <- optional: "trigger" (else default trigger)
[.evictor(...)] <- optional: "evictor" (else no evictor)
[.allowedLateness(...)] <- optional: "lateness" (else zero)
[.sideOutputLateData(...)] <- optional: "output tag" (else no side output for late data)
.reduce/aggregate/fold/apply() <- required: "function"
[.getSideOutput(...)] <- optional: "output tag"
无分组窗口(Non-Keyed Windows)
stream
.windowAll(...) <- required: "assigner"
[.trigger(...)] <- optional: "trigger" (else default trigger)
[.evictor(...)] <- optional: "evictor" (else no evictor)
[.allowedLateness(...)] <- optional: "lateness" (else zero)
[.sideOutputLateData(...)] <- optional: "output tag" (else no side output for late data)
.reduce/aggregate/fold/apply() <- required: "function"
[.getSideOutput(...)] <- optional: "output tag"
方括号[]
内的命令是可选的,这表明Flink允许你根据最符合你的要求来定义自己的window逻辑。
Window生命周期(Window Lifecycle)
简单地说,当一个属于window的元素到达之后这个window就创建了,而当时间(event time或者processing time)超过window的创建时间和用户指定的延迟时间相加时,窗口将被彻底清除。
Flink确保了==只清除基于时间的window,其他类型的window不清除==,例如:Global window。举个例子:对于一个每5分钟创建无覆盖的窗口,允许一个1分钟的时延的窗口策略,Flink将会在12:00到12:05这段时间内第一个元素到达时创建窗口,当水印(wartmark)通过12:06时,移除这个窗口。
此外,每个window都有一个 Trigger 和一个Function(例如:ProcessWindowFunction,ReduceFunction,AggregateFunction和FoldFunction)。Function包含了应用于窗口内容的计算,Trigger指定了函数何时被触发。一个触发策略可以是 "当窗口中的元素个数超过4个时" 或者 "当水印达到窗口的边界时"。触发器还可以决定在窗口创建和删除之间的任意时刻,清除窗口的内容。清除仅指清除window内的元素而不是window的元数据,新的数据还是可以被添加到当前的window中。
除了上面的提到之外,还可以指定一个 Evictor,Evictor可以在触发器触发之后和在函数被应用之前或者之后,清除窗口中的元素。
分组和非分组Windows(Keyed vs Non-Keyed Windows)
首先,第一件事是==指定stream是分组的还是未分组的,这个必须在定义window之前定义好==。使用keyBy(...)
会将stream拆分成逻辑分组的数据流,如果keyBy(...)
函数不被调用的话,stream将不是分组的。
在分组stream中,任何正在传入的事件的属性都可以被当做 Specifying Keys,分组stream将通过多任务并发执行window计算,每一个逻辑分组stream在执行中是独立地进行的。
在非分组stream中,原始stream不会拆分成多个逻辑stream并且所有的window逻辑将在一个任务中执行,并发度为1。
窗口分配器(Window Assingers)
指定完你的数据流是分组的还是非分组的之后,接下来需要定义一个窗口分配器(WindowAssigner)。窗口分配定义了元素如何分配到窗口中,这是通过调用window(...)
或者windowAll(...)
时,选择的窗口分配器(WindowAssigner)来指定的。
WindowAssigner是==负责将每一个到来的元素分配给一个或者多个窗口==,Flink提供了一些常用的预定义窗口分配器,即:滚动窗口(tumbling windows)、滑动窗口(sliding windows)、会话窗口(session windows)和全局窗口(globalwindows),也可以通过继承WindowAssigner类来自定义自己的窗口。除了全局窗口分配器,其他所有的内置窗口分配器都是通过时间来分配元素到窗口中的,这个时间要么是event time,要么是processing time。
了解更多processing time和event time的区别及时间戳(timestamp)和水印(watermark)是如何产生的,查看 event time
滚动窗口(Tumbling Windows)
滚动窗口分配器将每个元素分配的一个指定窗口大小的窗口中,==滚动窗口有一个固定的大小,并且不会出现重叠==。例如:指定了一个5分钟大小的滚动窗口,当前窗口将被评估并将按下图说明每5分钟创建一个新的窗口。
val input: DataStream[T] = ...
// 滚动event-time窗口
input
.keyBy(<key selector>)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.<windowed transformation>(<window function>)
// 滚动processing-time窗口
input
.keyBy(<key selector>)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.<windowed transformation>(<window function>)
// 每日偏移8小时的滚动event-time窗口
input
.keyBy(<key selector>)
.window(TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8)))
.<windowed transformation>(<window function>)
时间间隔可以通过Time.milliseconds(x)
,Time.seconds(x)
,Time.minutes(x)
等其中的一个来指定。
在上面的最后一个例子中,滚动窗口分配器接受了一个可选的偏移参数,可以用来改变窗口的排列。例如:没有偏移的情况,按小时的滚动窗口将按整点时间对齐,会得到一系列窗口如: 1:00:00 ~ 1:59:59、 2:00:00 ~ 2:59:59 等。如果指定了一个15分钟的偏移,将得到的窗口如下: 1:15:00 ~ 2:14:59、 2:15:00 ~ 3:14:59 等。
时间偏移一个很大的用处是用来调准非0时区的窗口,例如:在中国你需要指定一个8小时的时间偏移。
滑动窗口(Sliding Windows)
滑动窗口分配器将元素分配到固定长度的窗口中,与滚动窗口类似,窗口的大小由窗口大小参数来配置,另一个==窗口滑动参数控制滑动窗口开始的频率==。因此,滑动窗口如果滑动参数小于滚动参数的话,窗口是可以重叠的,在这种情况下元素会被分配到多个窗口中。例如:你有10分钟的窗口和5分钟的滑动,那么每个窗口中5分钟的窗口里包含着上个10分钟产生的数据。
val input: DataStream[T] = ...
// 滑动event-time窗口
input
.keyBy(<key selector>)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.<windowed transformation>(<window function>)
// 滑动processing-time窗口
input
.keyBy(<key selector>)
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.<windowed transformation>(<window function>)
// 偏移8小时的滑动processing-time窗口
input
.keyBy(<key selector>)
.window(SlidingProcessingTimeWindows.of(Time.hours(12), Time.hours(1), Time.hours(-8)))
.<windowed transformation>(<window function>)
除了需要指定滑动时间参数,其他与滚动窗口类似,也可以指定偏移
会话窗口(Session Windows)
会话窗口分配器==通过session活动来对元素进行分组==,会话窗口跟滚动窗口和滑动窗口相比,==不会有重叠和固定的开始时间和结束时间的情况==。相应的,当它在一个固定的时间周期内不再收到元素,即非活动间隔产生,那个这个窗口就会关闭。
会话窗口可以配置一个静态session gap或定义了非活跃周期的session gap提取函数。当这个非活跃周期产生,当前的会话窗口将关闭并且后续的元素将被分配到新的会话窗口中。
val input: DataStream[T] = ...
// event-time Session窗口,固定的Session gap
input
.keyBy(<key selector>)
.window(EventTimeSessionWindows.withGap(Time.minutes(10)))
.<windowed transformation>(<window function>)
// event-time Session窗口,动态的Session gap
input
.keyBy(<key selector>)
.window(EventTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[String] {
override def extract(element: String): Long = {
// 确定和返回session gap
}
}))
.<windowed transformation>(<window function>)
// processing-time Session窗口,固定的Session gap
input
.keyBy(<key selector>)
.window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
.<windowed transformation>(<window function>)
// processing-time Session窗口,动态的Session gap
input
.keyBy(<key selector>)
.window(DynamicProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[String] {
override def extract(element: String): Long = {
// 确定和返回session gap
}
}))
.<windowed transformation>(<window function>)
session windows没有一个固定的开始和结束时间。session windows操作为每一个到达的元素创建一个新的窗口,并合间隔时间小于指定时间间隔的窗口。为了进行合并,session windows的操作需要指定一个合并触发器(Trigger)和一个合并窗口函数(Window Function),如:ReduceFunction、AggregateFunction或者ProcessWindowFunction。
全局窗口(Global Windows)
全局窗口分配器==将所有具有相同key的元素分配到同一个全局窗口中==,这个窗口模式++仅适用于用户还需自定义触发器的情况++。否则,由于==全局窗口没有一个自然的结尾==,无法执行元素的聚合,将不会有计算被执行。
[图片上传失败...(image-24f35a-1534762158575)]
val input: DataStream[T] = ...
input
.keyBy(<key selector>)
.window(GlobalWindows.create())
.<windowed transformation>(<window function>)
窗口函数(Window Functions)
定义完窗口分配器后,我们还需要为每一个窗口指定我们需要执行的计算,这是窗口的责任,当系统决定一个窗口已经准备好执行之后,这个窗口函数将被用来处理窗口中的每一个元素(可能是分组的)。当一个窗口准备好之后,Flink是如何决定的?
window函数可以是ReduceFunction、AggregateFunction、FoldFunction或ProcessWindowFunction中的一个。前面两个更高效一些,因为在++每个窗口中增量地对每一个到达的元素执行聚合操作++。一个ProcessWindowFunction可以获取一个窗口中的所有元素的迭代器(Iterable)以及元素所属窗口的额外元信息。
有ProcessWindowFunction的窗口化操作会比其他的操作效率要差一些,因为Flink内部在调用函数之前会将窗口中的所有元素都缓存起来。这个可以通过ProcessWindowFunction和ReduceFunction、AggregateFunction、FoldFunction结合使用来获取窗口中所有元素的增量聚合和额外的窗口元数据
ReduceFunction
ReduceFunction指定了如何==通过两个输入的参数进行合并输出一个同类型的参数==的过程,Flink使用ReduceFunction来对窗口中的元素进行增量聚合。
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.reduce { (v1, v2) => (v1._1, v1._2 + v2._2) }
AggregateFunction
聚合函数是ReduceFunction的一种广义函数,具有三种类型:输入类型(in)、累加器类型(ACC)和输出类型(out)。输入类型是输入流中的元素类型,而聚合函数有一种将一个输入元素添加到累加器的方法。该接口还具有用于创建初始累加器的方法,用于将两个累加器合并为一个累加器,并从累加器中提取输出。
/**
* 这个AverageAggregate用来持续计算sum和count,getResult方法计算平均值
*/
class AverageAggregate extends AggregateFunction[(String, Long), (Long, Long), Double] {
// 创建初始累加器
override def createAccumulator() = (0L, 0L)
// 将一个输入元素添加到累加器
override def add(value: (String, Long), accumulator: (Long, Long)) =
(accumulator._1 + value._2, accumulator._2 + 1L)
// 输出结果
override def getResult(accumulator: (Long, Long)) = accumulator._1 / accumulator._2
// 合并累加器
override def merge(a: (Long, Long), b: (Long, Long)) =
(a._1 + b._1, a._2 + b._2)
}
/* */
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.aggregate(new AverageAggregate)
FoldFunction
1.6.0+已经过期
FoldFunction指定了一个输入元素如何与一个输出类型的元素合并的过程,这个FoldFunction会被每一个加入到窗口中的元素和当前的输出值增量地调用,第一个元素是与一个预定义的类型为输出类型的初始值合并。
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
// 追加所有输入的长整型到一个空的字符串中。
.fold("") { (acc, v) => acc + v._2 }
fold()
不能应用于Session window或者其他可合并的窗口中。
ProcessWindowFunction
一个ProcessWindowFunction获得一个包含了window中的所有元素的迭代器(Iterable),和一个Context对象包含访问时间和状态信息,提供了更大的灵活性。这些带来了性能的成本和资源的消耗,因为window中的元素无法进行增量迭代,而是缓存起来直到window被认为是可以处理时。
val input: DataStream[(String, Long)] = ...
input
.keyBy(_._1)
.timeWindow(Time.minutes(5))
.process(new MyProcessWindowFunction())
class MyProcessWindowFunction extends ProcessWindowFunction[(String, Long), String, String, TimeWindow] {
def process(key: String, context: Context, input: Iterable[(String, Long)], out: Collector[String]): () = {
var count = 0L
for (in <- input) {
count = count + 1
}
out.collect(s"Window ${context.window} count: $count")
}
}
上面的例子展示了统计一个window中元素个数,此外,还将window的信息添加到输出中。
使用ProcessWindowFunction来做简单的聚合操作,如:计数操作,性能是相当差的。将ReduceFunction跟ProcessWindowFunction结合起来,来获取增量聚合和添加到ProcessWindowFunction中的信息,性能更好。
ProcessWindowFunction with Incremental Aggregation
ProcessWindowFunction可以跟ReduceFunction、AggregateFunction或者FoldFunction结合来增量地对到达window中的元素进行聚合。当window关闭之后,ProcessWindowFunction就能提供聚合结果。当获取到WindowFunction额外的window元信息后就可以进行增量计算窗口了。
Incremental Window Aggregation with ReduceFunction
val input: DataStream[SensorReading] = ...
input
.keyBy(<key selector>)
.timeWindow(<duration>)
.reduce(
(r1: SensorReading, r2: SensorReading) => { if (r1.value > r2.value) r2 else r1 },
( key: String,
window: TimeWindow,
minReadings: Iterable[SensorReading],
out: Collector[(Long, SensorReading)] ) =>
{
val min = minReadings.iterator.next()
out.collect((window.getStart, min))
}
)
Incremental Window Aggregation with AggregateFunction
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.timeWindow(<duration>)
.aggregate(new AverageAggregate(), new MyProcessWindowFunction())
// Function definitions
class AverageAggregate extends AggregateFunction[(String, Long), (Long, Long), Double] {
...
}
class MyProcessWindowFunction extends ProcessWindowFunction[Double, (String, Double), String, TimeWindow] {
def process(key: String, context: Context, averages: Iterable[Double], out: Collector[(String, Double]): () = {
val average = averages.iterator.next()
out.collect((key, average))
}
}
Incremental Window Aggregation with FoldFunction
val input: DataStream[SensorReading] = ...
input
.keyBy(<key selector>)
.timeWindow(<duration>)
.fold (
("", 0L, 0),
(acc: (String, Long, Int), r: SensorReading) => { ("", 0L, acc._3 + 1) },
( key: String,
window: TimeWindow,
counts: Iterable[(String, Long, Int)],
out: Collector[(String, Long, Int)] ) =>
{
val count = counts.iterator.next()
out.collect((key, window.getEnd, count._3))
}
)
触发器(Triggers)
触发器决定了一个窗口何时可以被窗口函数处理,每一个窗口分配器都有一个默认的触发器,如果默认的触发器不能满足需要,你可以通过调用trigger(...)
来指定一个自定义的触发器。
触发器的接口有5个方法来允许触发器处理不同的事件:
onElement()
方法,每个元素被添加到窗口时调用onEventTime()
方法,当一个已注册的事件时间计时器启动时调用onProcessingTime()
方法,当一个已注册的处理时间计时器启动时调用onMerge()
方法,与状态性触发器相关,当使用会话窗口时,两个触发器对应的窗口合并时,合并两个触发器的状态。clear()
方法,执行任何需要清除的相应窗口
上面的方法中有两个需要注意的地方:
- 前三个通过返回一个TriggerResult来决定如何操作调用他们的事件,这些操作可以是下面操作中的一个:
CONTINUE:什么也不做
FIRE:触发计算
PURGE:清除窗口中的数据
FIRE_AND_PURGE:触发计算并清除窗口中的数据 - 这些函数可以注册 "处理时间定时器" 或者 "事件时间计时器",被用来为后续的操作使用
触发和清除(Fire and Purge)
一旦一个触发器决定一个窗口已经准备好进行处理,它将触发并返回FIRE
或者FIRE_AND_PURGE
。这是窗口操作==发送当前窗口结果的信号==,给定一个拥有一个ProcessWindowFunction的窗口,那么所有的元素都将发送到ProcessWindowFunction中(可能之后还会发送到驱逐器[Evitor]中)。ReduceFunction、AggregateFunction或者FoldFunction的窗口仅仅发送他们想要的聚合结果。
当一个触发器触发时,它可以是FIRE
或者FIRE_AND_PURGE
,如果是FIRE
,将保持window中的内容,如果是FIRE_AND_PURGE
,会清除window的内容。默认情况下,预实现的触发器仅仅是FIRE,不会清除window的状态。
清除操作仅清除window的内容,并留下潜在的窗口元信息和完整的触发器状态。
默认触发器(Default Triggers of WindowAssigners)
默认的触发器适用于许多种情况,例如:所有的事件时间分配器都有一个EventTimeTrigger
作为默认的触发器,这个触发器仅在当水印通过窗口的最后时间时触发。
GlobalWindow默认的触发器是
NeverTrigger
,是永远不会触发的,因此,在使用GlobalWindow时,需要定义一个自定义触发器。
通过调用
trigger(...)
来指定一个触发器,你就重写了WindowAssigner的默认触发器。例如:如果你为TumblingEventTimeWindows指定了一个CountTrigger,就不会再通过时间来获取触发了,而是通过计数。现在,如果你想通过时间和计数来触发的话,你需要写自定义的触发器。
内置的和自定义的触发器(Build-in and Custom Triggers)
Flink有一些内置的触发器:
- EventTimeTrigger,根据由水印衡量的事件时间的进度来的
- ProcessingTimeTrigger,根据处理时间来触发
- CountTrigger,一旦窗口中的元素个数超出了给定的限制就会触发
- PurgingTrigger,作为另一个触发器的参数并将它转换成一个清除类型
如果想实现一个自定义的触发器,需要使用抽象类Trigger。这个API还在优化中,后续的Flink版本可能会改变。
驱逐器(Evictors)
Flink的窗口模型允许指定一个除了WindowAssigner和Trigger之外的可选参数Evitor,这个可以通过调用evitor(...)
方法来实现。这个驱逐器可以在触发器触发之前或者之后,或者窗口函数被应用之前清理窗口中的元素。为了达到这个目的,Evitor接口有两个方法:
void evictBefore(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
void evictAfter(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
evitorBefore()
方法包含了在window function之前应用的驱逐逻辑,而evitorAfter()
方法包含了在window function之后应用的驱逐逻辑。在window function应用之前被驱逐的元素将不会再被window function处理。
Flink有三个预实现的驱逐器:
- CountEvitor:在窗口中保持一个用户指定数量的元素,并在窗口的开始处丢弃剩余的其他元素
- DeltaEvitor:通过一个DeltaFunction和一个阈值,计算窗口缓存中最近的一个元素和剩余的所有元素的delta值,并清除delta值大于或者等于阈值的元素
- TimeEvitor:对于一个给定的窗口,使用一个毫秒级的interval作为参数,它会找出元素中的最大时间戳max_ts,并清除时间戳小于(max_ts - interval)的元素。
默认情况下,所有预实现的evitor都是在window function前应用它们的逻辑
指定一个Evitor要防止预聚合,因为窗口中的所有元素必须得在计算之前传递到驱逐器中
Flink 并不保证窗口中的元素是有序的,所以驱逐器可能从窗口的开始处清除,元素到达的先后不是那么必要。
允许延迟(Allowed Lateness)
当使用event-time的window时,可能会出现元素到达晚了,Flink用来与事件时间联系的水印(watermark)已经过了元素所属的窗口的最后时间。
默认情况下,当水印已经过了窗口的最后时间时,晚到的元素会被丢弃。然而,Flink允许为窗口操作指定一个最大允许时延,允许时延指定了元素可以晚到多长时间,默认情况下是0,也就是说水印之后到达的元素将被丢弃。
水印已经过了窗口最后时间后才来的元素,如果还未到窗口最后时间加时延时间,那么元素任然添加到窗口中。如果依赖触发器的使用的话,晚到但是未丢弃的元素可能会导致窗口再次被触发。
为了达到这个目的,Flink将保持窗口的状态直到允许时延的发生,一旦发生,Flink将清除Window,删除window的状态。
val input: DataStream[T] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.allowedLateness(<time>)
.<windowed transformation>(<window function>)
当使用GlobalWindows分配器时,没有数据会被认为是延迟的,因为Global Window的最后时间是Long.MAX_VALUE。
以Side Output来获取延迟数据(Getting late data as a side output)
使用Flink的 Side Output 特性,你可以获得一个已经被丢弃的延迟数据流。
首先你需要在窗口化的数据流中调用sideOutputLateData(OutputTag)指定你需要获取延迟数据。然后,你就可以在window操作的结果中获取到Side output了。
val lateOutputTag = OutputTag[T]("late-data")
val input: DataStream[T] = ...
val result = input
.keyBy(<key selector>)
.window(<window assigner>)
.allowedLateness(<time>)
.sideOutputLateData(lateOutputTag)
.<windowed transformation>(<window function>)
val lateStream = result.getSideOutput(lateOutputTag)
链接:https://www.jianshu.com/p/551b714bdbbf
Flink 1.6.0 Windows操作的更多相关文章
- 《从0到1学习Flink》—— 介绍Flink中的Stream Windows
前言 目前有许多数据分析的场景从批处理到流处理的演变, 虽然可以将批处理作为流处理的特殊情况来处理,但是分析无穷集的流数据通常需要思维方式的转变并且具有其自己的术语(例如,"windowin ...
- C++通用WMI接口实现获取Windows操作系统内核版本号
作为一名Windows开发者,能熟练掌握WMI技术,在开发Windows应用程序的时候往往能够事半功倍.今天来给大家分享一个使用WMI来获取Windows操作系统内核版本号的例子. 首先我们打开WMI ...
- 3.0 Windows和Linux双系统安装(3)
3.0 Windows和Linux双系统安装(3) 3.1 精简的安装步骤如下:(如果已经有了前面两篇教程的安装经验,推荐看完3.1即可动手了) 双系统很多开发新人会用到,而且比起虚拟机好处是运行效率 ...
- 修改代码150万行!与 Blink 合并后的 Apache Flink 1.9.0 究竟有哪些重大变更?
8月22日,Apache Flink 1.9.0 正式发布,早在今年1月,阿里便宣布将内部过去几年打磨的大数据处理引擎Blink进行开源并向 Apache Flink 贡献代码.当前 Flink 1. ...
- 官宣 | Apache Flink 1.12.0 正式发布,流批一体真正统一运行!
官宣 | Apache Flink 1.12.0 正式发布,流批一体真正统一运行! 原创 Apache 博客 [Flink 中文社区](javascript:void(0) 翻译 | 付典 Revie ...
- Windows操作Redis及Redis命令
Windows操作Redis及Redis命令 一.Windows下操作Redis 设置密码 打开redis服务 Windows 下的redis命令行 二.redis常用命令大全 key String ...
- Apache Flink 1.12.0 正式发布,DataSet API 将被弃用,真正的流批一体
Apache Flink 1.12.0 正式发布 Apache Flink 社区很荣幸地宣布 Flink 1.12.0 版本正式发布!近 300 位贡献者参与了 Flink 1.12.0 的开发,提交 ...
- Eclipse中通过Android模拟器调用OpenGL ES2.0函数操作步骤
原文地址: Eclipse中通过Android模拟器调用OpenGL ES2.0函数操作步骤 - 网络资源是无限的 - 博客频道 - CSDN.NET http://blog.csdn.net/fen ...
- cocos2d-x3.0 windows 环境配置
cocos2d-x3.0 windows 环境配置 参考Oo泡泡糖oO的CSDN博文 :http://blog.csdn.net/u010296979/article/details/24273393 ...
随机推荐
- IDHTTP
Delphi IDHTTP用法详解 一.IDHTTP的基本用法 IDHttp和WebBrowser一样,都可以实现抓取远端网页的功能,但是http方式更快.更节约资源,缺点是需要手动维护cook,连接 ...
- hdu2089数位DP
旁听途说这个名字很久了,了解了一下. 改题目的意思是给你若干区间,让你找寻区间内不含62或4的数. 首先暴力必然T...那么实际上就是说,想办法做一种预处理,在每次输入的时候取值运算就可以了. 既然是 ...
- 4. Jmeter主界面的介绍
上篇文章我们已经介绍过如何安装Jmeter.那么在本篇文章我们将要介绍Jmeter主界面有哪些功能.我们双击jmeter.bat,如下图所示(注意我这是jmeter5.0版本): 我们将Jmter主界 ...
- Sleepy与DbgHlp库学习
参考:http://msdn.microsoft.com/en-us/library/windows/desktop/ms679291(v=vs.85).aspx http://msdn.micros ...
- LeetCode 相交链表&环形链表II
题目链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists/ 题目连接:https://leetcode-cn.com ...
- 剑指offer——72圆圈中最后剩下的数字
题目描述 每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此.HF作为牛客的资深元老,自然也准备了一些小游戏.其中,有个游戏是这样的:首先,让小朋友们围成一个大圈.然后,他随机指 ...
- Spark中的多线程并发处理
Spark中的多任务处理 Spark的一个非常常见的用例是并行运行许多作业. 构建作业DAG后,Spark将这些任务分配到多个Executor上并行处理.但这并不能帮助我们在同一个Spark应用程序中 ...
- C# WinfForm 控件之dev报表 XtraReport (五) 并排报表
有了前边的基础这个就很简单了,建一个容器报表 在detail,上放两个xrsubReport.再做两个明细报表,分别指定到xrsubreport就可以了
- 新建pc端页面的模板
pc端页面,要做兼容.新建pc端模板时,先要初始化浏览器的样式,我命名为reset.css @charset "utf-8"; /* 取消链接高亮 */ body,div,ul,l ...
- hdu 3123 2009 Asia Wuhan Regional Contest Online
以为有啥牛逼定理,没推出来,随便写写就A了----题非常水,可是wa了一次 n>=m 则n!==0 注意的一点,最后 看我的凝视 #include <cstdio> #includ ...