gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]