I'm trying to implement a thread-safe array component in the most efficient and safe way, backed by unit tests.
So far, I would prefer a struct array, to keep a value type and not a reference type.
But when I run the test below, I still have random crashes that I don't explain :
Here's my ThreadSafe array class :
public struct SafeArray<T>: RangeReplaceableCollection {
public typealias Element = T
public typealias Index = Int
public typealias SubSequence = SafeArray<T>
public typealias Indices = Range<Int>
private var array: [T]
private var locker = NSLock()
private func lock() { locker.lock() }
private func unlock() { locker.unlock() }
// MARK: - Public methods
// MARK: - Initializers
public init<S>(_ elements: S) where S: Sequence, SafeArray.Element == S.Element {
array = [S.Element](elements)
}
public init() { self.init([]) }
public init(repeating repeatedValue: SafeArray.Element, count: Int) {
let array = Array(repeating: repeatedValue, count: count)
self.init(array)
}
}
extension SafeArray {
// Single action
public func get() -> [T] {
lock(); defer { unlock() }
return Array(array)
}
public mutating func set(_ array: [T]) {
lock(); defer { unlock() }
self.array = Array(array)
}
}
And here's my XCUnitTest code :
final class ConcurrencyTests: XCTestCase {
private let concurrentQueue1 = DispatchQueue.init(label: "concurrentQueue1",
qos: .background,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: nil)
private let concurrentQueue2 = DispatchQueue.init(label: "concurrentQueue2",
qos: .background,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: nil)
private var safeArray = SafeArray(["test"])
func wait(for expectations: XCTestExpectation, timeout seconds: TimeInterval) {
wait(for: [expectations], timeout: seconds)
}
func waitForMainRunLoop() {
let mainRunLoopExpectation = expectation(description: "mainRunLoopExpectation")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { mainRunLoopExpectation.fulfill() }
wait(for: mainRunLoopExpectation, timeout: 0.5)
}
func waitFor(_ timeout: TimeInterval) {
let mainRunLoopExpectation = expectation(description: "timeoutExpectation")
DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { mainRunLoopExpectation.fulfill() }
wait(for: mainRunLoopExpectation, timeout: timeout + 0.5)
}
override func setUpWithError() throws {
try super.setUpWithError()
safeArray = SafeArray(["test"])
}
func testSafeArrayGet() {
var thread1: Thread!
var thread2: Thread!
concurrentQueue1.async {
thread1 = Thread.current
let startTime = Date()
for i in 0...1_000_000 {
self.safeArray.set(["modification"])
print("modification \(i)")
}
print("time modification: \(Date().timeIntervalSince(startTime))")
}
concurrentQueue2.async {
thread2 = Thread.current
let startTime = Date()
for i in 0...1_000_000 {
let _ = self.safeArray.get()
print("read \(i)")
}
print("time read: \(Date().timeIntervalSince(startTime))")
}
waitFor(10)
XCTAssert(!thread1.isMainThread && !thread2.isMainThread)
XCTAssert(thread1 != thread2)
}
}
Edit: Event with a class and a simple approach to make it thread safe, I get a crash. Here's a very simple test that crashes :
class TestClass {
var test = ["test"]
let nsLock = NSLock()
func safeSet(_ string: String) {
nsLock.lock()
test[0] = string // crash
nsLock.unlock()
}
}
func testStructThreadSafety() {
let testClass = TestClass()
DispatchQueue.concurrentPerform(iterations: 1_000_000) { i in
testClass.safeSet("modification \(i)")
let _ = testClass.test[0]
}
XCTAssert(true)
}
Why is it crashing? What am I doing wrong?
Note that if I make it a class I don't get crashes, but I would prefer to keep it a struct.
Related
I am very new in iOS development unit testing.
I have a view model as below
class PostsViewViewModel {
private let serviceRequest: NetworkRequestProtocol
public private(set) var requestOutput: PassthroughSubject<RequestOutput, Never> = .init()
private var cancellables = Set<AnyCancellable>()
init(
request: NetworkRequestProtocol,
user: LoginUserModel,
codeDataManager: CoreDataManagerProtocol) {
serviceRequest = request
loadPostsFromServerFor(user: user)
}
private func loadPostsFromServerFor(user: LoginUserModel) {
Task {
do {
let postsRecived = try await serviceRequest.callService(
with: ServiceEndPoint.fetchPostsForUser(id: user.userid),
model: [PostModel].self,
serviceMethod: .get
)
if postsRecived.isEmpty {
requestOutput.send(.fetchPostsDidSucceedWithEmptyList)
} else {
recievedRawPostsModel = postsRecived
createPostModelsFromPostRecieved(postsRecived)
requestOutput.send(.fetchPostsDidSucceed)
}
} catch {
requestOutput.send(.fetchPostsDidFail)
}
}
}
}
extension PostsViewViewModel {
enum RequestOutput {
case fetchPostsDidFail
case fetchPostsDidSucceed
case fetchPostsDidSucceedWithEmptyList
case reloadPost
}
}
Now I created a test class of ViewModel as below
final class PostViewViewModelTest: XCTestCase {
let userInput: PassthroughSubject<PostsViewViewModel.UserInput, Never> = .init()
private var cancellable = Set<AnyCancellable>()
private let mockUser = LoginUserModel(userid: 1)
private let coreDataManager = CoreDataStackInMemory()
private var sutPostViewModel: PostsViewViewModel!
override func setUp() {
sutPostViewModel = PostsViewViewModel(
request: MockNetworkRequestPostSuccess(),
user: mockUser, codeDataManager: coreDataManager
)
}
override func tearDown() {
sutPostViewModel = nil
}
func testPostsViewViewModel_WhenPostModelLoaded_NumberOfRowsSouldBeMoreThanZero() {
// let postViewModel = sutPostViewModel!
self.sutPostViewModel.requestOutput
//.receive(on: DispatchQueue.main)
.sink { [weak self] output in
XCTAssertTrue(output == .fetchPostsDidSucceed)
XCTAssertTrue((self?.sutPostViewModel.numberOfRowsInPostTableView)! > 0)
}
.store(in: &cancellable)
}
func testPostsViewViewModel_WhenPostModelLoaded_GetPostAtGivenIndexPathMustHaveEqualPostID() {
// let postViewModel = sutPostViewModel!
self.sutPostViewModel.requestOutput
.receive(on: DispatchQueue.main)
.sink { [weak self] output in
print(output == .reloadPost)
let post = self?.sutPostViewModel.getPost(at: IndexPath(row: 0, section: 0))
let postModel: [PostModel] = JSONLoader.load("Posts.json")
XCTAssertTrue(post.postID == postModel[0].id)
}
.store(in: &cancellable)
}
}
But test cases get crashed while access sutPostViewModel. I am unable to understand what am I doing wrong here.
While debugging I found tearDown() is being called before sink and test crash.
I think you might need to use an expectation.
func testPostsViewViewModel_WhenPostModelLoaded_NumberOfRowsSouldBeMoreThanZero() {
let expectation = expectation(description: "Sink Executed") // 1.
self.sutPostViewModel.requestOutput
.sink { [weak self] output in
XCTAssertTrue(output == .fetchPostsDidSucceed)
XCTAssertTrue((self?.sutPostViewModel.numberOfRowsInPostTableView)! > 0)
expectation.fulfill() //2. Fulfill to stop waiting
}
.store(in: &cancellable)
wait(for: [expectation], timeout: 5) // 3. Wait for 5 seconds before timeout and failure
}
You have asynchronous code so sink is executed after your test method runs. At that point tearDown has been called and your sut set to nil
I have encountered a memory "leak" that I don't understand. I have a caching class whose purpose is to limit the number of instances of heavy Data items. Even when it stores NO instances of those items, they are being retained somehow. If they are from a background thread, I believe they are released upon completion of the task. But, of course, as demonstrated by the code, on the main thread they persist.
This occurs on iPadOS 14.8, iPadOS 15 and MacCatalyst.
What am I missing or don't understand?
class DataCache {
var id: String
var data: Data? {
get {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("\(id).txt")
return try! Data(contentsOf: url)
}
set {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("\(id).txt")
try! newValue!.write(to: url)
}
}
init(id: String, data: Data) {
self.id = id
self.data = data
}
}
class Item {
static var selection = [Item]()
static var items = [String:Item]()
var id: String = UUID().uuidString
var itemData: DataCache
init() {
itemData = DataCache(id: id,
data: Data(String(repeating: "dummy", count: 4_000_000).utf8)
)
}
required init(_ other: Item) {
self.itemData = DataCache(id: self.id, data: other.itemData.data!)
}
func duplicate(times: Int) {
for index in 0..<times {
print(index)
Item.selection.append(Item(self))
}
}
}
#main struct Main {
static func main() throws {
let item = Item()
perform(item)
performOnSelection() { item in
let _ = Item(item)
}
while (true) {}
}
static func perform(_ item: Item) {
item.duplicate(times: 100)
}
static func performOnSelection(perform action: #escaping (Item)->Void) {
var done = false
DispatchQueue.global().async {
for item in Item.selection {
action(item)
}
done = true
}
while !done { sleep (1) }
}
}
You have autorelease objects being created. Insert an autoreleasepool to drain the pool periodically, e.g.:
func performOnSelection(perform action: #escaping (Item) -> Void) {
...
autoreleasepool {
perform(item)
}
performOnSelection() { item in
autoreleasepool {
let _ = Item(item)
}
}
...
}
and
func duplicate(times: Int) {
for index in 0..<times {
print(index)
autoreleasepool { [self] in
Item.selection.append(Item(self))
}
}
}
E.g. without autoreleasepool:
And with:
It turns off to be much faster when it is not dealing with all of those lingering autorelease objects, too. And peak memory usage has gone from 3.4gb to 47mb.
Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 2 years ago.
Improve this question
What is the best way to do Safe Thread?
Using NSLock:
class Observable<T> {
typealias Observer = (_ observable: Observable<T>, T) -> Void
private var observers: [Observer]
private let lock = NSLock()
private var _value: T
var value: T {
get {
lock.lock()
let value = _value
lock.unlock()
return value
}
set {
lock.lock()
_value = newValue
lock.unlock()
}
}
init(_ value: T) {
self._value = value
observers = []
}
func observe(observer: #escaping Observer) {
self.observers.append((observer))
}
private func notifyObservers(_ value: T) {
DispatchQueue.main.async {
self.observers.forEach { [unowned self](observer) in
observer(self, value)
}
}
}
}
Using Queue:
class SecondObservable<T> {
typealias Observer = (_ observable: SecondObservable<T>, T) -> Void
private var observers: [Observer]
private let safeQueue = DispatchQueue(label: "com.observable.value", attributes: .concurrent)
private var _value: T
var value: T {
get {
var value: T!
safeQueue.sync { value = _value }
return value
}
set {
safeQueue.async(flags: .barrier) { self._value = newValue }
}
}
init(_ value: T) {
self._value = value
observers = []
}
func observe(observer: #escaping Observer) {
self.observers.append((observer))
}
private func notifyObservers(_ value: T) {
DispatchQueue.main.async {
self.observers.forEach { [unowned self](observer) in
observer(self, value)
}
}
}
}
Or serial Queue:
class ThirdObservable<T> {
typealias Observer = (_ observable: ThirdObservable<T>, T) -> Void
private var observers: [Observer]
private let safeQueue = DispatchQueue(label: "com.observable.value")
private var _value: T
var value: T {
get {
var value: T!
safeQueue.async { value = self._value }
return value
}
set {
safeQueue.async { self._value = newValue }
}
}
init(_ value: T) {
self._value = value
observers = []
}
func observe(observer: #escaping Observer) {
self.observers.append((observer))
}
private func notifyObservers(_ value: T) {
DispatchQueue.main.async {
self.observers.forEach { [unowned self](observer) in
observer(self, value)
}
}
}
}
NSLock or a Queue with .concurrent attribute for the above case, and why?
Concurrent queue with barrier flag is more efficient than using NSLock in this case.
Both of them block other operations while a setter is running but the difference is when you call multiple getters concurrently or parallelism.
NSLock: Only allow 1 getter running at a time
Concurrent Queue with barrier flag: Allow multiple getters running at a time.
I'm trying to subscribe to a Publisher created from a PassthroughSubject twice and only one of them is executed when PassthroughSubject fires a value.
Here is what I tried:
class Worker {
let stringGeneratorResultSubject: PassthroughSubject<String, Error>
init(stringGeneratorResultSubject: PassthroughSubject<String, Error>) {
self.stringGeneratorResultSubject = stringGeneratorResultSubject
}
func generateString() {
stringGeneratorResultSubject.send("someValue")
}
}
class A {
let workerObj: Worker
let workerObjPublisher: AnyPublisher<String, Swift.Error>
init(workerObj: Worker,
workerObjPublisher: AnyPublisher<String, Swift.Error>) {
self.workerObj = workerObj
self.workerObjPublisher = workerObjPublisher
super.init()
getString()
}
func getString() {
workerObjPublisher.sink { result in
// do something with result for
}.store(in: &cancellable)
workerObj.generateString()
}
}
class B {
let workerObjPublisher: AnyPublisher<String, Swift.Error>
init(workerObjPublisher: AnyPublisher<String, Swift.Error>) {
self.workerObjPublisher = workerObjPublisher
super.init()
loadString()
}
func loadString() {
workerObjPublisher.sink { result in
// do something with result
}.store(in: &cancellable)
}
}
class Parent {
lazy var stringGeneratorResultSubject: PassthroughSubject<String, Swift.Error> = .init()
lazy var workerObj: Worker = .init(stringGeneratorResultSubject: stringGeneratorResultSubject)
lazy var aObj: A = .init(workerObj: workerObj,
workerObjPublisher: stringGeneratorResultSubject.eraseToAnyPublisher())
lazy var bObj: B = .init(workerObjPublisher: stringGeneratorResultSubject.eraseToAnyPublisher())
_ = bObj
aObj.getString()
}
Only class A's subscription block in getString() is called. class B's subscription block inside loadString() is not executed. Am I missing something?
I have a singleton class as so:
class Database {
static let instance:Database = Database()
private var db: Connection?
private init(){
do {
db = try Connection("\(path)/SalesPresenterDatabase.sqlite3")
}catch{print(error)}
}
}
Now I access this class using Database.instance.xxxxxx to perform a function within the class. However when I access the instance from another thread it throws bizarre results as if its trying to create another instance. Should I be referencing the instance in the same thread?
To clarify the bizarre results show database I/o errors because of two instances trying to access the db at once
Update
please see this question for more info on the database code: Using transactions to insert is throwing errors Sqlite.swift
class var shareInstance: ClassName {
get {
struct Static {
static var instance: ClassName? = nil
static var token: dispatch_once_t = 0
}
dispatch_once(&Static.token, {
Static.instance = ClassName()
})
return Static.instance!
}
}
USE: let object:ClassName = ClassName.shareInstance
Swift 3.0
class ClassName {
static let sharedInstance: ClassName = { ClassName()} ()
}
USE: let object:ClassName = ClassName.shareInstance
In Swift 3.0 add a private init to prevent others from using the default () initializer.
class ClassName {
static let sharedInstance = ClassName()
private init() {} //This prevents others from using the default '()' initializer for this class.
}
Singleton thread class.
final public class SettingsThreadSafe {
public static let shared = SettingsThreadSafe()
private let concurrentQueue = DispatchQueue(label: "com.appname.typeOfQueueAndUse", attributes: .concurrent)
private var settings: [String: Any] = ["Theme": "Dark",
"MaxConsurrentDownloads": 4]
private init() {}
public func string(forKey key: String) -> String? {
var result: String?
concurrentQueue.sync {
result = self.settings[key] as? String
}
return result
}
public func int(forKey key: String) -> Int? {
var result: Int?
concurrentQueue.sync {
result = self.settings[key] as? Int
}
return result
}
public func set(value: Any, forKey key: String) {
concurrentQueue.async( flags: .barrier ) {
self.settings[key] = value
}
}
}
Unit to test the singleton class.
func testConcurrentUsage() {
let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)
let expect = expectation(description: "Using SettingsThreadSafe.shared from multiple threads shall succeed")
let callCount = 300
for callIndex in 1...callCount {
concurrentQueue.async {
SettingsThreadSafe.shared.set(value: callIndex, forKey: String(callIndex))
}
}
while SettingsThreadSafe.shared.int(forKey: String(callCount)) != callCount {
// nop
}
expect.fulfill()
waitForExpectations(timeout: 5) { (error) in
XCTAssertNil(error, "Test expectation failed")
}
}