Last active
February 28, 2017 07:40
-
-
Save reitzig/4ba3bd5490c0dcfaa21424ceb3c66836 to your computer and use it in GitHub Desktop.
A countdown latch based ob CwlUtils primitives
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CwlCountdownLatch.swift | |
// CwlUtils | |
// | |
// Created by Raphael Reitzig on 2017/02/16. | |
// Copyright © 2017 Raphael Reitzig. All rights reserved. | |
// | |
// Permission to use, copy, modify, and/or distribute this software for any | |
// purpose with or without fee is hereby granted, provided that the above | |
// copyright notice and this permission notice appear in all copies. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY | |
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR | |
// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
// | |
import Foundation | |
/** | |
A simple countdown latch that uses busy waiting. | |
Created with a number of steps to wait for, callers | |
can count down one by one. | |
A step is performed by calling `countDown()` which is, | |
obviously, thread-safe. | |
Instances are single-use: once the latch has counted down to zero, | |
all future calls to `await()` return immediately. | |
- Author: Raphael Reitzig | |
- Date: 16/02/17 | |
*/ | |
public class CountdownLatch { | |
private let counter: AtomicBox<Int> | |
/** | |
Creates a new countdown latch that blocks callers of `await()` | |
until the specified number of steps have been. | |
- Parameter from: The number of steps to wait for. | |
*/ | |
public init(from start: Int) { | |
precondition(start >= 0, "Latch can not work with negative goal.") | |
assert(start > 0, "Latch with goal 0 will have no effect.") | |
self.counter = AtomicBox(start) | |
} | |
/** | |
Count this latch down by one step. | |
- Note: Thread-safe. | |
*/ | |
public func countDown() throws { | |
let newValue = self.counter.mutate { $0 -= 1 } | |
if newValue < 0 { | |
throw LatchError.alreadyZero | |
} | |
} | |
/** | |
Does not return before this latch has been counted down to zero. | |
*/ | |
public func await() { | |
// TODO: is there something better than busy waiting? | |
while self.counter.value > 0 {} | |
} | |
/** | |
Does not return before this latch has been counted down to zero. | |
However, aborts waiting after the specified time has elapsed. | |
- Parameter for: The number of milliseconds to wait before throwing. | |
- Throws: If the specified time has elapsed before the latch reached | |
zero. | |
*/ | |
public func await(for ms: Int) throws { | |
let start = currentTimeMillis() | |
while self.counter.value > 0 { | |
let expired = currentTimeMillis() - start | |
if expired > Int64(ms) { | |
throw LatchError.expired(deadline: ms, waited: expired) | |
} | |
} | |
} | |
/** | |
- Returns: UNIX timestamp, i.e. a time in milliseconds. | |
*/ | |
private func currentTimeMillis() -> Int64{ | |
let nowDouble = NSDate().timeIntervalSince1970 | |
return Int64(nowDouble*1000) | |
} | |
private enum LatchError: Error, CustomStringConvertible { | |
case expired(deadline: Int, waited: Int64) | |
case alreadyZero | |
var description: String { | |
switch self { | |
case .expired(let goal, let waited): return "Waited for \(waited)ms, limit was \(goal)ms." | |
case .alreadyZero: return "Latch has already been counted down to zero" | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CwlCountdownLatchTests.swift | |
// CwlUtils | |
// | |
// Created by Raphael Reitzig on 2017/02/16. | |
// Copyright © 2017 Raphael Reitzig. All rights reserved. | |
// | |
// Permission to use, copy, modify, and/or distribute this software for any | |
// purpose with or without fee is hereby granted, provided that the above | |
// copyright notice and this permission notice appear in all copies. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY | |
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR | |
// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
// | |
import Foundation | |
import Foundation | |
import XCTest | |
import CwlUtils | |
class CountdownLatchTests: XCTestCase { | |
private let n = 10 | |
/// Test that the latch works properly in a single-thread setting | |
func testDirectCountdown() { | |
let latch = CountdownLatch(from: n) | |
var iterations = 0 | |
for _ in 1...n { | |
do { | |
try latch.countDown() | |
iterations += 1 | |
} catch {} | |
} | |
latch.await() | |
XCTAssertEqual(iterations, n) | |
} | |
/// Test that the latch works properly with a single asynchronous accessor. | |
func testSingleAsyncCountdown() { | |
let latch = CountdownLatch(from: n) | |
let iterations = AtomicBox(0) | |
DispatchQueue.global(qos: .background).async { | |
for _ in 1...self.n { | |
do { | |
try latch.countDown() | |
iterations.mutate { $0 += 1 } | |
} catch {} | |
} | |
} | |
latch.await() | |
XCTAssertEqual(iterations.value, n) | |
} | |
/// Test that the latch works properly with multiple asynchronous accessors. | |
// TODO: Do all of them get dispatched to the same thread? If so, this test is moot. | |
func testManyAsyncCountdowns() { | |
let latch = CountdownLatch(from: n) | |
let iterations = AtomicBox(0) | |
for _ in 1...n { | |
DispatchQueue.global(qos: .background).async { | |
do { | |
try latch.countDown() | |
iterations.mutate { $0 += 1 } | |
} catch {} | |
} | |
} | |
latch.await() | |
XCTAssertEqual(iterations.value, n) | |
} | |
/// Test that countDown does indeed throw an error if the latch is already at zero | |
func testTooManyCountdowns() throws { | |
let latch = CountdownLatch(from: n) | |
var iterations = 0 | |
for _ in 1...n { | |
do { | |
try latch.countDown() | |
iterations += 1 | |
} catch { | |
XCTFail("Should not throw an error before zero!") | |
} | |
} | |
do { | |
try latch.countDown() | |
XCTFail("Should not count below zero!") | |
} catch { | |
// All is good, we expected this! | |
} | |
} | |
/// Test that await with time limit does indeed throw an error if it waits too long | |
func testAwaitExpired() { | |
let latch = CountdownLatch(from: 1) | |
DispatchQueue.global(qos: .background).async { | |
do { | |
sleep(1) // seconds | |
try latch.countDown() | |
} catch {} | |
} | |
do { | |
try latch.await(for: 10) // milliseconds | |
XCTFail("We should not wait longer than specified!") | |
} catch { | |
// All is good, we expected this! | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment