本文大部分内容翻译至《Pro Design Pattern In Swift》By Adam Freeman,一些地方做了些许修改,并将代码升级到了Swift2.0,翻译不当之处望多包涵。
对象池模式的变形
这里我们将介绍如何改变对象池模式来满足不同的需求。
理解对象池模式的变种
实现对象池包含四种策略:
- 对象的创建策略
- 对象的重用策略
- 空对象池策略
- 对象的分配策略
通过改变这些策略,可以满足你需要的对象池的合适的实现。
理解对象的创建策略
在上一章中,我们使用了饥饿策略来创建对象,就是指对象在使用之前就已经被创建了出来。
Library.swift
...
private init(stockLevel:Int) {
books = [Book]()
for count in 1 ... stockLevel {
books.append(Book(author: "Dickens, Charles", title: "Hard Times",
stock: count))
}
pool = Pool<Book>(items:books)
}
...
这种策略适用于当面对代表真实世界资源的对象时,因为对象的个数事先就已经非常的清楚。但是这种策略的缺点也显而易见,那就是即使是没有任何需求的时候,所有的对象也都将被创建和配置。
另一种策略就是延迟创建策略,就是只有当有需求时对象才被创建。这里我们创建一个BookSeller类:
BookSerller.swift
import Foundation
class BookSeller {
class func buyBook(author:String, title:String, stockNumber:Int) -> Book {
return Book(author: author, title: title, stock: stockNumber)
}
}
BookSeller类定义了一个类方法用来创建Book对象。下面我们将修改Pool类来支持延迟对象的创建直到有需求发生。
Pool.swift
import Foundation
class Pool<T> {
private var data = [T]()
private let queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
private let semaphore:dispatch_semaphore_t
private var itemCount = 0
private let maxItemCount:Int
private let itemFactory: () -> T
init(maxItemCount:Int, factory:() -> T) {
self.itemFactory = factory
self.maxItemCount = maxItemCount
semaphore = dispatch_semaphore_create(maxItemCount)
}
func getFromPool() -> T? {
var result:T?
if (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) == 0) {
dispatch_sync(queue, { () -> Void in
if (self.data.count == 0 && self.itemCount < self.maxItemCount) {
result = self.itemFactory()
self.itemCount++
} else {
result = self.data.removeAtIndex(0);
}
})
}
return result
}
func returnToPool(item:T) {
dispatch_async(queue) { () -> Void in
self.data.append(item)
dispatch_semaphore_signal(self.semaphore)
}
}
func processPoolItems(callback:[T] -> Void) {
dispatch_barrier_sync(queue, {() in
callback(self.data)
})
}
}
我们改变了Pool类的初始化方法,它接收一个用来创建对象的闭包和一个代表对象池大小的Int类型值。
另一个改变是我们增加了一个方法processPoolItems用来记录借出信息。因为在原来代码中,对象的创建是由Library类中来实现,所以Library中可以保持对Book数组对引用。这里我们为了实现使用了回调方法来操作Book数组,而dispatch_barrier_sync保证了队列中所有任务完成后才会执行回调方法。
最后我们修改Library 类实现延迟创建:
Library.swift
import Foundation
class Library {
private let pool:Pool<Book>
static let sharedInstance = Library(stockLevel: 200)
private init(stockLevel:Int) {
var stockId = 1
pool = Pool<Book>(maxItemCount: stockLevel, factory: { () -> Book in
return BookSeller.buyBook("Dickens, Charles",
title: "Hard Times", stockNumber: stockId++)
})
}
func checkoutBook(reader:String) -> Book? {
let book = pool.getFromPool()
book?.reader = reader
book?.checkoutCount++
return book
}
func returnBook(book:Book) {
book.reader = nil
pool.returnToPool(book)
}
func printReport() {
pool.processPoolItems { (books) -> Void in
for book in books {
print("...Book#\(book.stockNumber)...")
print("Checked out \(book.checkoutCount) times")
if (book.reader != nil) {
print("Checked out to \(book.reader!)")
} else {
print("In stock")
}
}
}
}
}
如果执行程序,我们也许会得到以下的结果:
Starting...
All blocks complete
...Book#7...
Checked out 2 times
In stock
...Book#4...
Checked out 4 times
In stock
...Book#13...
Checked out 1 times
In stock
...Book#1...
Checked out 1 times
In stock
...Book#2...
Checked out 1 times
In stock
...Book#3...
Checked out 1 times
In stock
...Book#9...
Checked out 1 times
In stock
...Book#10...
Checked out 1 times
In stock
...Book#8...
Checked out 1 times
In stock
...Book#5...
Checked out 2 times
In stock
...Book#11...
Checked out 1 times
In stock
...Book#6...
Checked out 3 times
In stock
...Book#12...
Checked out 1 times
In stock
There are 13 books in the pool
因为我们增加了一个随机时间等待的缘故,Book的确切数字每次都会不同。最糟糕的情况是20本书全都被创建,那就意味着图书馆对于200本书的购买总额估计过高。如果我们这里使用饥饿策略,那么200本的副本都将被创建,延迟策略的优点就在于此。
理解对象的重用策略
对象池模式的性质意味着池中的对象会重复的分配给请求对象,这样就导致了一个风险那就是对象被归还的时候处在一个糟糕的状态。在现实世界的图书馆中,这就好比归还的书出现了撕裂或者缺页。在软件中,就意味着对象出现了不一致的状态或者遭遇了不可恢复的错误。
最简单的解决方法是信任策略,这就是你相信归还的对象都会是一个可重用的状态。对象池中管理的Book对象目前还没发生这种问题是因为它们几乎没提供任何可变的公开状态。
另一种就是不信任策略,当对象被归还之前做一个检查来确保它们都是可重用的状态。那些不能重用的对象都将被丢弃。
Caution:不信任策略只能用于那些有能力替代对象的对象池。如果没有能力去替代对象,那么对象池最终会耗尽。
我们将改变Book对象的使用方法来实现它们只能被借出特定的次数,反应了一个事实那就是书会承担磨损和消耗直到没办法在使用。下面我们定义一个PoolItem的协议。
PoolItem.swift
import Foundation
@objc protocol PoolItem {
var canReuse:Bool {get}
}
PoolItem 协议定义了一个只读属性canReuse,当对象被归还时我们将检查这个属性如果是false该对象将被遗弃。
Tip:注意到我们使用了@objc标注,这将支持对象向下转型成PoolItem,让我们能在Pool中读取canReuse属性。
接下来修改Pool类:
Pool.swift
import Foundation
class Pool<T> {
private var data = [T]()
private let queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
private let semaphore:dispatch_semaphore_t
private var itemCount = 0
private let maxItemCount:Int
private let itemFactory: () -> T
init(maxItemCount:Int, factory:() -> T) {
self.itemFactory = factory
self.maxItemCount = maxItemCount
semaphore = dispatch_semaphore_create(maxItemCount)
}
func getFromPool() -> T? {
var result:T?
if (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) == 0) {
dispatch_sync(queue, { () -> Void in
if (self.data.count == 0 && self.itemCount < self.maxItemCount) {
result = self.itemFactory()
self.itemCount++
} else {
result = self.data.removeAtIndex(0);
}
})
}
return result
}
func returnToPool(item:T) {
dispatch_async(queue) { () -> Void in
let pitem = item as AnyObject as? PoolItem;
if (pitem == nil || pitem!.canReuse) {
self.data.append(item)
dispatch_semaphore_signal(self.semaphore)
}
}
}
func processPoolItems(callback:[T] -> Void) {
dispatch_barrier_sync(queue, {() in
callback(self.data)
})
}
}
因为不想限制池中管理对象的范围,所以这里只检查那些实现了PoolItem协议的对象是否能重用。
...
let pitem = item as? PoolItem
if (pitem == nil || pitem!.canReuse) {
self.data.append(item)
dispatch_semaphore_signal(self.semaphore)
}
...
接着我们让Book类实现PoolItem协议:
import Foundation
class Book : PoolItem {
let author:String
let title:String
let stockNumber:Int
var reader:String?
var checkoutCount = 0
init(author:String, title:String, stock:Int) {
self.author = author
self.title = title
self.stockNumber = stock
}
var canReuse:Bool {
get {
let reusable = checkoutCount < 5
if (!reusable) {
print("Eject: Book#\(self.stockNumber)")
}
return reusable
}
}
}
为了测试这个策略,我们必须修改图书馆的书的总数,以便让读者借到临界线。
Library.swift
...
static let sharedInstance = Library(stockLevel: 5)
...
运行程序,得到下面结果:
Starting...
Eject: Book#5
Eject: Book#4
All blocks complete
...Book#2...
Checked out 3 times
In stock
...Book#1...
Checked out 3 times
In stock
...Book#3...
Checked out 4 times
In stock
There are 3 books in the pool
理解空对象池策略
空对象池策略,显而易见,就是对象池空了时发生请求时该怎么办。最简单的方法就是我们上面所用的,请求的线程一直等待直到有对象被归还。
阻塞策略虽然简单,但是如果池中的对象的总数和需求总数不协调的话就会拖慢应用程序。 如果阻塞策略和和前面提到的不信任管理策略结合在一起的话还会造成应用程序死锁。
我们做如下修改:
main.swift
import Foundation
var queue = dispatch_queue_create("workQ", DISPATCH_QUEUE_CONCURRENT)
var group = dispatch_group_create()
print("Starting...")
for i in 1 ... 35{
dispatch_group_async(group, queue, {() in
var book = Library.sharedInstance.checkoutBook("reader#\(i)")
if (book != nil) {
NSThread.sleepForTimeInterval(Double(rand() % 2))
Library.sharedInstance.returnBook(book!)
}
})
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
print("All blocks complete")
Library.sharedInstance.printReport()
我们将借书的请求从20次增加到了35次,很显然我们会得到下面的结果:
Starting...
Eject: Book#4
Eject: Book#5
Eject: Book#3
Eject: Book#2
Eject: Book#1
实现请求失败策略
请求失败策略通过将责任转移给请求对象的组件来处理空对象池的情况。组件必须指定在请求失败之前的等待时间。
Pool.swift
...
func getFromPool(maxWaitSeconds:Int = 5) -> T? {
var result:T?
let waitTime = (maxWaitSeconds == -1) ? DISPATCH_TIME_FOREVER : dispatch_time(DISPATCH_TIME_NOW, (Int64(maxWaitSeconds) * Int64(NSEC_PER_SEC)))
if (dispatch_semaphore_wait(semaphore, waitTime) == 0) {
dispatch_sync(queue, { () -> Void in
if (self.data.count == 0 && self.itemCount < self.maxItemCount) {
result = self.itemFactory()
self.itemCount++
} else {
result = self.data.removeAtIndex(0);
}
})
}
return result
}
...
Caution:DISPATCH_TIME_NOW 常量只能在dispatch_time方法中使用。如果你在其他地方使用,只会返回0。
我们将waitTime的值作为参数传递给了dispatch_semaphore_wait方法。
...
if (dispatch_semaphore_wait(semaphore, waitTime) == 0) {
...
dispatch_semaphore_wait 方法会一直阻塞直到returnToPool方法中的信号量发出信号或者超过指定的等待时间。
我们需要知道为什么信号量(semaphore)允许线程继续执行。如果是因为在对象池里有对象可用了,那么我们将获取对象并交给请求组件。如果是因为超过等待时间, 那么我们希望返回一个空值。
getFromPool方法已经是返回一个可选类型的Book对象。这就意味着Library 类已经设置好了应对获取对象失败的情况。
Library .swift
...
func checkoutBook(reader:String) -> Book? {
let book = pool.getFromPool()
book?.reader = reader
book?.checkoutCount++
return book
}
...
所以现在我们修改main.swift:
main.swift
import Foundation
var queue = dispatch_queue_create("workQ", DISPATCH_QUEUE_CONCURRENT)
var group = dispatch_group_create()
print("Starting...")
for i in 1 ... 35{
dispatch_group_async(group, queue, {() in
var book = Library.sharedInstance.checkoutBook("reader#\(i)")
if (book != nil) {
NSThread.sleepForTimeInterval(Double(rand() % 2))
Library.sharedInstance.returnBook(book!)
}else{
dispatch_barrier_async(queue, { () -> Void in
print("Request \(i) failed")
})
}
})
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
dispatch_barrier_sync(queue, {() -> Void in
print("All blocks complete")
Library.sharedInstance.printReport()
})
如果没有获取到对象,我们就向控制台输出失败信息。因为print方法并不是线程安全的,所谓我们把它放进了GCD块中。这里我们用了 dispatch_barrier_async是因为是并发队列(DISPATCH_QUEUE_CONCURRENT)。
如果我们运行程序,会得到下面的输出:
处理枯竭的对象池
前面所介绍的空对象池策略的问题是当池中对象耗尽时它要求请求组件等待。但是我们看到所有的对象都已经因为过度使用被丢弃了,而对象池也没有能力创建新的对象。
Pool.swift
import Foundation
class Pool<T> {
private var data = [T]()
private let queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
private let semaphore:dispatch_semaphore_t
private var itemCount = 0
private let maxItemCount:Int
private let itemFactory: () -> T
private var ejectedItems = 0
private var poolExhausted = false
init(maxItemCount:Int, factory:() -> T) {
self.itemFactory = factory
self.maxItemCount = maxItemCount
semaphore = dispatch_semaphore_create(maxItemCount)
}
func getFromPool(maxWaitSeconds:Int = 5) -> T? {
var result:T?
let waitTime = (maxWaitSeconds == -1) ? DISPATCH_TIME_FOREVER : dispatch_time(DISPATCH_TIME_NOW, (Int64(maxWaitSeconds) * Int64(NSEC_PER_SEC)))
if (!poolExhausted) {
if (dispatch_semaphore_wait(semaphore, waitTime) == 0) {
if (!poolExhausted) {
dispatch_sync(queue){() -> Void in
if (self.data.count == 0 && self.itemCount < self.maxItemCount) {
result = self.itemFactory()
self.itemCount++
} else {
result = self.data.removeAtIndex(0);
}
}
}
}
}
return result
}
func returnToPool(item:T) {
dispatch_async(queue) { () -> Void in
let pitem = item as? PoolItem
if (pitem!.canReuse) {
self.data.append(item)
dispatch_semaphore_signal(self.semaphore)
}else{
self.ejectedItems++
if (self.ejectedItems == self.maxItemCount) {
self.poolExhausted = true
self.flushQueue()
}
}
}
}
private func flushQueue() {
let dQueue = dispatch_queue_create("drainer", DISPATCH_QUEUE_CONCURRENT)
var backlogCleared = false
dispatch_async(dQueue) { () -> Void in
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)
backlogCleared = true
}
dispatch_async(dQueue) { () -> Void in
while (!backlogCleared) {
dispatch_semaphore_signal(self.semaphore)
}
}
}
func processPoolItems(callback:[T] -> Void) {
dispatch_barrier_sync(queue){ () in
callback(self.data)
}
}
}
上面所做的修改提出了两个问题:识别对象池耗尽和在此期间拒绝任何请求。为了识别对象池什么时候耗尽,我们在returnToPool中记录被丢弃的对象数。而且当所有的对象都被丢弃时,我们将实例变量 poolExhausted的值设置为true并且调用了flushQueue方法。
同时我们修改了getFromPool方法,所以在等待信号量发出之前都会检查属性poolExhausted是否为false,这意味着如果对象池耗尽的时候来值组件的请求都将会立即返回。
第二个问题是处理那些刚好在对象池耗尽之前的请求。这些请求线程会一直等待知道GCD发出信号。很不幸的是,GCD信号量(semaphore)并没有提供一个可以唤醒所有等待线程的方法,所以这里我们通过flushQueue 方法间接的去实现。
这里我们创建了另一个队列并为它添加了两个执行块。第一个执行块等待信号量并且当信号发送时设置局部变量backLogCleared 为ture。
...
dispatch_async(dQueue) { () -> Void in
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)
backlogCleared = true
}
...
GCD信号量允许线程通过是先进先出原则(FIFO),这就意味着只有当所有在对象池刚好耗尽之前等待的线程允许通过后,这个执行块线程才会允许被通过。
第二个执行块重复的向信号量发信号直到backLogCleared的值改变。
...
dispatch_async(dQueue) { () -> Void in
while (!backlogCleared) {
dispatch_semaphore_signal(self.semaphore)
}
}
...
这里增加执行块的队列是并发的,并且信号量会发出通知直到所有积压的线程通过后。我想阻止那些等到信号量的请求去执行修改数组的代码,所以在信号量发出后也对poolExhausted的值做了检查。
...
if (!poolExhausted) {
if (dispatch_semaphore_wait(semaphore, waitTime) == 0) {
if (!poolExhausted) {
...
我们将maxWaitSeconds参数的值改为-1来测试。只有当对象池耗尽的时候请求才会被拒绝,而且结果也和上面等待5秒的例子相似,但是这种实现阻止了当请求等待时间无上限并且对象池耗尽时请求线程被锁住的问题。下面是执行结果:
创建一个灵活的对象池
如果对象池有能力创建对象满足需求,那么就没有必要拒绝组件的请求。在软件开发方面,灵活的对象池可以一般用在那种需要创建额外的对象来满足增长的需求的情况。
在现实中,为了满足最高需求通常图书馆会从最近的分馆借书。这并不是理想的解决方案,但也意味着借书的人不用在长时间的等待了。请看下面例子:
BookSource.swift
import Foundation
class BookSeller {
class func buyBook(author:String, title:String, stockNumber:Int) -> Book {
return Book(author: author, title: title, stock: stockNumber)
}
}
class LibraryNetwork {
class func borrowBook(author:String, title:String, stockNumber:Int) -> Book {
return Book(author: author, title: title, stock: stockNumber)
}
class func returnBook(book:Book) {
// do nothing
}
}
接着我们修改对象池类:
import Foundation
class Pool<T> {
private var data = [T]()
private let queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
private let semaphore:dispatch_semaphore_t
private let itemFactory: () -> T
private let peakFactory: () -> T
private let peakReaper:(T) -> Void
private var createdCount:Int = 0
private let normalCount:Int
private let peakCount:Int
private let returnCount:Int
private let waitTime:Int
init(itemCount:Int, peakCount:Int, returnCount: Int, waitTime:Int = 2,
itemFactory:() -> T, peakFactory:() -> T, reaper:(T) -> Void) {
self.normalCount = itemCount
self.peakCount = peakCount
self.waitTime = waitTime
self.returnCount = returnCount
self.itemFactory = itemFactory
self.peakFactory = peakFactory
self.peakReaper = reaper
self.semaphore = dispatch_semaphore_create(itemCount)
}
func getFromPool() -> T? {
var result:T?
let expiryTime = dispatch_time(DISPATCH_TIME_NOW,(Int64(waitTime) * Int64(NSEC_PER_SEC)))
if(dispatch_semaphore_wait(semaphore, expiryTime) == 0 ){
dispatch_sync(queue, { () -> Void in
if(self.data.count == 0){
result=self.itemFactory()
self.createdCount++
}else{
result = self.data.removeAtIndex(0)
}
})
}else{
dispatch_sync(queue, { () -> Void in
result = self.peakFactory()
self.createdCount++
})
}
return result
}
func returnToPool(item:T) {
dispatch_async(queue) { () -> Void in
if (self.data.count>self.returnCount && self.createdCount>self.normalCount) {
self.peakReaper(item)
self.createdCount--
}else{
self.data.append(item)
dispatch_semaphore_signal(self.semaphore)
}
}
}
func processPoolItems(callback:[T] -> Void) {
dispatch_barrier_sync(queue){ () in
callback(self.data)
}
}
}
最后我们修改Library类:
...
private init(stockLevel:Int) {
var stockId = 1
pool = Pool<Book>(
itemCount:stockLevel,
peakCount: stockLevel * 2,
returnCount: stockLevel / 2,
itemFactory: {() in
return BookSeller.buyBook("Dickens, Charles",
title: "Hard Times", stockNumber: stockId++)},
peakFactory: {() in
return LibraryNetwork.borrowBook("Dickens, Charles",
title: "Hard Times", stockNumber: stockId++)},
reaper: LibraryNetwork.returnBook
)
}
...
运行程序,得到一下输出:
理解分配策略
分配策略决定了如何选择一个可用对象去满足一个请求。到目前为止,我们所用的是先进先出策略(FIFO),将数组像队列一样使用。这个策略的优点是使用简单,但同时也意味着对象会被不均衡的分配,某些请求成功次数会多余其他请求。
对于大多数应用来说,先进先出策略都会很合适,但是有些应用也会要求一些不同的分配策略。下面我们将返回最少利用次数的对象。
Pool.swift
import Foundation
class Pool<T> {
private var data = [T]()
private let queue = dispatch_queue_create("arrayQ", DISPATCH_QUEUE_SERIAL)
private let semaphore:dispatch_semaphore_t
private let itemFactory: () -> T
private let itemAllocator:[T] -> Int
private let maxItemCount:Int
private var createdCount:Int = 0
init(itemCount:Int, itemFactory:() -> T, itemAllocator:([T] -> Int)) {
self.maxItemCount = itemCount
self.itemFactory = itemFactory
self.itemAllocator = itemAllocator
self.semaphore = dispatch_semaphore_create(itemCount)
}
func getFromPool(maxWaitSeconds:Int = -1) -> T? {
var result:T?
if (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) == 0){
dispatch_sync(queue, { () -> Void in
if (self.data.count == 0) {
result = self.itemFactory()
self.createdCount++
}else{
result = self.data.removeAtIndex(self.itemAllocator(self.data))
}
})
}
return result
}
func returnToPool(item:T) {
dispatch_async(queue) { () -> Void in
self.data.append(item);
dispatch_semaphore_signal(self.semaphore)
}
}
func processPoolItems(callback:[T] -> Void) {
dispatch_barrier_sync(queue){ () in
callback(self.data)
}
}
}
这是一个延迟创建对象并且固定大小的对象池。我们定义了一个初始化参数itemAllocator,它是一个接受数组为参数的闭包,这个闭包返回数组中选定对象的位置。
接着我们修改Library类:
Library.swift
...
private init(stockLevel:Int) {
var stockId = 1
pool = Pool<Book>(
itemCount:stockLevel,
itemFactory: {() in
return BookSeller.buyBook("Dickens, Charles",
title: "Hard Times", stockNumber: stockId++)},
itemAllocator: {( books) in return 0 }
)
}
...
这时闭包返回的是0,也就是先进先出策略,运行程序:
我们再做修改,实现最少使用策略:
Library.swift
...
private init(stockLevel:Int) {
var stockId = 1
pool = Pool<Book>(
itemCount:stockLevel,
itemFactory: {() in
return BookSeller.buyBook("Dickens, Charles",
title: "Hard Times", stockNumber: stockId++)},
itemAllocator: {(books) in
var selected = 0
for index in 1 ..< books.count {
if (books[index].checkoutCount < books[selected].checkoutCount) {
selected = index
}
}
return selected}
)
}
...
此时运行程序,可以看出结果比FIFO策略更佳均衡: