Skip to content

Commit 1155b17

Browse files
committed
Thead safe.
1 parent 1a2436a commit 1155b17

File tree

4 files changed

+81
-9
lines changed

4 files changed

+81
-9
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 B9Swift
3+
Copyright © 2019-2021 BB9z
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Using Swift Package Manager or import manually.
1515

1616
- NSHashTable free. It results better performance, support Liunx.
1717
- MulticastDelegate confirms `Sequence`, which means that lots of sequence features available.
18+
- Thread safe.
1819
- Other delightful details, eg: error handling optimization, debug log optimization.
1920

2021
## Background

Sources/B9MulticastDelegate/B9MulticastDelegate.swift

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
MulticastDelegate
33
B9Swift
44

5-
Copyright © 2019 BB9z
5+
Copyright © 2019-2021 BB9z
66
https://github.com/B9Swift/MulticastDelegate
77

88
The MIT License (MIT)
@@ -12,12 +12,15 @@
1212
import Foundation
1313

1414
/// Multicast delegate is a delegate that can have more than one element in its invocation list.
15+
///
16+
/// This class is thread safe.
1517
public final class MulticastDelegate<Element> {
1618

1719
public init() {
1820
}
1921

20-
private lazy var store = [Weak]()
22+
private var store = [Weak]()
23+
private let lock = NSLock()
2124

2225
/// Add an object to the multicast delegate.
2326
///
@@ -31,10 +34,14 @@ public final class MulticastDelegate<Element> {
3134
guard let d = delegate else { return }
3235
let weakRef = Weak(object: d as AnyObject)
3336
guard let dobj = weakRef.object else {
34-
print("[B9MulticastDelegate] warning: \(d) is not an object, it will be ignored. Adding a non-object to the delegate is meaningless.")
37+
print("[B9MulticastDelegate] warning: \(d) is not an object, it will be ignored. Adding a non-object as delegate is meaningless.")
38+
return
39+
}
40+
lock.lock()
41+
defer { lock.unlock() }
42+
if store.contains(where: { $0.object === dobj }) {
3543
return
3644
}
37-
if store.contains(where: { $0.object === dobj }) { return }
3845
store.append(weakRef)
3946
underestimatedCount += 1
4047
}
@@ -46,8 +53,10 @@ public final class MulticastDelegate<Element> {
4653
/// - Complexity: O(*n*), where *n* is the length of the internal storage.
4754
public func remove(_ delegate: Element?) {
4855
guard let d = delegate else { return }
56+
lock.lock()
4957
store.removeAll { $0.object === d as AnyObject || $0.object == nil }
5058
underestimatedCount = store.count
59+
lock.unlock()
5160
}
5261

5362
/// Calls the given closure on each object in the multicast delegate.
@@ -56,7 +65,10 @@ public final class MulticastDelegate<Element> {
5665
///
5766
/// - Parameter invocation: A closure that takes an object in the multicast delegate as a parameter.
5867
public func invoke(_ invocation: (Element) throws -> ()) rethrows {
59-
for ref in store {
68+
lock.lock()
69+
let shadowStore = store
70+
lock.unlock()
71+
for ref in shadowStore {
6072
if let d = ref.element {
6173
try invocation(d)
6274
}
@@ -73,6 +85,8 @@ public final class MulticastDelegate<Element> {
7385
///
7486
/// - Complexity: O(*n*), where *n* is the length of the internal storage.
7587
public func contains(object: AnyObject) -> Bool {
88+
lock.lock()
89+
defer { lock.unlock() }
7690
for weakRef in store {
7791
if weakRef.object === object {
7892
return true
@@ -115,10 +129,10 @@ extension MulticastDelegate: CustomStringConvertible {
115129
let address = Unmanaged.passUnretained(self).toOpaque()
116130
let itemsDescriptions = map { "\t\($0)" }
117131
if itemsDescriptions.isEmpty {
118-
return "<\(aType): \(address). elements: []>"
132+
return "<\(aType) \(address): elements: []>"
119133
}
120134
return """
121-
<\(aType): \(address). elements: [
135+
<\(aType) \(address): elements: [
122136
\(itemsDescriptions.joined(separator: ",\n"))
123137
]>
124138
"""

Tests/B9MulticastDelegateTests/B9MulticastDelegateTests.swift

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ class TestKindC {
2424
}
2525
}
2626

27+
class TestObject: CustomStringConvertible {
28+
init(description: String) {
29+
self.description = description
30+
}
31+
var description: String
32+
}
33+
2734
final class B9MulticastDelegateTests: XCTestCase {
2835

2936
func testAddAndRemove() {
@@ -94,7 +101,7 @@ final class B9MulticastDelegateTests: XCTestCase {
94101

95102
func testWeakRef() {
96103
let d = MulticastDelegate<XCTestCase>()
97-
autoreleasepool {
104+
do {
98105
let obj = XCTestCase()
99106
d.add(obj)
100107
XCTAssert(d.debugContent == [obj])
@@ -133,6 +140,56 @@ final class B9MulticastDelegateTests: XCTestCase {
133140
}
134141
XCTAssertEqual(errorCount, 1)
135142
}
143+
144+
func testSequenceThreadSafe() {
145+
let d = MulticastDelegate<CustomStringConvertible>()
146+
let queueRead = DispatchQueue(label: "Read")
147+
let queueWrite1 = DispatchQueue(label: "Write1")
148+
let queueWrite2 = DispatchQueue(label: "Write2")
149+
let readEnd = XCTestExpectation()
150+
let writeEnd1 = XCTestExpectation()
151+
let writeEnd2 = XCTestExpectation()
152+
153+
let objectCount = 2000
154+
let objs = (0...objectCount).map { TestObject(description: String($0)) }
155+
let objsProviderLock = NSLock()
156+
var numberOfObjectProvided = 0
157+
func object(of index: Int) -> TestObject {
158+
objsProviderLock.lock()
159+
defer { objsProviderLock.unlock() }
160+
numberOfObjectProvided += 1
161+
print("\(numberOfObjectProvided) + \(index)")
162+
return objs[index]
163+
}
164+
165+
queueWrite1.async {
166+
for i in 0..<(objectCount/2) {
167+
d.add(object(of: i))
168+
}
169+
writeEnd1.fulfill()
170+
}
171+
queueWrite2.async {
172+
for i in (objectCount/2)..<objectCount {
173+
d.add(object(of: i))
174+
}
175+
writeEnd2.fulfill()
176+
}
177+
queueRead.async {
178+
for i in 0...20 {
179+
usleep(100_000) // 100 ms
180+
print("loop \(i) start")
181+
var counter = 0
182+
d.forEach { obj in
183+
counter += 1
184+
}
185+
print("loop \(i) end, counter = \(counter)")
186+
}
187+
readEnd.fulfill()
188+
}
189+
wait(for: [readEnd, writeEnd1, writeEnd2], timeout: 10)
190+
assert(d.debugContent.count == objectCount)
191+
print("end")
192+
}
136193
}
137194

138195
// MARK: -

0 commit comments

Comments
 (0)