CACHE的一些基本概念

Allocation



         在CACHE中发现一个位置,并把新的cache数据存到这个位置的过程。这个处理过程可能包括evicting(驱逐)cache中已存在的数据,从而为新的数据腾出空间。



Associativity



         指每个Set(集)里包含的line frames(行帧)数。也就是cache的way(路)数。



Capacity miss容量失效



         因为cache的容量限制,导致不能包括一个程序的整个working set(工作集),从而造成cache失效。这是三个cache失效原因(容量、强制、冲突)之一。



Clean干净



         一个有效的cache行,它的内容没有被更高层内存或CPU修改写过,我们称这个cache行是“干净”的,显然相反状态是dirty(“脏”)



Coherence一致性



         如果读内存任意地址的数据返回的总是这个地址的数据最近的更新,我们说内存系统是一致的。存取指CPU和EDMA等的存取。



Compulsory miss强制失效



         有时称为首次引用失效。强制失效是一种肯定发生的一种失效,因为数据事先从没有使用过也就没有机会被cache。但有些情况也被称为强制失效,尽管它们不是第一被引用的数据,比如对非写定位的cache进行同一位置的重复写,以及对一个不被cache内存区域的存取。这是三个cache失效原因(容量、强制、冲突)之一。



Conflict miss 冲突失效



         由于Associativity的限制导致的而非容量限制造成的cache失效。



Direct-mapped cache直接映射cache



         直接映射cache映射低层内存的每个地址到cache的一个单一位置。这样低层内存的多个地址可能被映射到cache中的同一位置上。它相当于1-way set-associative cache。



Dirty脏



         对writeback回写cache,写到达多级存储结构的某一层,并更新这一层内容,但并不更新更低层的内存,这样,当一个cache行有效并包含更新后的数据,但却不更新下一更低层的内存,我们说这个cache是“脏”的,显然一个有效行的相反状态是“干净”。



DMA直接内存存取



         直接内存存取,通常一个DMA操作copy一个内存块从一个区域到另一个区域,或在外设和内存之间传输数据。对C64x DSP,DMA传输使用EDAM,这些DMA传输与程序执行是并行的。从cache一致性角度,EDMA的存取可以看成是另外一个并行处理器。



Eviction驱逐



         从cache移出一个line从而为新的数据腾出空间的过程我们成为Eviction。Eviction可以由用户发出的writeback-invalidate产生,被驱逐的line我们称为victim line。当victim line是dirty(“脏”)的时候,数据必须回写道下一级存储器中以维持内存的一致性。



Execute packet执行包



         在一个周期里并行执行的一个指令块,一个执行包可以包含1-8个指令。



Fetch packet取指包



         1个周期里存取的包含8条指令的指令块。显然一个取指包可以包含多个执行包,这样可能消耗多个周期。



First-reference miss首次引用失效



         是强制失效的一种形式。见强制失效。



Fully-associative cache全关联cache



         任何内存地址都可以被存储在cache的任何位置。这样的cache非常灵活,但通常硬件上不可实现。这种cache同直接映射cache和集关联cache形成鲜明对比,这两种cache在定位策略方面都有更多的限制,全关联cache只具有概念上的意义,当分析直接映射cache和集相关cache性能时对区分冲突失效和容量失效是有用的,全关联cache等价于这样一个集关联cache:它有line frame个way,仅有一个set。



Higher-level memory高层内存



在多级内存系统中,距离CPU较近的内存称为高层内存。在C64x系统中,最高层内存通常是L1层CACHE,这一层的内存直接同CPU相连。较高层内存通常充当较低层内存的CACHE。



Hit命中



         当请求的内存地址的数据在cache中,我们说cache命中。命中的反义词是miss(失效)。Cache的命中使cpu的阻塞时间最短,因为才cache中去数据要比从原始位置取更快。在某一层内存的“失效”可能在较低层“命中”。



Invalidate无效



         将一个有效的行标记为无效行的过程。这相当于废弃了这一行的内容,但并不回写任何更新的数据。当与writeback组合时,会先将dirty数据更新到保存有这个地址的下一层内存。这通常用于维持内存的一致性。



Least Recently Used(LRU)allocation



         对于set-associative和fully-associative cache,最近最少使用原则被用来一个set里选择一个line frame作为被驱逐的line,用来保存新的cache数据。



Line行



         是cache处理的最小单位块。显然一个cache行的尺寸要比CPU或高层内存存取的数据尺寸要大。例如尽管CPU可以进行单字节的存取,但如果发生一次读失效,则cache会将整个cache行的数据读入。



Line frame行帧



         Cache的一个位置,包含被cache的数据(一行)、一个关联的tag地址,这个行的状态信息。状态信息包括这一行是否valid(有效)、dirty(脏)、LRU状态



Line size行尺寸



         一个行的尺寸,单位字节。



Load through



         当CPU请求在第一级和第二级cache中都失效时,数据从外部内存获得,同时会存储在第一级和第二级内存中,如果一个cache,它保存数据并同时把数据送到高一层cache中,这种cache我们称之为load-through cache。相对于那种首先存储数据到低层然后第二步再把数据送到高层的cache,这种cache可以减少阻塞时间。



Long-distance access长距离存取



         CPU对不被cache内存区域数据的存取。显然,由于这种存取,不会影响cache的状态,但速度会比较慢。



Lower-level memory 低层内存



         在多级内存系统中,距离CPU较远的内存称为低层内存。在C64x系统中,最低层内存包括L2以下的外部系统内存和内存映射的外设。



LRU



         表示cache line的最近最少使用的状态信息。



Memory ordering内存访问排序



    定义在内存里以什么样的顺序看内存操作的结果,有时称为consistency(连贯性)。在分级结构的特定层的强内存访问排序表示在这一层上不可能以不同与程序序的顺序来观察内存存取的结果。松散内存访问排序允许内存分级体系结构以不同的顺序看内存操作的结果,注意强排序并不要求系统内存以程序序顺序操作,而仅仅是使操作的结果在其它请求者看来是与程序序一致的。



Miss失效



         当请求的内存地址的数据不在cache中,就是说发生了cache失效。一个cache失效,会引起阻塞请求者直到line frame被定位,数据从低层内存中获得为止。在某些情况下,比如CPU对L1D出现写失效,阻塞CPU是不必要的。Cache失效通常分为三种类型:强制失效、冲突失效和容量失效。



Miss pipelining



         服务单一的cache失效的过程需要多个流水周期。如果对连续的失效处理进行流水线操作,显然就增加对失效处理的效率,降低了阻塞周期。



Read allocate



读失效cache是仅在发生读失效时才在cache中定位空间并把数据读入。写失效发生时不会产生数据读入到cache中,而是直接把写数据送到下一层cache中。



Set集



         Line frames的一个集合。直接映射的cache一个set包含一个line frame,N-way set-associative cache每个set包含N个line frame。Full-associative cache仅有一个set,这个唯一的set包含所有的line frames。



Set-associative cache集相关cache



         一个set-associative cache包含多个line frames用于cache低层内存。当为一个新的line数据定位一个空间时,选择哪个line frame是基于cache的定位策略的。C64x是基于LRU策略的。



Snoop侦测



         是lower-level memory(低层内存)查询higher-level memory(高层内存)以确定高层内存是否包含给定地址的数据的一种方法。Snoop的主要目的是通过允许低层内存请求高层内存进行数据更新来维持内存的一致性,snoop操作可以触发writeback(回写),或更普通的writeback-invalidate(回写并无效),触发writeback-invalidate(回写并无效)操作的snoop有时又称为snoop-invalidates。



Tag标签



         地址高位作为一个Tag存储在line中,在决定cache是否命中的时候,cache控制器会查询Tag。



Thrash



         当存取操作引起cache性能遭受严重损坏,我们说thrash cache了。Thrash的原因可以有很多:一个可能是算法在短时间存取太多的数据而很少或根本不重复使用。也就是说working set太大。这样的算法将引起非常严重的容量失效。另一种情况是算法重复存取一小组不同地址的数据,这些数据映射到cache的相同set,这样就人为造成严的冲突失效。



Touch



         对一个给定地址的存储器操作,我们称之为touch那个地址。Touch也可以指读数组元素或存储器地址的其它范围,定位它们在一个特定层cache的唯一目的。一个CPU中心环用作touch一个范围的内存是为了定位它到cache经常被引用作为一个touch环。Touching一个数组是软件控制预取数据的一种形式。



Valid有效



         当一个cache line保存从下一级内存取的数据,那么这个line frame就是有效的。无效状态发生在line frame不保存任何数据,或者说还没有被cache的数据,或先前cache的数据由于某种原因(内存一直性要求、程序要求等)被无效。有效的状态并不表示数据是否已经被修改(其实这有另外的状态来表示,即dirty或clean)。



Victim



从cache移出一个line从而为新的数据腾出空间的过程我们成为Eviction。Eviction可以由用户发出的writeback-invalidate产生,被驱逐的line我们称为victim。当victim line是dirty(“脏”)的时候,数据必须回写到下一级存储器中以维持内存的一致性。



Victim Buffer



         保存victim直到它们背回写的特殊buffer。Victim lines被移到victim buffer,这样位新的数据腾出空间。



Victim Writeback



         当victim line是dirty(“脏”)的时候,数据必须回写道下一级存储器中以维持内存的一致性。我们称这个过程为Victim Writeback



Way



         对set-associative cache,每个set包含多个line frames。每个set包含的line frames数被称为way。以set做为行,line frame作为列,那么一列的line frames集合称为一个way。例如,一个4-way set-associative cache有4个way,每个set有4个关联的line frames,结果,对于一个可cache的内存地址,在cache中有4个可能的映射位置。



Working Set



         程序或算法就是数据和指令代码的集合。在一个特定时间内被引用的这个集合我们称为working set(工作集)。通常,对于一个算法,我们需要考虑高层内存的工作集,对于整个程序我们需要考虑底层内存的工作集。



Write allocate



         对于一个write allocate cache,如果发生写失效,就会定位一个空间并将失效的数据读入到cache中。空间定位按照LRU原则。一旦数据被读入到cache中,就进行写操作,对write allocate cache,仅更新当前层的内存,写的数据并不立刻送到下一层更低的内存。



Writeback回写



         将一个有效的但为脏的cache line回写到较低层内存的过程。回写后的这个cache line我们称之为clean(“干净”)。回写后的cache line状态依然为有效。



Writeback cache回写cache



         回写cache仅修改它自己的写命中的数据,它不会立刻更新下一级较低层内存的内容。数据在将来某时被回写,如:当这个cache line被驱逐时,或当低层内存向高层内存snoop这个地址时,当然也可以直接通过程序发送writeback命令。一个写命中,会引起对应的line被标记为dirty(“脏”),也就是说数据被更新了,但还没来得及更新低层内存。



Writeback-invalidate回写后使无效



将一个有效的但为脏的cache line回写到较低层内存并置这些cache line为无效状态的过程。对C64x,对一组cache line进行Writeback-invalidate时,仅回写那些有效且为脏的cache lines,但却置那一组中的所有cache lines为无效。



Write merging



         Write merging是组合多个独立的写操作成一个、位宽更大的写操作。显然这可以改善了内存系统的性能。例如,对C64x CPU,L1D写缓冲在某些情况下(如果它们是对同一个双字地址的写)能合并多个写。



Write-through cache



         对于一个write-through cache,能把所有的写直接旁路到较低层内存。Cache中永远不包含更新的数据,这样做的结果是cache line永远不会是dirty。C64x的cache并不使用这种cache。

受害者缓存
所谓受害者缓存(Victim Cache),是一个与直接匹配或低相联缓存并用的、容量很小的全相联缓存。当一个数据块被逐出缓存时,并不直接丢弃,而是暂先进入受害者缓存。如果受害者缓存已满,就替换掉其中一项。当进行缓存标签匹配时,在与索引指向标签匹配的同时,并行查看受害者缓存,如果在受害者缓存发现匹配,就将其此数据块与缓存中的不匹配数据块做交换,同时返回给处理器。



受害者缓存的意图是弥补因为低相联度造成的频繁替换所损失的时间局部性。



为什么要设计缓存
电脑的内存是以系统总线的时钟频率工作的,这个频率通常也就是CPU的外频(对于雷鸟、毒龙系列的处理器,由于在设计采用了DDR技术,CPU工作的外频为系统总线频率的两倍)。但是,CPU的工作频率(主频)是外频与倍频因子的乘积。这样一来,内存的工作频率就远低于CPU的工作频率了,造成的直接结果是:CPU在执行完一条指令后,常常需要”等待”一些时间才能再次访问内存,极大降了CPU工作效率。在这样一种情况下,Cache就应运而生,Cache是一种特殊的存储器,它由Cache 存储部件和Cache控制部件组成。Cache 存储部件一般采用与CPU同类型的半导体存储器件,存取速度比内存快几倍甚至十几倍。
缓存(Cache)在计算机硬件中普遍存在。比如在CPU中就有一级缓存,二级缓存,甚至三级缓存。缓存的工作原理一般是CPU需要读取数据时,会首先从缓存中查找需要的数据,如果找到了就直接进行处理,如果没有找到则从内存中读取数据。缓存不仅仅存在于硬件中,在各种软件系统中也处处可见。比如在Web系统中,缓存存在于服务器端,客户端或者代理服务器中。广泛使用的CDN网络,也可以看作一个巨大的缓存系统。缓存在Web系统中的使用有很多好处,不仅可以减少网络流量,降低客户访问延迟,还可以减轻服务器负载。



缓存如何工作
单颗CPU运行程序是一条指令一条指令地执行的,而且指令地址往往是连续的,意思就是说CPU在访问内存时,在较短的一段时间内往往集中于某个局部,这时候可能会碰到一些需要反复调用的子程序。电脑在工作时,把这些活跃的子程序存入比内存快得多的Cache中。CPU在访问内存时,首先判断所要访问的内容是否在Cache中,如果在,就称为命中,此时CPU直接从Cache中调用该内容;否则,就称为不命中,CPU只好去内存中调用所需的子程序或指令了。CPU不但可以直接从Cache中读出内容,也可以直接往其中写入内容。由于Cache的存取速率相当快,使得CPU的利用率大大提高,进而使整个系统的性能得以提升。
CPU通过地址映射Cache和主存关联起来,从而确定将要访问的主存的内容是否在缓存中,所谓映象问题是指如何确定Cache中的内容是主存中的哪一部分的拷贝,即必须应用某种函数把主存地址映象到Cache中定位,也称地址映象。当信息按这种方式装入Cache中后,执行程序时,应将主存地址变换为Cache地址,这个变换过程叫作地址变换。



地址映象方式通常采用直接映象、全相联映象、组相联映象三种
直接映像
是指每个主存页只能复制到某一固定的Cache页中。直接映像的规律是:将主存的2048页分为128组,每组有16页,分别与Cache的16页直接对应,即主存的第0页、第16页、第32页……只能映像到Cache的第0页。
全相联映像
全相联映像是指主存的每一页可以映像可以映像到Cache的任意一页。
组相联映像
组相联映像是直接映像与全相联映像的折中方案,它将Cache分为若干组,如8组;每组若干页,如2页;同时将主存分为若干组,如255组;每组内的页数与Cache的组数相同,如8页。组相联映像的规律是主存中的各页与Cache的组号有固定的映像关系,但可自由映像到对应的Cache组中的任意一页。即组间采用直接映像,而组内的页为全相联映像。
缓存更新机制
当CPU访问Cache未命中时,应从主存中读取信息,同时写入Cache。若Cache未满,则直接写入;若Cache已满,则需要进行替换。替换机构由硬件组成,并按替换算法进行设计,其作用是指出替换的页号。常用的替换算法有先进先出算法(FIFO)和近期最少使用算法(LRU)。



缓存读/写操作



读操作
访存时,将主存地址同时送主存和Cache,一则启动对主存的读操作,二则在Cache中按映像方式从中获取Cache地址,并将主存标记与Cache标记比较:若相同,则访问命中,从Cache中读取数据。因为Cache速度比主存速度快,所以不等主存读操作结束,即可继续下一次访存操作;若不相同,则访问未命中,则从主存中读取数据,并考虑是否按某种替换算法更新Cache某页的内容。
写操作
将数据写入主存有两种方法,写回法和写直达法。
写回法: 信息暂时只写入Cache,并用标志加以注明,直到该页内容需从Cache中替换出来时,才一次写入主存。优点是操作速度快,缺点是写回主存前,主存中没有这些内容,与Cache不一致,易造成错误。
写直达法: 信息在写入Cahce时也同时写入主存。优点是主存与Cache始终保持一致,但速度慢。
目前已经存在很多高性能的缓存系统,比如Memcache,Redis等,尤其是Redis,现已经广泛用于各种Web服务中。既然有了这些功能完善的缓存系统,那为什么我们还需要自己实现一个缓存系统呢?这么做主要有两个原因,第一,通过动手实现我们可以了解缓存系统的工作原理,这也是老掉牙的理由了。第二,Redis 之类的缓存系统都是独立存在的,如果只是开发一个简单的应用还需要单独使用Redis服务器,难免会过于复杂。这时候如果有一个功能完善的软件包实现了这些功能,只需要引入这个软件包就能实现缓存功能,而不需要单独使用 Redis 服务器,就最好不过了。



缓存组件设计
缓存系统中,缓存的数据一般都存储在内存中,所以我们设计的缓存系统要以某一种方式管理内存中的数据。既然缓存数据是存储在内存中的,那如果系统停机,那数据岂不就丢失了吗?其实一般情况下,缓存系统还支持将内存中的数据写入到文件中,在系统再次启动时,再将文件中的数据加载到内存中,这样一来就算系统停机,缓存数据也不会丢失。



同时缓存系统还提供过期数据清理机制,也就是说缓存的数据项是有生存时间的,如果数据项过期,则数据项会从内存中被删除,这样一来热数据会一直存在,而冷数据则会被删除,也没有必要进行缓存。



缓存系统还需要对外提供操作的接口,这样系统的其他组件才能使用缓存。一般情况下,缓存系统需要支持CRUD操作,即创建(添加),读取,更新和删除操作。



通过以上分析,可以总结出缓存系统需要有以下功能:



缓存数据的存储;
过期数据项管理;
内存数据导出/导入;
提供CRUD接口.
缓存组件实现
缓存数据需要存储在内存中,这样才可以被快速访问。那使用什么数据结构来存储数据项呢?一般情况下,我们使用哈希表来存储数据项,这样访问数据项将获得更好的性能。在golang语言中内建类型map已经实现了哈希表,所以可以直接将缓存数据项存储在map中。同时由于缓存系统需要支持过期数据清理,所以缓存数据项应该带有生存时间,这说明需要将缓存数据进行封装后,保存到缓存系统中。这样我们就需要先实现缓存数据项,其实现的代码如下:



1
2
3
4
5
6
7
8
9
10
11
12
type Item struct {
Object interface{} // 真正的数据项
Expiration int64 // 生存时间
}



// 判断数据项是否已经过期
func (item Item) Expired() bool {
if item.Expiration == 0 {
return false
}
return time.Now().UnixNano() > item.Expiration
}
在Item结构中,
Object: 用于存储任意类型的数据对象;
Expiration: 字段则存储了该数据项的过期时间;
Expired()方法: 用于检测当前缓存数据项是否过期,返回false表示过期, 其中数据项的过期时间,是Unix时间戳,单位是纳秒; 用time包实现一个ticker定期检查每一项数据项,如果发现数据项的过期时间小于当前时间,则调用删除方法将数据项从缓存系统中删除.
现在来设计实现缓存组件的结构, 代码如下:



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const (
// 没有过期时间标志
NoExpiration time.Duration = -1



// 默认的过期时间
DefaultExpiration time.Duration = 0 )


type Cache struct {
defaultExpiration time.Duration
items map[string]Item // 缓存数据项存储在 map 中
mu sync.RWMutex // 读写锁
gcInterval time.Duration // 过期数据项清理周期
stopGc chan bool
}



// 过期缓存数据项清理
func (c *Cache) gcLoop() {
ticker := time.NewTicker(c.gcInterval)
for {
select {
case <-ticker.C:
c.DeleteExpired()
case <-c.stopGc:
ticker.Stop()
return
}
}
}



/ 删除缓存数据项
func (c *Cache) del(k string) {
delete(c.items, k)
}



// 删除过期数据项
func (c *Cache) DeleteExpired() {
now := time.Now().UnixNano()
c.mu.Lock()
defer c.mu.Unlock()



for k, v := range c.items {
if v.Expiration > 0 && now > v.Expiration {
c.del(k)
}
} } 缓存组件结构Cache, NoExpiration: 表示数据项永远不过期; DefaultExpiration: 后者表示数据项默认过期时间; items: 是一个map,用于存储缓存数据项; mu: 读写锁, 为保证缓存读写数据一致性,在相应操作前应加锁; gcInterval: 表示隔多久清理一次过期缓存数据; gcLoop()方法: 该方通过time.Ticker 定期执行DeleteExpired()方法,从而清理过期的数据项;通过监听c.stopGc通道,在必要的时候安全结束gcLoop(); 通过实现Set和Add方法向缓存系统中添加数据, 通过实现Get方法获取缓存数据,实现如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 设置缓存数据项,如果数据项存在则覆盖
func (c *Cache) Set(k string, v interface{}, d time.Duration) {
//已省略添加过期时间逻辑
c.mu.Lock()
defer c.mu.Unlock()
c.items[k] = Item{
Object: v,
Expiration: e,
}
}



// 设置数据项, 没有锁操作
func (c *Cache) set(k string, v interface{}, d time.Duration) {
// to-do

}



// 获取数据项,如果找到数据项,还需要判断数据项是否已经过期
func (c *Cache) get(k string) (interface{}, bool) {
// to-do
}



// 添加数据项,如果数据项已经存在,则返回错误
func (c *Cache) Add(k string, v interface{}, d time.Duration) error {
// to-do
}



// 获取数据项
func (c *Cache) Get(k string) (interface{}, bool) {
// to-do
}
Set()与Add()的主要区别在于Set()方法将数据添加到缓存系统中时,如果数据项已经存在则会覆盖,而后者如果发现数据项已经存在则会发生报错,这样能避免缓存被错误的覆盖;
通过Set(), Add()和Get()来写入/读取缓存数据时,应注意加锁保证数据操作的一致性。
除了向缓存系统中写入/读取缓存数据以外,还需要实现更新和删除缓存数据, 实现如下:



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 替换一个存在的数据项
func (c *Cache) Replace(k string, v interface{}, d time.Duration) error {
c.mu.Lock()
_, found := c.get(k)
if !found {
c.mu.Unlock()
return fmt.Errorf(“Item %s doesn’t exist”, k)
}
c.set(k, v, d)
c.mu.Unlock()
return nil
}



// 删除一个数据项
func (c *Cache) Delete(k string) {
c.mu.Lock()
c.del(k)
c.mu.Unlock()
}
为了是缓存数据在重启程序或系统后依然存在,则应将缓存数据持久化处理,本文方案提供将缓存数据写入本地文件的持久化操作,具体实现如下:



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 将缓存数据项写入到 io.Writer 中
func (c *Cache) Save(w io.Writer) (err error) {
enc := gob.NewEncoder(w)
defer func() {
if x := recover(); x != nil {
err = fmt.Errorf(“Error registering item types with Gob library”)
}
}()
c.mu.RLock()
defer c.mu.RUnlock()
for _, v := range c.items {
gob.Register(v.Object)
}
err = enc.Encode(&c.items)
return
}



// 保存数据项到文件中
func (c *Cache) SaveToFile(file string) error {
f, err := os.Create(file)
if err != nil {
return err
}
if err = c.Save(f); err != nil {
f.Close()
return err
}
return f.Close()
}



// 从 io.Reader 中读取数据项
func (c *Cache) Load(r io.Reader) error {
dec := gob.NewDecoder(r)
items := map[string]Item{}
err := dec.Decode(&items)
if err == nil {
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range items {
ov, found := c.items[k]
if !found || ov.Expired() {
c.items[k] = v
}
}
}
return err
}



// 从文件中加载缓存数据项
func (c *Cache) LoadFile(file string) error {
f, err := os.Open(file)
if err != nil {
return err
}
if err = c.Load(f); err != nil {
f.Close()
return err
}
return f.Close()
}
Save()通过gob模块将二进制缓存数据转码写入到实现了io.Writer 接口的对象中;
Load()则从io.Reader中读取二进制数据,然后通过gob模块将数据进行反序列化;
SaveToFile和LoadFile则向指定的文件中写入和读取缓存数据.
至此整个缓存系统基本实现,支持数据对象添加,删除,替换,和查询操作,同时还支持过期数据的删除。



https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/


Category golang