Helge Heß pointed out that naive usage of Pipe
in child Process
es can break your program if you pipe too much data. I wasn’t aware of this, followed his references, and here are my findings. Older Mac OS X versions had a pipe buffer size of 16KiB by default, offering 64KiB on demand; in my N=1 test on an M1 with macOS 14, I always get 64KiB buffers, even if I only send 1 Byte. Run pipe buffer size discovery tests yourself to check.
Continue reading …
Today I learned that you can cancel a delayed dispatch_block_t
with the new dispatch_block_cancel
(available since OS X 10.10/iOS 8.0). Thanks Matt for the post – here’s a Swift example:
let work = dispatch_block_create(0) { print("Hello!") }
# Execute after 10s
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(10 * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue(), work)
dispatch_block_cancel(work)
# Will never print "Hello!"
Note: canceling doesn’t work if the block is being executed.
If I knew that this API existed, I might not have used the very cumbersome approach from below in Move!.
Super-Weird Legacy Version of a Cancelable Delayed Block
For historic purposes, here’s an adaptation of the cancelable dispatch block you may find on the internet that I once have adapted for Swift:
typealias CancelableDispatchBlock = (cancel: Bool) -> Void
func dispatch(
cancelableBlock block: dispatch_block_t,
atDate date: NSDate
) -> CancelableDispatchBlock? {
// Use two pointers for the same block handle to make
// the block reference itself.
var cancelableBlock: CancelableDispatchBlock? = nil
let delayBlock: CancelableDispatchBlock = { cancel in
if !cancel {
dispatch_async(dispatch_get_main_queue(), block)
}
cancelableBlock = nil
}
cancelableBlock = delayBlock
let interval = Int64(date.timeIntervalSinceNow)
let delay = interval * Int64(NSEC_PER_SEC)
dispatch_after(dispatch_walltime(nil, delay), dispatch_get_main_queue()) {
guard let cancelableBlock = cancelableBlock else { return }
cancelableBlock(cancel: false)
}
return cancelableBlock
}
func cancelBlock(block: CancelableDispatchBlock?) {
guard let block = block else { return }
block(cancel: true)
}
The trick is this: the delayed block delayBlock: CancelableDispatchBlock
captures its context where a reference to cancelableBlock
is included – but not set yet. Then you make the reference point to the delayBlock
itself.
The actual canceling is a fake, though. The block is still called. It aborts early if the cancel
parameter is true
, though.
To debug my threading issues and help bring forth future problems, I have created a simmple object that slows the current queue down:
let IsRunningTests = NSClassFromString("XCTestCase") != nil
struct QueueJigglePoint {
/// Randomly interfere with the thread.
static func jiggle() {
guard !IsRunningTests else { return }
#if DEBUG
usleep(2*1000000) // 2 seconds
#endif
}
}
I got this idea from Brett Schuchert on pages 188–90 of Uncle Bob’s Clean Code. A Handbook of Agile Software Craftsmanship. There, interference with traditional threading should randomly sleep, yield, or fall through. Enqueued blocks are a lot less volatile, so I only came up with sleeping.
Randomizing the sleep interval is up next. But a fixed number of 2–10 seconds helps find UI-blocking code already.
Just throw in a QueueJigglePoint.jiggle()
in NSManagedObjectContext.performBlock
executions, when dispatching async to the background, or when reading files, for example.