Use os_unfair_lock for MultiThreadDictionary instead of DispatchQueue

This commit is contained in:
Shadowfacts 2022-09-11 22:09:04 -04:00
parent c2d1fe45d8
commit 77c44c323f
4 changed files with 80 additions and 54 deletions

View File

@ -24,7 +24,7 @@ class ImageCache {
private let cache: ImageDataCache private let cache: ImageDataCache
private let desiredPixelSize: CGSize? 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) private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default)
@ -96,7 +96,7 @@ class ImageCache {
if let data = data { if let data = data {
try? self.cache.set(url.absoluteString, data: data, image: image) try? self.cache.set(url.absoluteString, data: data, image: image)
} }
self.groups.removeValueWithoutReturning(forKey: url) _ = self.groups.removeValue(forKey: url)
} }
groups[url] = group groups[url] = group
return group return group

View File

@ -7,52 +7,79 @@
// //
import Foundation import Foundation
import os
class MultiThreadDictionary<Key: Hashable, Value> { // once we target iOS 16, replace uses of this with OSAllocatedUnfairLock<[Key: Value]>
private let name: String // to make the lock semantics more clear
private var dict = [Key: Value]() @available(iOS, obsoleted: 16.0)
private let queue: DispatchQueue class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
private let lock: any Lock<[Key: Value]>
init(name: String) { init() {
self.name = name if #available(iOS 16.0, *) {
self.queue = DispatchQueue(label: "MultiThreadDictionary (\(name)) Coordinator", attributes: .concurrent) self.lock = OSAllocatedUnfairLock(initialState: [:])
} else {
self.lock = UnfairLock(initialState: [:])
}
} }
subscript(key: Key) -> Value? { subscript(key: Key) -> Value? {
get { get {
var result: Value? = nil return lock.withLock { dict in
queue.sync { dict[key]
result = dict[key]
} }
return result
} }
set(value) { set(value) {
queue.async(flags: .barrier) { lock.withLock { dict in
self.dict[key] = value 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. /// 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? { func removeValue(forKey key: Key) -> Value? {
var value: Value? = nil return lock.withLock { dict in
queue.sync(flags: .barrier) { dict.removeValue(forKey: key)
value = dict.removeValue(forKey: key)
} }
return value
} }
func contains(key: Key) -> Bool { func contains(key: Key) -> Bool {
var value: Bool! return lock.withLock { dict in
queue.sync { dict.keys.contains(key)
value = 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)
} }
} }

View File

@ -36,7 +36,7 @@ struct AccountDisplayNameLabel<Account: AccountProtocol>: View {
let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange) let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange)
guard !matches.isEmpty else { return } guard !matches.isEmpty else { return }
let emojiImages = MultiThreadDictionary<String, Image>(name: "AcccountDisplayNameLabel Emoji Images") let emojiImages = MultiThreadDictionary<String, Image>()
let group = DispatchGroup() let group = DispatchGroup()

View File

@ -38,10 +38,7 @@ extension BaseEmojiLabel {
return return
} }
// not using a MultiThreadDictionary so that cached images can be added immediately let emojiImages = MultiThreadDictionary<String, UIImage>()
// 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] = [:]
var foundEmojis = false var foundEmojis = false
let group = DispatchGroup() let group = DispatchGroup()
@ -71,12 +68,8 @@ extension BaseEmojiLabel {
group.leave() group.leave()
return return
} }
// sync back to the main thread to add the dictionary emojiImages[emoji.shortcode] = transformedImage
// todo: using the main thread for this isn't great group.leave()
DispatchQueue.main.async {
emojiImages[emoji.shortcode] = transformedImage
group.leave()
}
} }
if let request = request { if let request = request {
emojiRequests.append(request) emojiRequests.append(request)
@ -92,21 +85,27 @@ extension BaseEmojiLabel {
func buildStringWithEmojisReplaced(usePlaceholders: Bool) -> NSAttributedString { func buildStringWithEmojisReplaced(usePlaceholders: Bool) -> NSAttributedString {
let mutAttrString = NSMutableAttributedString(attributedString: attributedString) let mutAttrString = NSMutableAttributedString(attributedString: attributedString)
// replaces the emojis starting from the end of the string as to not alter the indices of preceeding emojis // lock once for the entire loop, rather than lock/unlocking for each iteration to do the lookup
for match in matches.reversed() { // OSAllocatedUnfairLock.withLock expects a @Sendable closure, so this warns about captures of non-sendable types (attribute dstrings, text checking results)
let shortcode = (attributedString.string as NSString).substring(with: match.range(at: 1)) // 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)
let attachment: NSTextAttachment // so, just ignore the warnings
emojiImages.withLock { emojiImages in
if let emojiImage = emojiImages[shortcode] { // replaces the emojis starting from the end of the string as to not alter the indices of preceeding emojis
attachment = NSTextAttachment(emojiImage: emojiImage, in: self.emojiFont, with: self.emojiTextColor) for match in matches.reversed() {
} else if usePlaceholders { let shortcode = (attributedString.string as NSString).substring(with: match.range(at: 1))
attachment = NSTextAttachment(emojiPlaceholderIn: self.emojiFont) let attachment: NSTextAttachment
} else {
continue 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 return mutAttrString