diff --git a/EasyDi.xcodeproj/project.pbxproj b/EasyDi.xcodeproj/project.pbxproj index 3ffc147..dba275a 100644 --- a/EasyDi.xcodeproj/project.pbxproj +++ b/EasyDi.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 8BEE13521F9A27C800A02331 /* EasyDi.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BEE13501F9A27C800A02331 /* EasyDi.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BEE13561F9A27EA00A02331 /* EasyDi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEE13551F9A27EA00A02331 /* EasyDi.swift */; }; 8BEE13571F9A27F200A02331 /* EasyDi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEE13551F9A27EA00A02331 /* EasyDi.swift */; }; + A5ABB84321A5522400C96320 /* Test_Threadsafety.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5ABB84221A5522400C96320 /* Test_Threadsafety.swift */; }; C3614B541F1C8B6800B1F4A1 /* Test_Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3614B451F1C8B5F00B1F4A1 /* Test_Context.swift */; }; C3614B551F1C8B6800B1F4A1 /* Test_CrossAssemblyInjections.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3614B461F1C8B5F00B1F4A1 /* Test_CrossAssemblyInjections.swift */; }; C3614B561F1C8B6800B1F4A1 /* Test_Injections.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3614B471F1C8B5F00B1F4A1 /* Test_Injections.swift */; }; @@ -54,6 +55,7 @@ 8BEE13501F9A27C800A02331 /* EasyDi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EasyDi.h; sourceTree = ""; }; 8BEE13511F9A27C800A02331 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8BEE13551F9A27EA00A02331 /* EasyDi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EasyDi.swift; sourceTree = ""; }; + A5ABB84221A5522400C96320 /* Test_Threadsafety.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test_Threadsafety.swift; sourceTree = ""; }; C3614B3B1F1C8AE900B1F4A1 /* EasyDi-iOS-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "EasyDi-iOS-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; C3614B441F1C8B5F00B1F4A1 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Tests/Info.plist; sourceTree = SOURCE_ROOT; }; C3614B451F1C8B5F00B1F4A1 /* Test_Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Test_Context.swift; path = Tests/Test_Context.swift; sourceTree = SOURCE_ROOT; }; @@ -137,6 +139,7 @@ D2B16C962123116500CF69E8 /* Test_ImplicitlyUnwrappedOptional.swift */, C3614B4A1F1C8B5F00B1F4A1 /* Test_Scope.swift */, C3614B4B1F1C8B5F00B1F4A1 /* Test_StructsInjection.swift */, + A5ABB84221A5522400C96320 /* Test_Threadsafety.swift */, ); path = Tests; sourceTree = ""; @@ -366,6 +369,7 @@ C3614B541F1C8B6800B1F4A1 /* Test_Context.swift in Sources */, C3614B561F1C8B6800B1F4A1 /* Test_Injections.swift in Sources */, C3614B5A1F1C8B6800B1F4A1 /* Test_StructsInjection.swift in Sources */, + A5ABB84321A5522400C96320 /* Test_Threadsafety.swift in Sources */, E6DCF5C61F2F62A600D9F8BC /* Test_CrossAssemblyInjections_SingletonCycle.swift in Sources */, C3614B581F1C8B6800B1F4A1 /* Test_ProtocolBasedInjection.swift in Sources */, ); diff --git a/Sources/EasyDi.swift b/Sources/EasyDi.swift index d60fef6..78c4be8 100644 --- a/Sources/EasyDi.swift +++ b/Sources/EasyDi.swift @@ -7,6 +7,19 @@ import Foundation public typealias InjectableObject = Any +public protocol DIContextLocker { + func lock() + func unlock() +} + +public struct DIContextEmptyLocker { + func lock() {} + func unlock() {} +} + +extension NSRecursiveLock: DIContextLocker { +} + /// This class is used to join assembly instances into separated shared group. /// /// All assemblies with one context shares object graph stack. @@ -23,27 +36,27 @@ public typealias InjectableObject = Any /// ``` /// public final class DIContext { - - fileprivate lazy var syncQueue:DispatchQueue = Dispatch.DispatchQueue(label: "EasyDi Context Sync Queue", qos: .userInteractive) - + public static var defaultInstance = DIContext() - fileprivate var assemblies:[String:Assembly] = [:] + fileprivate var assemblies: [String: Assembly] = [:] var objectGraphStorage: [String: InjectableObject] = [:] - - var objectGraphStackDepth:Int = 0 + var objectGraphStackDepth: Int = 0 + let locker: DIContextLocker /// All lazy singletons are stored here. /// /// Dictionary key is **key** parameter from **define** method - var singletons:[String: InjectableObject] = [:] + var singletons: [String: InjectableObject] = [:] /// Array of applyed substitutions /// /// Dictionary key is **key** parameter from **define** method - internal var substitutions:[String: UntypedPatchClosure] = [:] + var substitutions: [String: UntypedPatchClosure] = [:] - public init() {} + public init(locker: DIContextLocker = NSRecursiveLock()) { + self.locker = locker + } /// This method creates assembly instance based on it's return type. /// @@ -53,7 +66,6 @@ public final class DIContext { /// lazy var anotherAssembly: AnotherAssemblyClass = self.context.assembly() /// ``` public func assembly() -> AssemblyType { - let instance = self.instance(for: AssemblyType.self) return castAssemblyInstance(instance, asType: AssemblyType.self) } @@ -68,21 +80,18 @@ public final class DIContext { /// var assembly = context.instance(for AssemblyClass.self) /// ``` public func instance(for assemblyType: Assembly.Type) -> Assembly { - - var instance: Assembly? = nil - syncQueue.sync { - let assemblyClassName:String = String(reflecting: assemblyType) - if let existingInstance = self.assemblies[assemblyClassName] { - instance = existingInstance - } - else { - let newInstance = assemblyType.newInstance() - newInstance.context = self - self.assemblies[assemblyClassName] = newInstance - instance = newInstance - } + locker.lock(); defer { locker.unlock() } + + let assemblyClassName = String(reflecting: assemblyType) + if let existingInstance = self.assemblies[assemblyClassName] { + return existingInstance + } + else { + let newInstance = assemblyType.newInstance() + newInstance.context = self + self.assemblies[assemblyClassName] = newInstance + return newInstance } - return instance! } } @@ -146,14 +155,14 @@ public enum Scope { /// ``` open class Assembly: AssemblyInternal { - public internal(set) var context: DIContext! + public internal(set) weak var context: DIContext! /// This method creates assembly for specified context or default context if no parameters provided /// /// - parameter context: DIContext object which assembly should belong to /// /// - returns: Assembly instance - public static func instance(from context: DIContext = DIContext.defaultInstance)->Self { + public static func instance(from context: DIContext = DIContext.defaultInstance) -> Self { let instance = context.instance(for: self) return castAssemblyInstance(instance, asType: self) @@ -178,9 +187,9 @@ open class Assembly: AssemblyInternal { public func addSubstitution( for simpleDefinitionKey: String, with substitutionClosure: @escaping SubstitutionClosure) { - - let definitionKey: String = String(reflecting: self).replacingOccurrences(of: ".", with: "")+simpleDefinitionKey - self.context.substitutions[definitionKey] = substitutionClosure + + let definitionKey = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleDefinitionKey + context.substitutions[definitionKey] = substitutionClosure } /// This method removes substitution from assembly @@ -188,8 +197,9 @@ open class Assembly: AssemblyInternal { /// - parameter definitionKey: should exactly match method or property name of substituting dependency /// public func removeSubstitution(for simpleDefinitionKey: String) { - let definitionKey: String = String(reflecting: self).replacingOccurrences(of: ".", with: "")+simpleDefinitionKey - self.context.substitutions[definitionKey] = nil + + let definitionKey = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleDefinitionKey + context.substitutions[definitionKey] = nil } /// The method defines return-only placeholder for object. @@ -221,7 +231,7 @@ open class Assembly: AssemblyInternal { public func definePlaceholder(key: String = #function) -> ObjectType { let closure: DefinitionClosure? = nil - return self.define(key: key, definitionClosure: closure) + return define(key: key, definitionClosure: closure) } /// This method defines injection into existing object without return @@ -313,7 +323,7 @@ open class Assembly: AssemblyInternal { init initClosure: @autoclosure @escaping () -> ObjectType, inject injectClosure: ObjectInjectClosure? = nil ) -> ResultType { - return self.define(key: key, definitionKey: definitionKey, scope: scope) { (definition:Definition) in + return define(key: key, definitionKey: definitionKey, scope: scope) { (definition:Definition) in definition.initClosure = initClosure definition.injectClosure = injectClosure } @@ -338,19 +348,21 @@ open class Assembly: AssemblyInternal { definitionKey simpleDefinitionKey: String = #function, scope: Scope = .objectGraph, definitionClosure: DefinitionClosure? = nil) -> ResultType { - - // Objects are stored in context by key made of Assembly class name and name of var or method - let key: String = String(reflecting: self).replacingOccurrences(of: ".", with: "")+simpleKey - let definitionKey: String = String(reflecting: self).replacingOccurrences(of: ".", with: "")+simpleDefinitionKey - - var result:ObjectType guard let context = self.context else { - fatalError("Assembly has no context to work in") + fatalError("Associated context doesn't exists anymore") } + context.locker.lock(); defer { context.locker.unlock() } + + // Objects are stored in context by key made of Assembly class name and name of var or method + let key: String = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleKey + let definitionKey: String = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleDefinitionKey + + var result: ObjectType + // First of all it checks if there's substitution for this var or method - if let substitutionClosure = self.context.substitutions[definitionKey] { + if let substitutionClosure = context.substitutions[definitionKey] { let substitutionObject = substitutionClosure() guard let object = substitutionObject as? ResultType else { @@ -358,19 +370,16 @@ open class Assembly: AssemblyInternal { } return object - // Next check for existing singletons - } else if scope == .lazySingleton, let singleton = self.context.singletons[key] { - + } else if scope == .lazySingleton, let singleton = context.singletons[key] { + result = singleton as! ObjectType // And trying to return object from graph } else if let objectFromStack = context.objectGraphStorage[key], scope != .prototype, let unwrappedObject = objectFromStack as? ObjectType { - result = unwrappedObject - } else { // Create Definition object to store injections and dependencies information @@ -400,9 +409,8 @@ open class Assembly: AssemblyInternal { } // And save singletons - if self.context.singletons[key] == nil, scope == .lazySingleton { - - self.context.singletons[key] = result + if context.singletons[key] == nil, scope == .lazySingleton { + context.singletons[key] = result } guard let finalResult = result as? ResultType else { diff --git a/Tests/Test_Threadsafety.swift b/Tests/Test_Threadsafety.swift new file mode 100644 index 0000000..7f204c4 --- /dev/null +++ b/Tests/Test_Threadsafety.swift @@ -0,0 +1,65 @@ +// +// Test_Threadsafety.swift +// EasyDi-iOS-Tests +// +// Created by Sergey V. Krupov on 21.11.2018. +// Copyright © 2018 AndreyZarembo. All rights reserved. +// + +import XCTest +import EasyDi + +fileprivate protocol SomeProtocol { +} + +fileprivate class SomeObject: SomeProtocol { + var values = Array(repeating: "", count: 4000) +} + +fileprivate class TestAssembly: Assembly { + + var someObject: SomeProtocol { + return define(init: SomeObject()) { + for i in 0 ..< $0.values.count { + $0.values[i] = self.getSomeValue(at: i) + } + return $0 + } + } + + // Сделано для того, чтобы стабильно воспроизводить падение. Вряд ли в реальном приложении будет такой код. + private func getSomeValue(at index: Int) -> String { + return define(key: "getSomeValue_\(index)", init: "value-\(index)") + } +} + +final class Test_Threadsafety: XCTestCase { + + func test_ThreadSafety() { + + let context = DIContext() + let assembly = TestAssembly.instance(from: context) + + // Явно создаю 2 потока, т.к. не известно на скольких потоках будет работать concurrent dispatch queue + + let queue1 = DispatchQueue(label: "Queue1") + let expectation1 = expectation(description: "Queue-1") + queue1.async { + for _ in 1 ..< 10 { + _ = assembly.someObject + } + expectation1.fulfill() + } + + let queue2 = DispatchQueue(label: "Queue2") + let expectation2 = expectation(description: "Queue-2") + queue2.async { + for _ in 1 ..< 10 { + _ = assembly.someObject + } + expectation2.fulfill() + } + + wait(for: [expectation1, expectation2], timeout: 10) + } +}