├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── LICENSE_HOYLEN ├── LICENSE_MEZONI ├── README.md ├── lib ├── mutex.dart ├── read_write_mutex.dart ├── semaphore.dart ├── sync.dart └── waitgroup.dart ├── pubspec.yaml └── test ├── mutex_test.dart ├── read_write_mutex_test.dart └── semaphore_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .packages 3 | .pub/ 4 | build/ 5 | packages 6 | # Remove the following pattern if you wish to check in your lock file 7 | pubspec.lock 8 | 9 | # Files created by dart2js 10 | *.dart.js 11 | *.part.js 12 | *.js.deps 13 | *.js.map 14 | *.info.json 15 | 16 | # Directory created by dartdoc 17 | doc/api/ 18 | 19 | # JetBrains IDEs 20 | .idea/ 21 | *.iml 22 | *.ipr 23 | *.iws 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.0 4 | 5 | - Add null-safety support 6 | - Update SDK requirement to 2.12.0 7 | 8 | ## 0.1.0 9 | 10 | - Initial version. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Steven Roose 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /LICENSE_HOYLEN: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Hoylen Sue. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /LICENSE_MEZONI: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Andrew Mezoni. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sync 2 | 3 | A library for managing asynchronous processes inspired by the Go sync package. 4 | 5 | ## Features 6 | 7 | - Mutex & ReadWriteMutex 8 | - Semaphore 9 | - WaitGroup 10 | 11 | ## License 12 | 13 | Unless otherwise mentioned at the top of a file, all code is licensed under the MIT license as 14 | found in the LICENSE file. 15 | -------------------------------------------------------------------------------- /lib/mutex.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, Hoylen Sue. 2 | // All rights reserved. 3 | // Subject to BSD 3-clause license. See file LICENSE_HOYLEN. 4 | library sync.mutex; 5 | 6 | import "dart:async"; 7 | 8 | import "read_write_mutex.dart"; 9 | 10 | /// Mutual exclusion. 11 | /// 12 | /// Usage: 13 | /// 14 | /// m = new Mutex(); 15 | /// 16 | /// await m.acquire(); 17 | /// try { 18 | /// // critical section 19 | /// } 20 | /// finally { 21 | /// m.release(); 22 | /// } 23 | /// 24 | class Mutex { 25 | /// Implemented as a ReadWriteMutex that is used only with write locks. 26 | final ReadWriteMutex _rwMutex = new ReadWriteMutex(); 27 | 28 | /// Indicates if a lock has currently been acquired. 29 | bool get isLocked => (_rwMutex.isLocked); 30 | 31 | /// Acquire a lock 32 | /// 33 | /// Returns a future that will be completed when the lock has been acquired. 34 | /// 35 | Future acquire() => _rwMutex.acquireWrite(); 36 | 37 | /// Release a lock. 38 | /// 39 | /// Release a lock that has been acquired. 40 | /// 41 | void release() => _rwMutex.release(); 42 | } 43 | -------------------------------------------------------------------------------- /lib/read_write_mutex.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, Hoylen Sue. 2 | // All rights reserved. 3 | // Subject to BSD 3-clause license. See file LICENSE_HOYLEN. 4 | library sync.read_write_mutex; 5 | 6 | import "dart:async"; 7 | import "dart:collection"; 8 | 9 | /// Represents a request for a lock. 10 | /// 11 | /// This is instantiated for each acquire and, if necessary, it is added 12 | /// to the waiting queue. 13 | /// 14 | class _ReadWriteMutexRequest { 15 | final bool isRead; // true = read lock requested; false = write lock requested 16 | 17 | final Completer completer = new Completer(); 18 | 19 | _ReadWriteMutexRequest(this.isRead); 20 | } 21 | 22 | /// Mutual exclusion that supports read and write locks. 23 | /// 24 | /// Multiple read locks can be simultaneously acquired, but at most only 25 | /// one write lock can be acquired at any one time. 26 | /// 27 | /// Create the mutex: 28 | /// 29 | /// m = new ReadWriteMutex(); 30 | /// 31 | /// Some code can acquire a write lock: 32 | /// 33 | /// await m.acquireWrite(); 34 | /// try { 35 | /// // critical write section 36 | /// assert(m.isWriteLocked); 37 | /// } 38 | /// finally { 39 | /// m.release(); 40 | /// } 41 | /// 42 | /// Other code can acquire a read lock. 43 | /// 44 | /// await m.acquireRead(); 45 | /// try { 46 | /// // critical read section 47 | /// assert(m.isReadLocked); 48 | /// } 49 | /// finally { 50 | /// m.release(); 51 | /// } 52 | /// 53 | /// The current implementation lets locks be acquired in first-in-first-out 54 | /// order. This ensures there will not be any lock starvation, which can 55 | /// happen if some locks are prioritised over others. Submit a feature 56 | /// request issue, if there is a need for another scheduling algorithm. 57 | /// 58 | class ReadWriteMutex { 59 | final Queue<_ReadWriteMutexRequest> _waiting = 60 | new Queue<_ReadWriteMutexRequest>(); 61 | 62 | int _state = 0; // -1 = write lock, +ve = number of read locks; 0 = no lock 63 | 64 | /// Indicates if a lock (read or write) has currently been acquired. 65 | bool get isLocked => (_state != 0); 66 | 67 | /// Indicates if a write lock has currently been acquired. 68 | bool get isWriteLocked => (_state == -1); 69 | 70 | /// Indicates if a read lock has currently been acquired. 71 | bool get isReadLocked => (0 < _state); 72 | 73 | /// Acquire a read lock 74 | /// 75 | /// Returns a future that will be completed when the lock has been acquired. 76 | Future acquireRead() => _acquire(true); 77 | 78 | /// Acquire a write lock 79 | /// 80 | /// Returns a future that will be completed when the lock has been acquired. 81 | Future acquireWrite() => _acquire(false); 82 | 83 | /// Release a lock. 84 | /// 85 | /// Release a lock that has been acquired. 86 | void release() { 87 | if (_state == -1) { 88 | // Write lock released 89 | _state = 0; 90 | } else if (0 < _state) { 91 | // Read lock released 92 | _state--; 93 | } else if (_state == 0) { 94 | throw new StateError("no lock to release"); 95 | } else { 96 | assert(false); 97 | } 98 | 99 | // Let all jobs that can now acquire a lock do so. 100 | 101 | while (_waiting.isNotEmpty) { 102 | var nextJob = _waiting.first; 103 | if (_jobAcquired(nextJob)) { 104 | _waiting.removeFirst(); 105 | } else { 106 | break; // no more can be acquired 107 | } 108 | } 109 | } 110 | 111 | /// Internal acquire method. 112 | /// 113 | Future _acquire(bool isRead) { 114 | var newJob = new _ReadWriteMutexRequest(isRead); 115 | if (!_jobAcquired(newJob)) { 116 | _waiting.add(newJob); 117 | } 118 | return newJob.completer.future; 119 | } 120 | 121 | /// Determine if the [job] can now acquire the lock. 122 | /// 123 | /// If it can acquire the lock, the job's completer is completed, the 124 | /// state updated, and true is returned. If not, false is returned. 125 | /// 126 | bool _jobAcquired(_ReadWriteMutexRequest job) { 127 | assert(-1 <= _state); 128 | if (_state == 0 || (0 < _state && job.isRead)) { 129 | // Can acquire 130 | _state = (job.isRead) ? (_state + 1) : -1; 131 | job.completer.complete(); 132 | return true; 133 | } else { 134 | return false; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/semaphore.dart: -------------------------------------------------------------------------------- 1 | library sync.semaphore; 2 | 3 | import "dart:async"; 4 | import "dart:collection"; 5 | 6 | /// A Semaphore class. 7 | class Semaphore { 8 | final int maxCount; 9 | 10 | int _counter = 0; 11 | Queue _waitQueue = new Queue(); 12 | 13 | Semaphore([this.maxCount = 1]) { 14 | if (maxCount < 1) { 15 | throw new RangeError.value(maxCount, "maxCount"); 16 | } 17 | } 18 | 19 | /// Acquires a permit from this semaphore, asynchronously blocking until one 20 | /// is available. 21 | Future acquire() { 22 | var completer = new Completer(); 23 | if (_counter + 1 <= maxCount) { 24 | _counter++; 25 | completer.complete(); 26 | } else { 27 | _waitQueue.add(completer); 28 | } 29 | return completer.future; 30 | } 31 | 32 | /// Releases a permit, returning it to the semaphore. 33 | void release() { 34 | if (_counter == 0) { 35 | throw new StateError("Unable to release semaphore."); 36 | } 37 | _counter--; 38 | if (_waitQueue.length > 0) { 39 | _counter++; 40 | var completer = _waitQueue.removeFirst(); 41 | completer.complete(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/sync.dart: -------------------------------------------------------------------------------- 1 | library sync; 2 | 3 | export "mutex.dart"; 4 | export "read_write_mutex.dart"; 5 | export "semaphore.dart"; 6 | export "waitgroup.dart"; 7 | -------------------------------------------------------------------------------- /lib/waitgroup.dart: -------------------------------------------------------------------------------- 1 | library sync.waitgroup; 2 | 3 | import "dart:async"; 4 | 5 | /// A WaitGroup waits for a collection of processes to finish. 6 | /// The main process calls [add] to set the number of processes to wait for. 7 | /// Then each of the processes runs and calls [done] when finished. At the same 8 | /// time, [wait] can be used to block until all processes have finished. 9 | class WaitGroup { 10 | int _counter = 0; 11 | Completer? _completer; 12 | 13 | WaitGroup(); 14 | 15 | /// Adds delta, which may be negative, to the WaitGroup counter. 16 | /// If a wait Future is open and the counter becomes zero, the future is 17 | /// released. 18 | /// If the counter goes negative, it throws. 19 | void add([int amount = 1]) { 20 | if (_counter + amount < 0) { 21 | throw new StateError("WaitGroup counter cannot go negative."); 22 | } 23 | _counter += amount; 24 | final completer = _completer; 25 | if (_counter == 0 && completer != null) { 26 | completer.complete(); 27 | } 28 | } 29 | 30 | /// Decrements the WaitGroup counter. 31 | void done() => add(-1); 32 | 33 | /// Returns a future that will complete when the WaitGroup counter is zero. 34 | Future wait() { 35 | if (_counter == 0) { 36 | return new Future.value(); 37 | } 38 | 39 | final completer = _completer; 40 | if (completer == null) { 41 | final completer = Completer(); 42 | _completer = completer; 43 | return completer.future; 44 | } 45 | return completer.future; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: sync 2 | description: A library for managing asynchronous processes inspired by the Go sync package. 3 | version: 0.3.0 4 | author: Steven Roose 5 | homepage: https://github.com/stevenroose/dart-sync 6 | 7 | environment: 8 | sdk: '>=2.12.0 <3.0.0' 9 | 10 | dev_dependencies: 11 | test: ">=1.16.2 <2.0.0" 12 | -------------------------------------------------------------------------------- /test/mutex_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, Hoylen Sue. 2 | // All rights reserved. 3 | // Subject to BSD 3-clause license. See file LICENSE_HOYLEN. 4 | 5 | import "dart:async"; 6 | import "package:test/test.dart"; 7 | import "package:sync/mutex.dart"; 8 | 9 | /// Wait for duration. 10 | /// 11 | /// During this time other code may execute, which could lead to race conditions 12 | /// if critical sections of code are not protected. 13 | /// 14 | Future sleep([Duration? duration]) async { 15 | assert(duration != null && duration is Duration); 16 | 17 | var completer = new Completer(); 18 | new Timer(duration!, () { 19 | completer.complete(); 20 | }); 21 | 22 | return completer.future; 23 | } 24 | 25 | /// Account simulating the classic "simultaneous update" concurrency problem. 26 | /// 27 | /// The deposit operation reads the balance, waits for a short time (where 28 | /// problems can occur if the balance is changed) and then writes out the 29 | /// new balance. 30 | /// 31 | class Account { 32 | int get balance => _balance; 33 | int _balance = 0; 34 | 35 | int _operation = 0; 36 | 37 | Mutex mutex = new Mutex(); 38 | 39 | /// Set to true to print out read/write to the balance during deposits 40 | static final bool debugOutput = false; 41 | 42 | /// Time used for calculating time offsets in debug messages. 43 | DateTime _startTime = new DateTime.now(); 44 | 45 | void _debugPrint([String? message]) { 46 | if (debugOutput) { 47 | if (message != null) { 48 | var t = new DateTime.now().difference(_startTime).inMilliseconds; 49 | print("$t: $message"); 50 | } else { 51 | print(""); 52 | } 53 | } 54 | } 55 | 56 | void reset([int startingBalance = 0]) { 57 | _balance = startingBalance; 58 | if (debugOutput) { 59 | _startTime = new DateTime.now(); 60 | _debugPrint(); 61 | } 62 | } 63 | 64 | /// Waits [startDelay] and then invokes critical section without mutex. 65 | /// 66 | Future depositUnsafe(int amount, int startDelay, int dangerWindow) async { 67 | await sleep(new Duration(milliseconds: startDelay)); 68 | await _depositCriticalSection(amount, dangerWindow); 69 | } 70 | 71 | /// Waits [startDelay] and then invokes critical section with mutex. 72 | /// 73 | Future depositWithMutex(int amount, int startDelay, int dangerWindow) async { 74 | await sleep(new Duration(milliseconds: startDelay)); 75 | 76 | await mutex.acquire(); 77 | try { 78 | expect(mutex.isLocked, isTrue); 79 | await _depositCriticalSection(amount, dangerWindow); 80 | expect(mutex.isLocked, isTrue); 81 | } finally { 82 | mutex.release(); 83 | } 84 | } 85 | 86 | /// Critical section of adding [amount] to the balance. 87 | /// 88 | /// Reads the balance, then sleeps for [dangerWindow] milliseconds, before 89 | /// saving the new balance. If not protected, another invocation of this 90 | /// method while it is sleeping will read the balance before it is updated. 91 | /// The one that saves its balance last will overwrite the earlier saved 92 | /// balances (effectively those other deposits will be lost). 93 | /// 94 | Future _depositCriticalSection(int amount, int dangerWindow) async { 95 | var op = ++_operation; 96 | 97 | _debugPrint("[$op] read balance: $_balance"); 98 | 99 | var tmp = _balance; 100 | await sleep(new Duration(milliseconds: dangerWindow)); 101 | _balance = tmp + amount; 102 | 103 | _debugPrint("[$op] write balance: $_balance (= $tmp + $amount)"); 104 | } 105 | } 106 | 107 | //---------------------------------------------------------------- 108 | 109 | void main() { 110 | final int CORRECT_BALANCE = 68; 111 | 112 | var account = new Account(); 113 | 114 | group("Mutex", () { 115 | test("without mutex", () async { 116 | // First demonstrate that without mutex incorrect results are produced. 117 | 118 | // Without mutex produces incorrect result 119 | // 000. a reads 0 120 | // 025. b reads 0 121 | // 050. a writes 42 122 | // 075. b writes 26 123 | account.reset(); 124 | await Future.wait([ 125 | account.depositUnsafe(42, 0, 50), 126 | account.depositUnsafe(26, 25, 50) // result overwrites first deposit 127 | ]); 128 | expect(account.balance, equals(26)); // incorrect: first deposit lost 129 | 130 | // Without mutex produces incorrect result 131 | // 000. b reads 0 132 | // 025. a reads 0 133 | // 050. b writes 26 134 | // 075. a writes 42 135 | account.reset(); 136 | await Future.wait([ 137 | account.depositUnsafe(42, 25, 50), // result overwrites second deposit 138 | account.depositUnsafe(26, 0, 50) 139 | ]); 140 | expect(account.balance, equals(42)); // incorrect: second deposit lost 141 | }); 142 | 143 | test("with mutex", () async { 144 | // Test correct results are produced with mutex 145 | 146 | // With mutex produces correct result 147 | // 000. a acquires lock 148 | // 000. a reads 0 149 | // 025. b is blocked 150 | // 050. a writes 42 151 | // 050. a releases lock 152 | // 050. b acquires lock 153 | // 050. b reads 42 154 | // 100. b writes 68 155 | account.reset(); 156 | await Future.wait([ 157 | account.depositWithMutex(42, 0, 50), 158 | account.depositWithMutex(26, 25, 50) 159 | ]); 160 | expect(account.balance, equals(CORRECT_BALANCE)); 161 | 162 | // With mutex produces correct result 163 | // 000. b acquires lock 164 | // 000. b reads 0 165 | // 025. a is blocked 166 | // 050. b writes 26 167 | // 050. b releases lock 168 | // 050. a acquires lock 169 | // 050. a reads 26 170 | // 100. a writes 68 171 | account.reset(); 172 | await Future.wait([ 173 | account.depositWithMutex(42, 25, 50), 174 | account.depositWithMutex(26, 0, 50) 175 | ]); 176 | expect(account.balance, equals(CORRECT_BALANCE)); 177 | }); 178 | 179 | test("multiple acquires are serialized", () async { 180 | // Demonstrate that sections running in a mutex are effectively serialized 181 | const int delay = 200; // milliseconds 182 | const int overhead = 100; // milliseconds 183 | account.reset(); 184 | var startTime = new DateTime.now(); 185 | await Future.wait([ 186 | account.depositWithMutex(1, 0, delay), 187 | account.depositWithMutex(1, 0, delay), 188 | account.depositWithMutex(1, 0, delay), 189 | account.depositWithMutex(1, 0, delay), 190 | account.depositWithMutex(1, 0, delay), 191 | account.depositWithMutex(1, 0, delay), 192 | account.depositWithMutex(1, 0, delay), 193 | account.depositWithMutex(1, 0, delay), 194 | account.depositWithMutex(1, 0, delay), 195 | account.depositWithMutex(1, 0, delay), 196 | ]); 197 | var finishTime = new DateTime.now(); 198 | var ms = finishTime.difference(startTime).inMilliseconds; 199 | expect(ms, greaterThan(delay * 10)); 200 | expect(ms, lessThan(delay * 10 + overhead)); 201 | }); 202 | }); 203 | } 204 | -------------------------------------------------------------------------------- /test/read_write_mutex_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, Hoylen Sue. 2 | // All rights reserved. 3 | // Subject to BSD 3-clause license. See file LICENSE_HOYLEN. 4 | 5 | import "dart:async"; 6 | import "package:test/test.dart"; 7 | import "package:sync/read_write_mutex.dart"; 8 | 9 | /// Wait for duration. 10 | /// 11 | /// During this time other code may execute, which could lead to race conditions 12 | /// if critical sections of code are not protected. 13 | /// 14 | Future sleep([Duration? duration]) async { 15 | assert(duration != null && duration is Duration); 16 | 17 | var completer = new Completer(); 18 | new Timer(duration!, () { 19 | completer.complete(); 20 | }); 21 | 22 | return completer.future; 23 | } 24 | 25 | class RWTester { 26 | int get numWrites => _numWrites; 27 | int _numWrites = 0; 28 | 29 | int _operation = 0; 30 | 31 | ReadWriteMutex mutex = new ReadWriteMutex(); 32 | 33 | /// Set to true to print out read/write to the balance during deposits 34 | static final bool debugOutput = false; 35 | 36 | late DateTime _startTime; 37 | 38 | void _debugPrint([String? message]) { 39 | if (debugOutput) { 40 | if (message != null) { 41 | var t = new DateTime.now().difference(_startTime).inMilliseconds; 42 | print("$t: $message"); 43 | } else { 44 | print(""); 45 | } 46 | } 47 | } 48 | 49 | /// Constructor for an account. 50 | /// 51 | /// Uses RentrantMutex if [reentrant] is true; otherwise uses NormalMutex. 52 | /// 53 | RWTester() { 54 | _startTime = new DateTime.now(); 55 | } 56 | 57 | void reset([int startingBalance = 0]) { 58 | _numWrites = startingBalance; 59 | if (debugOutput) { 60 | _startTime = new DateTime.now(); 61 | _debugPrint(); 62 | } 63 | } 64 | 65 | /// Waits [startDelay] and then invokes critical section with mutex. 66 | /// 67 | Future writing(int startDelay, int dangerWindow) async { 68 | await sleep(new Duration(milliseconds: startDelay)); 69 | 70 | await mutex.acquireWrite(); 71 | try { 72 | await _writingCriticalSection(dangerWindow); 73 | } finally { 74 | mutex.release(); 75 | } 76 | } 77 | 78 | /// Critical section of adding [amount] to the balance. 79 | /// 80 | /// Reads the balance, then sleeps for [dangerWindow] milliseconds, before 81 | /// saving the new balance. If not protected, another invocation of this 82 | /// method while it is sleeping will read the balance before it is updated. 83 | /// The one that saves its balance last will overwrite the earlier saved 84 | /// balances (effectively those other deposits will be lost). 85 | /// 86 | Future _writingCriticalSection(int dangerWindow) async { 87 | var op = ++_operation; 88 | 89 | _debugPrint("[$op] write start: <- $_numWrites"); 90 | 91 | var tmp = _numWrites; 92 | expect(mutex.isWriteLocked, isTrue); 93 | await sleep(new Duration(milliseconds: dangerWindow)); 94 | expect(mutex.isWriteLocked, isTrue); 95 | expect(_numWrites, equals(tmp)); 96 | 97 | _numWrites = tmp + 1; // change the balance 98 | 99 | _debugPrint("[$op] write finish: -> $_numWrites"); 100 | } 101 | 102 | /// Waits [startDelay] and then invokes critical section with mutex. 103 | /// 104 | /// This method demonstrates the use of a read lock on the mutex. 105 | /// 106 | Future reading(int startDelay, int dangerWindow) async { 107 | await sleep(new Duration(milliseconds: startDelay)); 108 | 109 | await mutex.acquireRead(); 110 | try { 111 | return await _readingCriticalSection(dangerWindow); 112 | } finally { 113 | mutex.release(); 114 | } 115 | } 116 | 117 | /// Critical section that must be done in a read lock. 118 | /// 119 | Future _readingCriticalSection(int dangerWindow) async { 120 | var op = ++_operation; 121 | 122 | _debugPrint("[$op] read start: <- $_numWrites"); 123 | 124 | var tmp = _numWrites; 125 | expect(mutex.isReadLocked, isTrue); 126 | await sleep(new Duration(milliseconds: dangerWindow)); 127 | expect(mutex.isReadLocked, isTrue); 128 | expect(_numWrites, equals(tmp)); 129 | 130 | _debugPrint("[$op] read finish: <- $_numWrites"); 131 | } 132 | } 133 | 134 | //---------------------------------------------------------------- 135 | 136 | void main() { 137 | var account = new RWTester(); 138 | 139 | group("ReadWriteMutex", () { 140 | test("multiple read locks", () async { 141 | const int delay = 200; // milliseconds 142 | const int overhead = 50; // milliseconds 143 | account.reset(); 144 | var startTime = new DateTime.now(); 145 | await Future.wait([ 146 | account.reading(0, delay), 147 | account.reading(0, delay), 148 | account.reading(0, delay), 149 | account.reading(0, delay), 150 | account.reading(0, delay), 151 | account.reading(0, delay), 152 | account.reading(0, delay), 153 | account.reading(0, delay), 154 | account.reading(0, delay), 155 | account.reading(0, delay), 156 | ]); 157 | var finishTime = new DateTime.now(); 158 | var ms = finishTime.difference(startTime).inMilliseconds; 159 | expect(ms, greaterThan(delay)); 160 | expect(ms, lessThan(delay + overhead)); 161 | expect(account.numWrites, equals(0)); 162 | }); 163 | 164 | test("multiple write locks", () async { 165 | const int delay = 200; // milliseconds 166 | const int overhead = 100; // milliseconds 167 | account.reset(); 168 | var startTime = new DateTime.now(); 169 | await Future.wait([ 170 | account.writing(0, delay), 171 | account.writing(0, delay), 172 | account.writing(0, delay), 173 | account.writing(0, delay), 174 | account.writing(0, delay), 175 | account.writing(0, delay), 176 | account.writing(0, delay), 177 | account.writing(0, delay), 178 | account.writing(0, delay), 179 | account.writing(0, delay), 180 | ]); 181 | var finishTime = new DateTime.now(); 182 | var ms = finishTime.difference(startTime).inMilliseconds; 183 | expect(ms, greaterThan(delay * 10)); 184 | expect(ms, lessThan(delay * 10 + overhead)); 185 | expect(account.numWrites, equals(10)); 186 | }); 187 | 188 | test("mixture of read and write locks", () async { 189 | const int delay = 200; // milliseconds 190 | const int overhead = 100; // milliseconds 191 | account.reset(); 192 | var startTime = new DateTime.now(); 193 | await Future.wait([ 194 | account.writing(0, 1000), 195 | account.reading(100, delay), 196 | account.reading(110, delay), 197 | account.reading(120, delay), 198 | account.writing(130, delay), 199 | account.writing(140, delay), 200 | account.writing(150, delay), 201 | account.reading(160, delay), 202 | account.reading(170, delay), 203 | account.reading(180, delay), 204 | account.writing(190, delay), 205 | account.writing(200, delay), 206 | account.writing(210, delay), 207 | account.reading(220, delay), 208 | account.reading(230, delay), 209 | account.reading(240, delay), 210 | ]); 211 | var finishTime = new DateTime.now(); 212 | var ms = finishTime.difference(startTime).inMilliseconds; 213 | expect(ms, greaterThan(1000 + delay * 9)); 214 | expect(ms, lessThan(1000 + delay * 9 + overhead)); 215 | expect(account.numWrites, equals(7)); 216 | }); 217 | }); 218 | } 219 | -------------------------------------------------------------------------------- /test/semaphore_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Andrew Mezoni 2 | // All rights reserved. 3 | // Subject to BSD 3-clause license. See file LICENSE_MEZONI. 4 | 5 | import "dart:async"; 6 | 7 | import "package:sync/semaphore.dart"; 8 | import "package:test/test.dart"; 9 | 10 | void main() { 11 | group("Semaphore", () { 12 | test("semaphore synchronisation", () async { 13 | var res1 = []; 14 | var res2 = []; 15 | Future action(List res, int milliseconds) { 16 | expect(res.length, 0, reason: "Not exlusive start"); 17 | res.length++; 18 | var completer = new Completer(); 19 | new Timer(new Duration(milliseconds: milliseconds), () { 20 | expect(res.length, 1, reason: "Not exlusive end"); 21 | res.length--; 22 | completer.complete(); 23 | }); 24 | 25 | return completer.future; 26 | } 27 | 28 | var s1 = new Semaphore(1); 29 | var s2 = new Semaphore(1); 30 | var list = []; 31 | for (var i = 0; i < 3; i++) { 32 | Future f(Semaphore s, List l) async { 33 | try { 34 | await s.acquire(); 35 | await action(l, 100); 36 | } finally { 37 | s.release(); 38 | } 39 | } 40 | 41 | list.add(new Future(() => f(s1, res1))); 42 | list.add(new Future(() => f(s2, res2))); 43 | } 44 | 45 | // Run concurrently 46 | await Future.wait(list); 47 | }); 48 | 49 | test("semaphore max count", () async { 50 | var list1 = []; 51 | var maxCount = 3; 52 | Future action(List list, int milliseconds) { 53 | expect(list.length <= maxCount, true, reason: "Not exlusive start"); 54 | list.length++; 55 | var completer = new Completer(); 56 | new Timer(new Duration(milliseconds: milliseconds), () { 57 | expect(list.length <= maxCount, true, reason: "Not exlusive end"); 58 | list.length--; 59 | completer.complete(); 60 | }); 61 | 62 | return completer.future; 63 | } 64 | 65 | var s1 = new Semaphore(3); 66 | var list = []; 67 | for (var i = 0; i < maxCount * 2; i++) { 68 | Future f(Semaphore s, List l) async { 69 | try { 70 | await s.acquire(); 71 | await action(l, 100); 72 | } finally { 73 | s.release(); 74 | } 75 | } 76 | 77 | list.add(new Future(() => f(s1, list1))); 78 | } 79 | 80 | // Run concurrently 81 | await Future.wait(list); 82 | }); 83 | }); 84 | } 85 | --------------------------------------------------------------------------------