├── LICENSE.md ├── README.md ├── RunloopQueue.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── RunloopQueue.xcscheme ├── RunloopQueue ├── RunloopQueue+Streams.swift ├── RunloopQueue+URLConnection.swift └── RunloopQueue.swift └── RunloopQueueTests ├── Info.plist └── RunloopQueueTests.swift /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Cascable AB 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RunloopQueue 2 | 3 | `RunloopQueue` is a class for running code on a background thread. It works a *bit* like Grand Central Dispatch, but is instead powered by the magic of run loops! 4 | 5 | `RunloopQueue` works on both macOS and iOS. 6 | 7 | ```swift 8 | queue = RunloopQueue(named: "My Cool Queue") 9 | 10 | queue.async { 11 | // This code is running on a background thread. 12 | performAReallyLongOperation() 13 | } 14 | ``` 15 | 16 | ### Usage 17 | 18 | To use `RunloopQueue`, copy the `RunloopQueue.swift` file to your project. You can also add `RunloopQueue+Streams.swift` and `RunloopQueue+URLConnection.swift` if you'd like the `Stream` and `URLConnection` helpers. 19 | 20 | ### Real-World Applications 21 | 22 | `RunloopQueue` is currently being used in our Cascable family of apps, most notably in `CascableCore`, our SDK for working with WiFi-enabled cameras. `CascableCore` uses `RunloopQueue` for managing connections with the cameras it supports, and scheduling messages back and forth. You can find out more about `CascableCore` in our [CascableCore Demo App](https://github.com/Cascable/cascablecore-demo). 23 | 24 | ### RunloopQueue vs. Grand Central Dispatch 25 | 26 | `RunloopQueue` is *not* designed to be a replacement for Grand Central Dispatch. In fact, if you're performing the common flow of starting on the main thread, performing some background work then calling back to the main thread at the end of the operation to update UI, you'll need Grand Central Dispatch to get back to the main thread. 27 | 28 | ```swift 29 | queue.async { 30 | // This code is running on a background thread. 31 | performAReallyLongOperation() 32 | DispatchQueue.main.async { 33 | // This code is running on the main thread. 34 | performUIUpdates() 35 | } 36 | } 37 | ``` 38 | 39 | However, `RunloopQueue` does offer some advantages over Grand Central Dispatch in certain circumstances: 40 | 41 | #### Thread Consistency 42 | 43 | `RunloopQueue` guarantees that all operations on a given `RunloopQueue` instance will be executed on the same thread, whether they're performed synchronously or asynchronously. This can be handy if you're interacting with a library that expects thread consistency. 44 | 45 | #### Support For "Where Am I?" Checking 46 | 47 | `RunloopQueue` provides the `isRunningOnQueue()` method for checking whether or not the code calling that method is running on the given `RunloopQueue` instance's thread. 48 | 49 | #### Safe(er) Synchronous Operations 50 | 51 | Grand Central Dispatch is *very* unsafe when it comes to synchronous operations, and it's very easy to deadlock your code. Personally, it's so unsafe that I ban its use in any project that I have that sort of influence over. 52 | 53 | `RunloopQueue`, however, is much better in this regard. Thanks to the `isRunningOnQueue()` method, it can avoid deadlocks when synchronous operations start other synchronous operations: 54 | 55 | ```swift 56 | queue.sync { 57 | queue.sync { 58 | print("Inner sync") 59 | } 60 | print("Outer sync") 61 | } 62 | ``` 63 | 64 | Please note that as in *any* multithreaded environment, synchronous operations are prone to deadlocking if you're not careful. 65 | 66 | #### Friendly to Streams and URL Connections 67 | 68 | If you're working with streams or `URLConnection` objects, `RunloopQueue` provides convenience methods for enqueuing such objects on its background thread. This is great if you'd like to process your incoming data in the background: 69 | 70 | ```swift 71 | let input: InputStream = … 72 | let output: OutputStream = … 73 | 74 | input.delegate = self 75 | output.delegate = self 76 | 77 | // Scheduling the streams on a RunloopQueue will cause their 78 | // delegate methods to be called on a background thread. 79 | queue.schedule(input) 80 | queue.schedule(output) 81 | 82 | input.open() 83 | output.open() 84 | ``` 85 | 86 | ### Objective-C 87 | 88 | `RunloopQueue` is written in Swift 3, but is fully compatible with Objective-C. 89 | 90 | ```objc 91 | self.runloopQueue = [[CBLRunloopQueue alloc] initWithName:@"My Cool Queue"]; 92 | [self.runloopQueue async:^{ 93 | [self performAReallyLongOperation]; 94 | }]; 95 | 96 | [self.runloopQueue scheduleStream:self.inputStream]; 97 | ``` 98 | 99 | ### License 100 | 101 | For more information, see the [LICENSE.md](LICENSE.md) file. 102 | 103 | -------------------------------------------------------------------------------- /RunloopQueue.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 500724A21F3B502B00011AAF /* RunloopQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E336DA1E5331C100D5B55E /* RunloopQueue.swift */; }; 11 | 500724A31F3B502E00011AAF /* RunloopQueue+URLConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50485EBB1F1A4EE900E2F8E1 /* RunloopQueue+URLConnection.swift */; }; 12 | 500724A41F3B503000011AAF /* RunloopQueue+Streams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E336EA1E535D8200D5B55E /* RunloopQueue+Streams.swift */; }; 13 | 50E336E31E5352E800D5B55E /* RunloopQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E336E21E5352E800D5B55E /* RunloopQueueTests.swift */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 500724A61F3B576000011AAF /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 18 | 500724A71F3B576000011AAF /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 19 | 50485EBB1F1A4EE900E2F8E1 /* RunloopQueue+URLConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RunloopQueue+URLConnection.swift"; sourceTree = ""; }; 20 | 50E336DA1E5331C100D5B55E /* RunloopQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RunloopQueue.swift; sourceTree = ""; }; 21 | 50E336E01E5352E800D5B55E /* RunloopQueueTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunloopQueueTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 50E336E21E5352E800D5B55E /* RunloopQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunloopQueueTests.swift; sourceTree = ""; }; 23 | 50E336E41E5352E800D5B55E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | 50E336EA1E535D8200D5B55E /* RunloopQueue+Streams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RunloopQueue+Streams.swift"; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | 50E336DD1E5352E800D5B55E /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | ); 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | /* End PBXFrameworksBuildPhase section */ 36 | 37 | /* Begin PBXGroup section */ 38 | 50E336C11E5331AF00D5B55E = { 39 | isa = PBXGroup; 40 | children = ( 41 | 500724A71F3B576000011AAF /* README.md */, 42 | 500724A61F3B576000011AAF /* LICENSE.md */, 43 | 50E336CC1E5331AF00D5B55E /* RunloopQueue */, 44 | 50E336E11E5352E800D5B55E /* RunloopQueueTests */, 45 | 50E336CB1E5331AF00D5B55E /* Products */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 50E336CB1E5331AF00D5B55E /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 50E336E01E5352E800D5B55E /* RunloopQueueTests.xctest */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 50E336CC1E5331AF00D5B55E /* RunloopQueue */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 50E336DA1E5331C100D5B55E /* RunloopQueue.swift */, 61 | 50E336EA1E535D8200D5B55E /* RunloopQueue+Streams.swift */, 62 | 50485EBB1F1A4EE900E2F8E1 /* RunloopQueue+URLConnection.swift */, 63 | ); 64 | path = RunloopQueue; 65 | sourceTree = ""; 66 | }; 67 | 50E336E11E5352E800D5B55E /* RunloopQueueTests */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 50E336E21E5352E800D5B55E /* RunloopQueueTests.swift */, 71 | 50E336E41E5352E800D5B55E /* Info.plist */, 72 | ); 73 | path = RunloopQueueTests; 74 | sourceTree = ""; 75 | }; 76 | /* End PBXGroup section */ 77 | 78 | /* Begin PBXNativeTarget section */ 79 | 50E336DF1E5352E800D5B55E /* RunloopQueueTests */ = { 80 | isa = PBXNativeTarget; 81 | buildConfigurationList = 50E336E71E5352E800D5B55E /* Build configuration list for PBXNativeTarget "RunloopQueueTests" */; 82 | buildPhases = ( 83 | 50E336DC1E5352E800D5B55E /* Sources */, 84 | 50E336DD1E5352E800D5B55E /* Frameworks */, 85 | 50E336DE1E5352E800D5B55E /* Resources */, 86 | ); 87 | buildRules = ( 88 | ); 89 | dependencies = ( 90 | ); 91 | name = RunloopQueueTests; 92 | productName = RunloopQueueTests; 93 | productReference = 50E336E01E5352E800D5B55E /* RunloopQueueTests.xctest */; 94 | productType = "com.apple.product-type.bundle.unit-test"; 95 | }; 96 | /* End PBXNativeTarget section */ 97 | 98 | /* Begin PBXProject section */ 99 | 50E336C21E5331AF00D5B55E /* Project object */ = { 100 | isa = PBXProject; 101 | attributes = { 102 | LastSwiftUpdateCheck = 0820; 103 | LastUpgradeCheck = 0820; 104 | ORGANIZATIONNAME = "Cascable AB"; 105 | TargetAttributes = { 106 | 50E336DF1E5352E800D5B55E = { 107 | CreatedOnToolsVersion = 8.2.1; 108 | ProvisioningStyle = Automatic; 109 | }; 110 | }; 111 | }; 112 | buildConfigurationList = 50E336C51E5331AF00D5B55E /* Build configuration list for PBXProject "RunloopQueue" */; 113 | compatibilityVersion = "Xcode 3.2"; 114 | developmentRegion = English; 115 | hasScannedForEncodings = 0; 116 | knownRegions = ( 117 | English, 118 | en, 119 | Base, 120 | ); 121 | mainGroup = 50E336C11E5331AF00D5B55E; 122 | productRefGroup = 50E336CB1E5331AF00D5B55E /* Products */; 123 | projectDirPath = ""; 124 | projectRoot = ""; 125 | targets = ( 126 | 50E336DF1E5352E800D5B55E /* RunloopQueueTests */, 127 | ); 128 | }; 129 | /* End PBXProject section */ 130 | 131 | /* Begin PBXResourcesBuildPhase section */ 132 | 50E336DE1E5352E800D5B55E /* Resources */ = { 133 | isa = PBXResourcesBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | /* End PBXResourcesBuildPhase section */ 140 | 141 | /* Begin PBXSourcesBuildPhase section */ 142 | 50E336DC1E5352E800D5B55E /* Sources */ = { 143 | isa = PBXSourcesBuildPhase; 144 | buildActionMask = 2147483647; 145 | files = ( 146 | 500724A31F3B502E00011AAF /* RunloopQueue+URLConnection.swift in Sources */, 147 | 50E336E31E5352E800D5B55E /* RunloopQueueTests.swift in Sources */, 148 | 500724A21F3B502B00011AAF /* RunloopQueue.swift in Sources */, 149 | 500724A41F3B503000011AAF /* RunloopQueue+Streams.swift in Sources */, 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXSourcesBuildPhase section */ 154 | 155 | /* Begin XCBuildConfiguration section */ 156 | 50E336D51E5331AF00D5B55E /* Debug */ = { 157 | isa = XCBuildConfiguration; 158 | buildSettings = { 159 | ALWAYS_SEARCH_USER_PATHS = NO; 160 | CLANG_ANALYZER_NONNULL = YES; 161 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 162 | CLANG_CXX_LIBRARY = "libc++"; 163 | CLANG_ENABLE_MODULES = YES; 164 | CLANG_ENABLE_OBJC_ARC = YES; 165 | CLANG_WARN_BOOL_CONVERSION = YES; 166 | CLANG_WARN_CONSTANT_CONVERSION = YES; 167 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 168 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 169 | CLANG_WARN_EMPTY_BODY = YES; 170 | CLANG_WARN_ENUM_CONVERSION = YES; 171 | CLANG_WARN_INFINITE_RECURSION = YES; 172 | CLANG_WARN_INT_CONVERSION = YES; 173 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 174 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 175 | CLANG_WARN_UNREACHABLE_CODE = YES; 176 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 177 | CODE_SIGN_IDENTITY = "-"; 178 | COPY_PHASE_STRIP = NO; 179 | DEBUG_INFORMATION_FORMAT = dwarf; 180 | ENABLE_STRICT_OBJC_MSGSEND = YES; 181 | ENABLE_TESTABILITY = YES; 182 | GCC_C_LANGUAGE_STANDARD = gnu99; 183 | GCC_DYNAMIC_NO_PIC = NO; 184 | GCC_NO_COMMON_BLOCKS = YES; 185 | GCC_OPTIMIZATION_LEVEL = 0; 186 | GCC_PREPROCESSOR_DEFINITIONS = ( 187 | "DEBUG=1", 188 | "$(inherited)", 189 | ); 190 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 191 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 192 | GCC_WARN_UNDECLARED_SELECTOR = YES; 193 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 194 | GCC_WARN_UNUSED_FUNCTION = YES; 195 | GCC_WARN_UNUSED_VARIABLE = YES; 196 | MACOSX_DEPLOYMENT_TARGET = 10.12; 197 | MTL_ENABLE_DEBUG_INFO = YES; 198 | ONLY_ACTIVE_ARCH = YES; 199 | SDKROOT = macosx; 200 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 201 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 202 | }; 203 | name = Debug; 204 | }; 205 | 50E336D61E5331AF00D5B55E /* Release */ = { 206 | isa = XCBuildConfiguration; 207 | buildSettings = { 208 | ALWAYS_SEARCH_USER_PATHS = NO; 209 | CLANG_ANALYZER_NONNULL = YES; 210 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 211 | CLANG_CXX_LIBRARY = "libc++"; 212 | CLANG_ENABLE_MODULES = YES; 213 | CLANG_ENABLE_OBJC_ARC = YES; 214 | CLANG_WARN_BOOL_CONVERSION = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 217 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 218 | CLANG_WARN_EMPTY_BODY = YES; 219 | CLANG_WARN_ENUM_CONVERSION = YES; 220 | CLANG_WARN_INFINITE_RECURSION = YES; 221 | CLANG_WARN_INT_CONVERSION = YES; 222 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 223 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 224 | CLANG_WARN_UNREACHABLE_CODE = YES; 225 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 226 | CODE_SIGN_IDENTITY = "-"; 227 | COPY_PHASE_STRIP = NO; 228 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 229 | ENABLE_NS_ASSERTIONS = NO; 230 | ENABLE_STRICT_OBJC_MSGSEND = YES; 231 | GCC_C_LANGUAGE_STANDARD = gnu99; 232 | GCC_NO_COMMON_BLOCKS = YES; 233 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 234 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 235 | GCC_WARN_UNDECLARED_SELECTOR = YES; 236 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 237 | GCC_WARN_UNUSED_FUNCTION = YES; 238 | GCC_WARN_UNUSED_VARIABLE = YES; 239 | MACOSX_DEPLOYMENT_TARGET = 10.12; 240 | MTL_ENABLE_DEBUG_INFO = NO; 241 | SDKROOT = macosx; 242 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 243 | }; 244 | name = Release; 245 | }; 246 | 50E336E81E5352E800D5B55E /* Debug */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | COMBINE_HIDPI_IMAGES = YES; 250 | INFOPLIST_FILE = RunloopQueueTests/Info.plist; 251 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 252 | PRODUCT_BUNDLE_IDENTIFIER = se.cascable.RunloopQueueTests; 253 | PRODUCT_NAME = "$(TARGET_NAME)"; 254 | SWIFT_VERSION = 5.0; 255 | }; 256 | name = Debug; 257 | }; 258 | 50E336E91E5352E800D5B55E /* Release */ = { 259 | isa = XCBuildConfiguration; 260 | buildSettings = { 261 | COMBINE_HIDPI_IMAGES = YES; 262 | INFOPLIST_FILE = RunloopQueueTests/Info.plist; 263 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 264 | PRODUCT_BUNDLE_IDENTIFIER = se.cascable.RunloopQueueTests; 265 | PRODUCT_NAME = "$(TARGET_NAME)"; 266 | SWIFT_VERSION = 5.0; 267 | }; 268 | name = Release; 269 | }; 270 | /* End XCBuildConfiguration section */ 271 | 272 | /* Begin XCConfigurationList section */ 273 | 50E336C51E5331AF00D5B55E /* Build configuration list for PBXProject "RunloopQueue" */ = { 274 | isa = XCConfigurationList; 275 | buildConfigurations = ( 276 | 50E336D51E5331AF00D5B55E /* Debug */, 277 | 50E336D61E5331AF00D5B55E /* Release */, 278 | ); 279 | defaultConfigurationIsVisible = 0; 280 | defaultConfigurationName = Release; 281 | }; 282 | 50E336E71E5352E800D5B55E /* Build configuration list for PBXNativeTarget "RunloopQueueTests" */ = { 283 | isa = XCConfigurationList; 284 | buildConfigurations = ( 285 | 50E336E81E5352E800D5B55E /* Debug */, 286 | 50E336E91E5352E800D5B55E /* Release */, 287 | ); 288 | defaultConfigurationIsVisible = 0; 289 | defaultConfigurationName = Release; 290 | }; 291 | /* End XCConfigurationList section */ 292 | }; 293 | rootObject = 50E336C21E5331AF00D5B55E /* Project object */; 294 | } 295 | -------------------------------------------------------------------------------- /RunloopQueue.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RunloopQueue.xcodeproj/xcshareddata/xcschemes/RunloopQueue.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /RunloopQueue/RunloopQueue+Streams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunloopQueue+Streams.swift 3 | // RunloopQueue 4 | // 5 | // Created by Daniel Kennett on 2017-02-14. 6 | // For license information, see LICENSE.md. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension RunloopQueue { 12 | 13 | /// Schedules the given stream into the queue. 14 | /// 15 | /// - Parameter stream: The stream to schedule. 16 | @objc(scheduleStream:) 17 | func schedule(_ stream: Stream) { 18 | if let input = stream as? InputStream { 19 | sync { CFReadStreamScheduleWithRunLoop(input as CFReadStream, CFRunLoopGetCurrent(), .commonModes) } 20 | } else if let output = stream as? OutputStream { 21 | sync { CFWriteStreamScheduleWithRunLoop(output as CFWriteStream, CFRunLoopGetCurrent(), .commonModes) } 22 | } 23 | } 24 | 25 | /// Removes the given stream from the queue. 26 | /// 27 | /// - Parameter stream: The stream to remove. 28 | @objc(unscheduleStream:) 29 | func unschedule(_ stream: Stream) { 30 | if let input = stream as? InputStream { 31 | sync { CFReadStreamUnscheduleFromRunLoop(input as CFReadStream, CFRunLoopGetCurrent(), .commonModes) } 32 | } else if let output = stream as? OutputStream { 33 | sync { CFWriteStreamUnscheduleFromRunLoop(output as CFWriteStream, CFRunLoopGetCurrent(), .commonModes) } 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /RunloopQueue/RunloopQueue+URLConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunloopQueue+Streams.swift 3 | // RunloopQueue 4 | // 5 | // Created by Daniel Kennett on 2017-02-14. 6 | // For license information, see LICENSE.md. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension RunloopQueue { 12 | 13 | /// Schedules the given NSURLConnection into the queue. 14 | /// 15 | /// - Parameter connection: The connection to schedule. 16 | @objc(scheduleConnection:) 17 | func schedule(_ connection: NSURLConnection) { 18 | sync { 19 | connection.schedule(in: RunLoop.current, forMode: RunLoop.Mode.common) 20 | } 21 | } 22 | 23 | /// Removes the given NSURLConnection from the queue. 24 | /// 25 | /// - Parameter connection: The connection to remove. 26 | @objc(unscheduleConnection:) 27 | func unschedule(_ connection: NSURLConnection) { 28 | sync { 29 | connection.unschedule(from: RunLoop.current, forMode: RunLoop.Mode.common) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /RunloopQueue/RunloopQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunloopQueue.swift 3 | // RunloopQueue 4 | // 5 | // Created by Daniel Kennett on 2017-02-14. 6 | // For license information, see LICENSE.md. 7 | // 8 | 9 | import Foundation 10 | 11 | /// RunloopQueue is a serial queue based on CFRunLoop, running on the background thread. 12 | @objc(CBLRunloopQueue) 13 | public class RunloopQueue: NSObject { 14 | 15 | //MARK: - Code That Runs On The Main/Creating Thread 16 | private let thread: RunloopQueueThread 17 | 18 | /// Init a new queue with the given name. 19 | /// 20 | /// - Parameter name: The name of the queue. 21 | @objc(initWithName:) public init(named name: String?) { 22 | thread = RunloopQueueThread() 23 | thread.name = name 24 | super.init() 25 | startRunloop() 26 | } 27 | 28 | deinit { 29 | let runloop = self.runloop 30 | sync { CFRunLoopStop(runloop) } 31 | } 32 | 33 | /// Returns `true` if the queue is running, otherwise `false`. Once stopped, a queue cannot be restarted. 34 | @objc public var running: Bool { 35 | get { return true } 36 | } 37 | 38 | /// Execute a block of code in an asynchronous manner. Will return immediately. 39 | /// 40 | /// - Parameter block: The block of code to execute. 41 | @objc public func async(_ block: @escaping (() -> (Void))) { 42 | CFRunLoopPerformBlock(runloop, CFRunLoopMode.defaultMode.rawValue, block) 43 | thread.awake() 44 | } 45 | 46 | /// Execute a block of code in a synchronous manner. Will return when the code has executed. 47 | /// 48 | /// It's important to be careful with `sync()` to avoid deadlocks. In particular, calling `sync()` from inside 49 | /// a block previously passed to `sync()` will deadlock if the second call is made from a different thread. 50 | /// 51 | /// - Parameter block: The block of code to execute. 52 | @objc public func sync(_ block: @escaping (() -> (Void))) { 53 | 54 | if isRunningOnQueue() { 55 | block() 56 | return 57 | } 58 | 59 | let conditionLock = NSConditionLock(condition: 0) 60 | 61 | CFRunLoopPerformBlock(runloop, CFRunLoopMode.defaultMode.rawValue) { 62 | conditionLock.lock() 63 | block() 64 | conditionLock.unlock(withCondition: 1) 65 | } 66 | 67 | thread.awake() 68 | conditionLock.lock(whenCondition: 1) 69 | conditionLock.unlock() 70 | } 71 | 72 | /// Query if the caller is running on this queue. 73 | /// 74 | /// - Returns: `true` if the caller is running on this queue, otherwise `false`. 75 | @objc public func isRunningOnQueue() -> Bool { 76 | return CFEqual(CFRunLoopGetCurrent(), runloop) 77 | } 78 | 79 | //MARK: - Code That Runs On The Background Thread 80 | 81 | private var runloop: CFRunLoop! = nil 82 | private func startRunloop() { 83 | 84 | let conditionLock = NSConditionLock(condition: 0) 85 | 86 | thread.start() { 87 | [weak self] runloop in 88 | // This is on the background thread. 89 | 90 | conditionLock.lock() 91 | defer { conditionLock.unlock(withCondition: 1) } 92 | 93 | guard let self = self else { return } 94 | self.runloop = runloop 95 | } 96 | 97 | conditionLock.lock(whenCondition: 1) 98 | conditionLock.unlock() 99 | } 100 | } 101 | 102 | private class RunloopQueueThread: Thread { 103 | 104 | // Required to keep the runloop running when nothing is going on. 105 | private let runloopSource: CFRunLoopSource 106 | private var currentRunloop: CFRunLoop? 107 | 108 | override init() { 109 | var sourceContext = CFRunLoopSourceContext() 110 | runloopSource = CFRunLoopSourceCreate(nil, 0, &sourceContext) 111 | } 112 | 113 | /// The callback to be called once the runloop has started executing. Will be called on the runloop's own thread. 114 | var whenReadyCallback: ((CFRunLoop) -> (Void))? = nil 115 | 116 | func start(whenReady call: @escaping ((CFRunLoop) -> (Void))) { 117 | whenReadyCallback = call 118 | start() 119 | } 120 | 121 | func awake() { 122 | guard let runloop = currentRunloop else { return } 123 | if CFRunLoopIsWaiting(runloop) { 124 | CFRunLoopSourceSignal(runloopSource) 125 | CFRunLoopWakeUp(runloop) 126 | } 127 | } 128 | 129 | override func main() { 130 | 131 | let strongSelf = self 132 | let runloop = CFRunLoopGetCurrent()! 133 | currentRunloop = runloop 134 | 135 | CFRunLoopAddSource(runloop, runloopSource, CFRunLoopMode.commonModes) 136 | 137 | let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.entry.rawValue, false, 0) { 138 | observer, activity in 139 | strongSelf.whenReadyCallback?(runloop) 140 | } 141 | 142 | CFRunLoopAddObserver(runloop, observer, CFRunLoopMode.commonModes) 143 | CFRunLoopRun() 144 | CFRunLoopRemoveObserver(runloop, observer, CFRunLoopMode.commonModes) 145 | CFRunLoopRemoveSource(runloop, runloopSource, CFRunLoopMode.commonModes) 146 | 147 | currentRunloop = nil 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /RunloopQueueTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /RunloopQueueTests/RunloopQueueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunloopQueueTests.swift 3 | // RunloopQueueTests 4 | // 5 | // Created by Daniel Kennett on 2017-02-14. 6 | // For license information, see LICENSE.md. 7 | // 8 | 9 | import XCTest 10 | 11 | class RunloopQueueTests: XCTestCase { 12 | 13 | var queue: RunloopQueue! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | queue = RunloopQueue(named: "Test") 18 | } 19 | 20 | override func tearDown() { 21 | queue = nil 22 | super.tearDown() 23 | } 24 | 25 | func testSync() { 26 | var workCompleted = false 27 | 28 | XCTAssertFalse(queue.isRunningOnQueue()) 29 | 30 | queue.sync { 31 | XCTAssert(self.queue.isRunningOnQueue()) 32 | workCompleted = true 33 | } 34 | 35 | XCTAssertFalse(queue.isRunningOnQueue()) 36 | XCTAssert(workCompleted) 37 | } 38 | 39 | func testInnerSync() { 40 | var outerWorkCompleted = false 41 | var innerWorkCompleted = false 42 | 43 | XCTAssertFalse(queue.isRunningOnQueue()) 44 | 45 | queue.sync { 46 | XCTAssert(self.queue.isRunningOnQueue()) 47 | outerWorkCompleted = true 48 | 49 | self.queue.sync { 50 | XCTAssert(self.queue.isRunningOnQueue()) 51 | innerWorkCompleted = true 52 | } 53 | } 54 | 55 | XCTAssertFalse(queue.isRunningOnQueue()) 56 | XCTAssert(outerWorkCompleted) 57 | XCTAssert(innerWorkCompleted) 58 | } 59 | 60 | func testSerialSync() { 61 | var firstWorkCompleted = false 62 | var secondWorkCompleted = false 63 | 64 | XCTAssertFalse(queue.isRunningOnQueue()) 65 | 66 | queue.sync { 67 | XCTAssert(self.queue.isRunningOnQueue()) 68 | firstWorkCompleted = true 69 | } 70 | 71 | XCTAssert(firstWorkCompleted) 72 | XCTAssertFalse(secondWorkCompleted) 73 | 74 | queue.sync { 75 | XCTAssert(self.queue.isRunningOnQueue()) 76 | secondWorkCompleted = true 77 | } 78 | 79 | XCTAssertFalse(queue.isRunningOnQueue()) 80 | XCTAssert(firstWorkCompleted) 81 | XCTAssert(secondWorkCompleted) 82 | } 83 | 84 | func testAsync() { 85 | var workCompleted = false 86 | let workDone = self.expectation(description: "Work done") 87 | 88 | XCTAssertFalse(queue.isRunningOnQueue()) 89 | 90 | queue.async { 91 | XCTAssert(self.queue.isRunningOnQueue()) 92 | workCompleted = true 93 | workDone.fulfill() 94 | } 95 | 96 | waitForExpectations(timeout: 2.0, handler: nil) 97 | XCTAssertFalse(queue.isRunningOnQueue()) 98 | XCTAssert(workCompleted) 99 | } 100 | 101 | func testSerialAsync() { 102 | var firstWorkCompleted = false 103 | var secondWorkCompleted = false 104 | let firstWorkDone = self.expectation(description: "First work done") 105 | let secondWorkDone = self.expectation(description: "Second work done") 106 | 107 | XCTAssertFalse(queue.isRunningOnQueue()) 108 | 109 | queue.async { 110 | XCTAssertFalse(secondWorkCompleted) 111 | Thread.sleep(forTimeInterval: 1.0) 112 | XCTAssert(self.queue.isRunningOnQueue()) 113 | firstWorkCompleted = true 114 | firstWorkDone.fulfill() 115 | } 116 | 117 | queue.async { 118 | XCTAssert(firstWorkCompleted) 119 | Thread.sleep(forTimeInterval: 1.0) 120 | XCTAssert(self.queue.isRunningOnQueue()) 121 | secondWorkCompleted = true 122 | secondWorkDone.fulfill() 123 | } 124 | 125 | XCTAssertFalse(firstWorkCompleted) 126 | XCTAssertFalse(secondWorkCompleted) 127 | 128 | waitForExpectations(timeout: 3.0, handler: nil) 129 | XCTAssertFalse(queue.isRunningOnQueue()) 130 | XCTAssert(firstWorkCompleted) 131 | XCTAssert(secondWorkCompleted) 132 | } 133 | 134 | } 135 | --------------------------------------------------------------------------------