[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-taler-ios] 07/32: AsyncSemaphore
From: |
gnunet |
Subject: |
[taler-taler-ios] 07/32: AsyncSemaphore |
Date: |
Mon, 16 Oct 2023 00:03:05 +0200 |
This is an automated email from the git hooks/post-receive script.
marc-stibane pushed a commit to branch master
in repository taler-ios.
commit 4a76e437802b9e51cdea6b37db599973f859bbed
Author: Marc Stibane <marc@taler.net>
AuthorDate: Fri Oct 13 09:08:11 2023 +0200
AsyncSemaphore
---
TalerWallet.xcodeproj/project.pbxproj | 6 +
TalerWallet1/Helper/AsyncSemaphore.swift | 253 +++++++++++++++++++++++++++++++
2 files changed, 259 insertions(+)
diff --git a/TalerWallet.xcodeproj/project.pbxproj
b/TalerWallet.xcodeproj/project.pbxproj
index efac9bd..1a3aade 100644
--- a/TalerWallet.xcodeproj/project.pbxproj
+++ b/TalerWallet.xcodeproj/project.pbxproj
@@ -10,6 +10,8 @@
4E16E12329F3BB99008B9C86 /* CurrencyFormatter.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4E16E12229F3BB99008B9C86 /*
CurrencyFormatter.swift */; };
4E2254972A822B8100E41D29 /* payment_received.m4a in Resources
*/ = {isa = PBXBuildFile; fileRef = 4E2254952A822B8100E41D29 /*
payment_received.m4a */; };
4E2254982A822B8100E41D29 /* payment_sent.m4a in Resources */ =
{isa = PBXBuildFile; fileRef = 4E2254962A822B8100E41D29 /* payment_sent.m4a */;
};
+ 4E3327BA2AD1635100BF5AD6 /* AsyncSemaphore.swift in Sources */
= {isa = PBXBuildFile; fileRef = 4E3327B92AD1635100BF5AD6 /*
AsyncSemaphore.swift */; };
+ 4E3327BB2AD1635100BF5AD6 /* AsyncSemaphore.swift in Sources */
= {isa = PBXBuildFile; fileRef = 4E3327B92AD1635100BF5AD6 /*
AsyncSemaphore.swift */; };
4E363CBC2A237E0900D7E98C /* URL+id+iban.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4E363CBB2A237E0900D7E98C /* URL+id+iban.swift
*/; };
4E363CBE2A23CB2100D7E98C /* AnyTransition+backslide.swift in
Sources */ = {isa = PBXBuildFile; fileRef = 4E363CBD2A23CB2100D7E98C /*
AnyTransition+backslide.swift */; };
4E363CC02A24754200D7E98C /* Settings.bundle in Resources */ =
{isa = PBXBuildFile; fileRef = 4E363CBF2A24754200D7E98C /* Settings.bundle */;
};
@@ -284,6 +286,7 @@
4E16E12229F3BB99008B9C86 /* CurrencyFormatter.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= CurrencyFormatter.swift; sourceTree = "<group>"; };
4E2254952A822B8100E41D29 /* payment_received.m4a */ = {isa =
PBXFileReference; lastKnownFileType = file; path = payment_received.m4a;
sourceTree = "<group>"; };
4E2254962A822B8100E41D29 /* payment_sent.m4a */ = {isa =
PBXFileReference; lastKnownFileType = file; path = payment_sent.m4a; sourceTree
= "<group>"; };
+ 4E3327B92AD1635100BF5AD6 /* AsyncSemaphore.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= AsyncSemaphore.swift; sourceTree = "<group>"; };
4E363CBB2A237E0900D7E98C /* URL+id+iban.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= "URL+id+iban.swift"; sourceTree = "<group>"; };
4E363CBD2A23CB2100D7E98C /* AnyTransition+backslide.swift */ =
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType =
sourcecode.swift; path = "AnyTransition+backslide.swift"; sourceTree =
"<group>"; };
4E363CBF2A24754200D7E98C /* Settings.bundle */ = {isa =
PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path =
Settings.bundle; sourceTree = "<group>"; };
@@ -558,6 +561,7 @@
isa = PBXGroup;
children = (
4E363CBD2A23CB2100D7E98C /*
AnyTransition+backslide.swift */,
+ 4E3327B92AD1635100BF5AD6 /*
AsyncSemaphore.swift */,
4EDBDCD82AB787CB00925C02 /* CallStack.swift */,
4E16E12229F3BB99008B9C86 /*
CurrencyFormatter.swift */,
4EAD117529F672FA008EDD0B /*
KeyboardResponder.swift */,
@@ -1029,6 +1033,7 @@
4E3EAE2A2A990778009F1BE8 /*
PendingRowView.swift in Sources */,
4E3EAE2B2A990778009F1BE8 /* LoadingView.swift
in Sources */,
4E3EAE8C2AA0933C009F1BE8 /* Font+Taler.swift in
Sources */,
+ 4E3327BA2AD1635100BF5AD6 /*
AsyncSemaphore.swift in Sources */,
4E3EAE2C2A990778009F1BE8 /*
ManualWithdraw.swift in Sources */,
4E3EAE2D2A990778009F1BE8 /*
Model+Exchange.swift in Sources */,
4E3EAE2E2A990778009F1BE8 /*
QRCodeDetailView.swift in Sources */,
@@ -1131,6 +1136,7 @@
4EB0955E2989CBFE0043A8A1 /*
PendingRowView.swift in Sources */,
4EB0956D2989CBFE0043A8A1 /* LoadingView.swift
in Sources */,
4E3EAE8D2AA0933C009F1BE8 /* Font+Taler.swift in
Sources */,
+ 4E3327BB2AD1635100BF5AD6 /*
AsyncSemaphore.swift in Sources */,
4E50B3502A1BEE8000F9F01C /*
ManualWithdraw.swift in Sources */,
4E3B4BC92A42BC4800CC88B8 /*
Model+Exchange.swift in Sources */,
4E5A88F52A38A4FD00072618 /*
QRCodeDetailView.swift in Sources */,
diff --git a/TalerWallet1/Helper/AsyncSemaphore.swift
b/TalerWallet1/Helper/AsyncSemaphore.swift
new file mode 100644
index 0000000..8ab5bdc
--- /dev/null
+++ b/TalerWallet1/Helper/AsyncSemaphore.swift
@@ -0,0 +1,253 @@
+// Copyright (C) 2022 Gwendal Roué
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import Foundation
+
+/// An object that controls access to a resource across multiple execution
+/// contexts through use of a traditional counting semaphore.
+///
+/// You increment a semaphore count by calling the ``signal()`` method, and
+/// decrement a semaphore count by calling ``wait()`` or one of its variants.
+///
+/// ## Topics
+///
+/// ### Creating a Semaphore
+///
+/// - ``init(value:)``
+///
+/// ### Signaling the Semaphore
+///
+/// - ``signal()``
+///
+/// ### Waiting for the Semaphore
+///
+/// - ``wait()``
+/// - ``waitUnlessCancelled()``
+public final class AsyncSemaphore: @unchecked Sendable {
+ /// `Suspension` is the state of a task waiting for a signal.
+ ///
+ /// It is a class because instance identity helps `waitUnlessCancelled()`
+ /// deal with both early and late cancellation.
+ ///
+ /// We make it @unchecked Sendable in order to prevent compiler warnings:
+ /// instances are always protected by the semaphore's lock.
+ private class Suspension: @unchecked Sendable {
+ enum State {
+ /// Initial state. Next is suspendedUnlessCancelled, or cancelled.
+ case pending
+
+ /// Waiting for a signal, with support for cancellation.
+ case suspendedUnlessCancelled(UnsafeContinuation<Void, Error>)
+
+ /// Waiting for a signal, with no support for cancellation.
+ case suspended(UnsafeContinuation<Void, Never>)
+
+ /// Cancelled before we have started waiting.
+ case cancelled
+ }
+
+ var state: State
+
+ init(state: State) {
+ self.state = state
+ }
+ }
+
+ // MARK: - Internal State
+
+ /// The semaphore value.
+ private var value: Int
+
+ /// As many elements as there are suspended tasks waiting for a signal.
+ private var suspensions: [Suspension] = []
+
+ /// The lock that protects `value` and `suspensions`.
+ ///
+ /// It is recursive in order to handle cancellation (see the implementation
+ /// of ``waitUnlessCancelled()``).
+ private let _lock = NSRecursiveLock()
+
+ // MARK: - Creating a Semaphore
+
+ /// Creates a semaphore.
+ ///
+ /// - parameter value: The starting value for the semaphore. Do not pass a
+ /// value less than zero.
+ public init(value: Int) {
+ precondition(value >= 0, "AsyncSemaphore requires a value equal or
greater than zero")
+ self.value = value
+ }
+
+ deinit {
+ precondition(suspensions.isEmpty, "AsyncSemaphore is deallocated while
some task(s) are suspended waiting for a signal.")
+ }
+
+ // MARK: - Locking
+
+ // Let's hide the locking primitive in order to avoid a compiler warning:
+ //
+ // > Instance method 'lock' is unavailable from asynchronous contexts;
+ // > Use async-safe scoped locking instead; this is an error in Swift 6.
+ //
+ // We're not sweeping bad stuff under the rug. We really need to protect
+ // our inner state (`value` and `suspension`) across the calls to
+ // `withUnsafeContinuation`. Unfortunately, this method introduces a
+ // suspension point. So we need a lock.
+ private func lock() { _lock.lock() }
+ private func unlock() { _lock.unlock() }
+
+ // MARK: - Waiting for the Semaphore
+
+ /// Waits for, or decrements, a semaphore.
+ ///
+ /// Decrement the counting semaphore. If the resulting value is less than
+ /// zero, this function suspends the current task until a signal occurs,
+ /// without blocking the underlying thread. Otherwise, no suspension
happens.
+ public func wait() async {
+ lock()
+
+ value -= 1
+ if value >= 0 {
+ unlock()
+ return
+ }
+
+ await withUnsafeContinuation { continuation in
+ // Register the continuation that `signal` will resume.
+ let suspension = Suspension(state: .suspended(continuation))
+ suspensions.insert(suspension, at: 0) // FIFO
+ unlock()
+ }
+ }
+
+ /// Waits for, or decrements, a semaphore, with support for cancellation.
+ ///
+ /// Decrement the counting semaphore. If the resulting value is less than
+ /// zero, this function suspends the current task until a signal occurs,
+ /// without blocking the underlying thread. Otherwise, no suspension
happens.
+ ///
+ /// If the task is canceled before a signal occurs, this function
+ /// throws `CancellationError`.
+ public func waitUnlessCancelled() async throws {
+ lock()
+
+ value -= 1
+ if value >= 0 {
+ defer { unlock() }
+
+ do {
+ // All code paths check for cancellation
+ try Task.checkCancellation()
+ } catch {
+ // Cancellation is like a signal: we don't really "consume"
+ // the semaphore, and restore the value.
+ value += 1
+ throw error
+ }
+
+ return
+ }
+
+ // Get ready for being suspended waiting for a continuation, or for
+ // early cancellation.
+ let suspension = Suspension(state: .pending)
+
+ try await withTaskCancellationHandler {
+ try await withUnsafeThrowingContinuation { (continuation:
UnsafeContinuation<Void, Error>) in
+ if case .cancelled = suspension.state {
+ // Early cancellation: waitUnlessCancelled() is called from
+ // a cancelled task, and the `onCancel` closure below
+ // has marked the suspension as cancelled.
+ // Resume with a CancellationError.
+ unlock()
+ continuation.resume(throwing: CancellationError())
+ } else {
+ // Current task is not cancelled: register the continuation
+ // that `signal` will resume.
+ suspension.state = .suspendedUnlessCancelled(continuation)
+ suspensions.insert(suspension, at: 0) // FIFO
+ unlock()
+ }
+ }
+ } onCancel: {
+ // withTaskCancellationHandler may immediately call this block (if
+ // the current task is cancelled), or call it later (if the task is
+ // cancelled later). In the first case, we're still holding the
lock,
+ // waiting for the continuation. In the second case, we do not hold
+ // the lock. Being able to handle both situations is the reason why
+ // we use a recursive lock.
+ lock()
+
+ // We're no longer waiting for a signal
+ value += 1
+ if let index = suspensions.firstIndex(where: { $0 === suspension
}) {
+ suspensions.remove(at: index)
+ }
+
+ if case let .suspendedUnlessCancelled(continuation) =
suspension.state {
+ // Late cancellation: the task is cancelled while waiting
+ // from the semaphore. Resume with a CancellationError.
+ unlock()
+ continuation.resume(throwing: CancellationError())
+ } else {
+ // Early cancellation: waitUnlessCancelled() is called from
+ // a cancelled task.
+ //
+ // The next step is the `withTaskCancellationHandler`
+ // operation closure right above.
+ suspension.state = .cancelled
+ unlock()
+ }
+ }
+ }
+
+ // MARK: - Signaling the Semaphore
+
+ /// Signals (increments) a semaphore.
+ ///
+ /// Increment the counting semaphore. If the previous value was less than
+ /// zero, this function resumes a task currently suspended in ``wait()``
+ /// or ``waitUnlessCancelled()``.
+ ///
+ /// - returns: This function returns true if a suspended task is
+ /// resumed. Otherwise, the result is false, meaning that no task was
+ /// waiting for the semaphore.
+ @discardableResult
+ public func signal() -> Bool {
+ lock()
+
+ value += 1
+
+ switch suspensions.popLast()?.state { // FIFO
+ case let .suspendedUnlessCancelled(continuation):
+ unlock()
+ continuation.resume()
+ return true
+ case let .suspended(continuation):
+ unlock()
+ continuation.resume()
+ return true
+ default:
+ unlock()
+ return false
+ }
+ }
+}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-taler-ios] 08/32: cleanup, (continued)
- [taler-taler-ios] 08/32: cleanup, gnunet, 2023/10/15
- [taler-taler-ios] 13/32: width of rendered string, gnunet, 2023/10/15
- [taler-taler-ios] 10/32: secret token, gnunet, 2023/10/15
- [taler-taler-ios] 15/32: TwoRowButtons, gnunet, 2023/10/15
- [taler-taler-ios] 05/32: No spellout, gnunet, 2023/10/15
- [taler-taler-ios] 09/32: TabBar for Taler Wallet - GNU Taler stays on SideView, gnunet, 2023/10/15
- [taler-taler-ios] 16/32: Source of Truth for balances, gnunet, 2023/10/15
- [taler-taler-ios] 30/32: font, gnunet, 2023/10/15
- [taler-taler-ios] 27/32: SingleAxisGeometryReader, gnunet, 2023/10/15
- [taler-taler-ios] 06/32: More CallStack for Debugging, gnunet, 2023/10/15
- [taler-taler-ios] 07/32: AsyncSemaphore,
gnunet <=
- [taler-taler-ios] 23/32: fix for broken scalable font (not finished), gnunet, 2023/10/15
- [taler-taler-ios] 11/32: bankAccessApiBaseUrl -> corebankApiBaseUrl, gnunet, 2023/10/15
- [taler-taler-ios] 12/32: AsyncSemaphore to serialize Getbalances, gnunet, 2023/10/15
- [taler-taler-ios] 31/32: comment, gnunet, 2023/10/15
- [taler-taler-ios] 25/32: Bargraph after Currency, gnunet, 2023/10/15
- [taler-taler-ios] 32/32: Bump version to 0.9.3 (20), gnunet, 2023/10/15
- [taler-taler-ios] 21/32: cleanup, gnunet, 2023/10/15
- [taler-taler-ios] 26/32: cleanup, gnunet, 2023/10/15
- [taler-taler-ios] 22/32: badge, gnunet, 2023/10/15
- [taler-taler-ios] 28/32: Button Layout, gnunet, 2023/10/15