Use os_unfair_lock for MultiThreadDictionary instead of DispatchQueue
This commit is contained in:
parent
c2d1fe45d8
commit
77c44c323f
|
@ -24,7 +24,7 @@ class ImageCache {
|
|||
private let cache: ImageDataCache
|
||||
private let desiredPixelSize: CGSize?
|
||||
|
||||
private var groups = MultiThreadDictionary<URL, RequestGroup>(name: "ImageCache request groups")
|
||||
private var groups = MultiThreadDictionary<URL, RequestGroup>()
|
||||
|
||||
private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default)
|
||||
|
||||
|
@ -96,7 +96,7 @@ class ImageCache {
|
|||
if let data = data {
|
||||
try? self.cache.set(url.absoluteString, data: data, image: image)
|
||||
}
|
||||
self.groups.removeValueWithoutReturning(forKey: url)
|
||||
_ = self.groups.removeValue(forKey: url)
|
||||
}
|
||||
groups[url] = group
|
||||
return group
|
||||
|
|
|
@ -7,52 +7,79 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
class MultiThreadDictionary<Key: Hashable, Value> {
|
||||
private let name: String
|
||||
private var dict = [Key: Value]()
|
||||
private let queue: DispatchQueue
|
||||
// once we target iOS 16, replace uses of this with OSAllocatedUnfairLock<[Key: Value]>
|
||||
// to make the lock semantics more clear
|
||||
@available(iOS, obsoleted: 16.0)
|
||||
class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
|
||||
private let lock: any Lock<[Key: Value]>
|
||||
|
||||
init(name: String) {
|
||||
self.name = name
|
||||
self.queue = DispatchQueue(label: "MultiThreadDictionary (\(name)) Coordinator", attributes: .concurrent)
|
||||
init() {
|
||||
if #available(iOS 16.0, *) {
|
||||
self.lock = OSAllocatedUnfairLock(initialState: [:])
|
||||
} else {
|
||||
self.lock = UnfairLock(initialState: [:])
|
||||
}
|
||||
}
|
||||
|
||||
subscript(key: Key) -> Value? {
|
||||
get {
|
||||
var result: Value? = nil
|
||||
queue.sync {
|
||||
result = dict[key]
|
||||
return lock.withLock { dict in
|
||||
dict[key]
|
||||
}
|
||||
return result
|
||||
}
|
||||
set(value) {
|
||||
queue.async(flags: .barrier) {
|
||||
self.dict[key] = value
|
||||
lock.withLock { dict in
|
||||
dict[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeValueWithoutReturning(forKey key: Key) {
|
||||
queue.async(flags: .barrier) {
|
||||
self.dict.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// If the result of this function is unused, it is preferable to use `removeValueWithoutReturning` as it executes asynchronously and doesn't block the calling thread.
|
||||
func removeValue(forKey key: Key) -> Value? {
|
||||
var value: Value? = nil
|
||||
queue.sync(flags: .barrier) {
|
||||
value = dict.removeValue(forKey: key)
|
||||
return lock.withLock { dict in
|
||||
dict.removeValue(forKey: key)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func contains(key: Key) -> Bool {
|
||||
var value: Bool!
|
||||
queue.sync {
|
||||
value = dict.keys.contains(key)
|
||||
return lock.withLock { dict in
|
||||
dict.keys.contains(key)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func withLock<R>(_ body: @Sendable (inout [Key: Value]) throws -> R) rethrows -> R where R: Sendable {
|
||||
return try lock.withLock(body)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: replace this only with OSAllocatedUnfairLock
|
||||
@available(iOS, obsoleted: 16.0)
|
||||
fileprivate protocol Lock<State> {
|
||||
associatedtype State
|
||||
func withLock<R>(_ body: @Sendable (inout State) throws -> R) rethrows -> R where R: Sendable
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
extension OSAllocatedUnfairLock: Lock {
|
||||
}
|
||||
|
||||
// from http://www.russbishop.net/the-law
|
||||
fileprivate class UnfairLock<State>: Lock {
|
||||
private var lock: UnsafeMutablePointer<os_unfair_lock>
|
||||
private var state: State
|
||||
init(initialState: State) {
|
||||
self.state = initialState
|
||||
self.lock = .allocate(capacity: 1)
|
||||
self.lock.initialize(to: os_unfair_lock())
|
||||
}
|
||||
deinit {
|
||||
self.lock.deallocate()
|
||||
}
|
||||
func withLock<R>(_ body: (inout State) throws -> R) rethrows -> R where R: Sendable {
|
||||
os_unfair_lock_lock(lock)
|
||||
defer { os_unfair_lock_unlock(lock) }
|
||||
return try body(&state)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ struct AccountDisplayNameLabel<Account: AccountProtocol>: View {
|
|||
let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange)
|
||||
guard !matches.isEmpty else { return }
|
||||
|
||||
let emojiImages = MultiThreadDictionary<String, Image>(name: "AcccountDisplayNameLabel Emoji Images")
|
||||
let emojiImages = MultiThreadDictionary<String, Image>()
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
|
|
|
@ -38,10 +38,7 @@ extension BaseEmojiLabel {
|
|||
return
|
||||
}
|
||||
|
||||
// not using a MultiThreadDictionary so that cached images can be added immediately
|
||||
// without jumping through various queues so that we can use them immediately
|
||||
// in building either the final string or the string with placeholders
|
||||
var emojiImages: [String: UIImage] = [:]
|
||||
let emojiImages = MultiThreadDictionary<String, UIImage>()
|
||||
var foundEmojis = false
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
@ -71,12 +68,8 @@ extension BaseEmojiLabel {
|
|||
group.leave()
|
||||
return
|
||||
}
|
||||
// sync back to the main thread to add the dictionary
|
||||
// todo: using the main thread for this isn't great
|
||||
DispatchQueue.main.async {
|
||||
emojiImages[emoji.shortcode] = transformedImage
|
||||
group.leave()
|
||||
}
|
||||
emojiImages[emoji.shortcode] = transformedImage
|
||||
group.leave()
|
||||
}
|
||||
if let request = request {
|
||||
emojiRequests.append(request)
|
||||
|
@ -92,21 +85,27 @@ extension BaseEmojiLabel {
|
|||
func buildStringWithEmojisReplaced(usePlaceholders: Bool) -> NSAttributedString {
|
||||
let mutAttrString = NSMutableAttributedString(attributedString: attributedString)
|
||||
|
||||
// replaces the emojis starting from the end of the string as to not alter the indices of preceeding emojis
|
||||
for match in matches.reversed() {
|
||||
let shortcode = (attributedString.string as NSString).substring(with: match.range(at: 1))
|
||||
let attachment: NSTextAttachment
|
||||
|
||||
if let emojiImage = emojiImages[shortcode] {
|
||||
attachment = NSTextAttachment(emojiImage: emojiImage, in: self.emojiFont, with: self.emojiTextColor)
|
||||
} else if usePlaceholders {
|
||||
attachment = NSTextAttachment(emojiPlaceholderIn: self.emojiFont)
|
||||
} else {
|
||||
continue
|
||||
// lock once for the entire loop, rather than lock/unlocking for each iteration to do the lookup
|
||||
// OSAllocatedUnfairLock.withLock expects a @Sendable closure, so this warns about captures of non-sendable types (attribute dstrings, text checking results)
|
||||
// even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878)
|
||||
// so, just ignore the warnings
|
||||
emojiImages.withLock { emojiImages in
|
||||
// replaces the emojis starting from the end of the string as to not alter the indices of preceeding emojis
|
||||
for match in matches.reversed() {
|
||||
let shortcode = (attributedString.string as NSString).substring(with: match.range(at: 1))
|
||||
let attachment: NSTextAttachment
|
||||
|
||||
if let emojiImage = emojiImages[shortcode] {
|
||||
attachment = NSTextAttachment(emojiImage: emojiImage, in: self.emojiFont, with: self.emojiTextColor)
|
||||
} else if usePlaceholders {
|
||||
attachment = NSTextAttachment(emojiPlaceholderIn: self.emojiFont)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
let attachmentStr = NSAttributedString(attachment: attachment)
|
||||
mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
|
||||
}
|
||||
|
||||
let attachmentStr = NSAttributedString(attachment: attachment)
|
||||
mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
|
||||
}
|
||||
|
||||
return mutAttrString
|
||||
|
|
Loading…
Reference in New Issue