├── .gitignore ├── license.txt ├── pom.xml ├── publish.py ├── readme.md └── src ├── main └── java │ └── com │ └── zarbosoft │ └── coroutines │ ├── Blocking.java │ ├── Cohelp.java │ ├── Coroutine.java │ ├── CriticalSection.java │ ├── Generator.java │ ├── ManualExecutor.java │ ├── NullaryBlocking.java │ ├── RWCriticalSection.java │ ├── SuspendableConsumer.java │ ├── SuspendableFunction.java │ ├── SuspendableSupplier.java │ └── WRCriticalSection.java └── test └── java └── com └── zarbosoft └── coroutines ├── TestCriticalSection.java ├── TestGeneral.java ├── TestRWCriticalSection.java └── TestWRCriticalSection.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite3 2 | *.iml 3 | *.swp 4 | __pycache__ 5 | env.json 6 | .idea -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018 rendaw 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | com.zarbosoft 5 | coroutines 6 | 0.1.1 7 | ${project.groupId}:${project.artifactId} 8 | Coroutines in Java 9 | https://github.com/rendaw/java-coroutines 10 | 11 | 12 | 2-Clause BSD License 13 | https://opensource.org/licenses/BSD-2-Clause 14 | 15 | 16 | 17 | 18 | rendaw 19 | spoo@zarbosoft.com 20 | Zarbosoft 21 | http://www.zarbsoft.com 22 | 23 | 24 | 25 | scm:git:git://github.com/rendaw/java-coroutines.git 26 | scm:git:ssh://github.com:rendaw/java-coroutines.git 27 | http://github.com/rendaw/java-coroutines/tree/master 28 | 29 | 30 | 31 | ossrh 32 | https://oss.sonatype.org/content/repositories/snapshots 33 | 34 | 35 | ossrh 36 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 37 | 38 | 39 | 40 | 41 | 42 | org.sonatype.ossindex.maven 43 | ossindex-maven-plugin 44 | 3.0.1 45 | 46 | 47 | audit-dependencies 48 | validate 49 | 50 | audit 51 | 52 | 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-compiler-plugin 58 | 3.7.0 59 | 60 | 1.8 61 | 1.8 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-dependency-plugin 67 | 2.3 68 | 69 | 70 | getClasspathFilenames 71 | 72 | properties 73 | 74 | 75 | 76 | 77 | 78 | org.apache.maven.plugins 79 | maven-antrun-plugin 80 | 81 | 82 | coroutines-instrument 83 | compile 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | run 94 | 95 | 96 | 97 | coroutines-instrument-tests 98 | test-compile 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | run 109 | 110 | 111 | 112 | 113 | 114 | org.apache.maven.plugins 115 | maven-jar-plugin 116 | 2.4 117 | 118 | 119 | 120 | com.zarbosoft.coroutinescore.instrument.JavaAgent 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | com.zarbosoft 131 | coroutines-core 132 | 0.0.10 133 | 134 | 135 | org.slf4j 136 | slf4j-api 137 | 1.7.24 138 | 139 | 140 | junit 141 | junit 142 | 4.12 143 | test 144 | 145 | 146 | org.hamcrest 147 | hamcrest-library 148 | 1.3 149 | test 150 | 151 | 152 | com.zarbosoft.rendaw 153 | common 154 | 1.0.11 155 | 156 | 157 | 158 | 159 | prerelease 160 | 161 | 162 | 163 | org.apache.maven.plugins 164 | maven-source-plugin 165 | 2.2.1 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | attach-sources 176 | 177 | jar-no-fork 178 | 179 | 180 | 181 | 182 | 183 | org.apache.maven.plugins 184 | maven-javadoc-plugin 185 | 3.0.1 186 | 187 | 1.8 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | attach-javadocs 197 | 198 | jar 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | release 208 | 209 | 210 | 211 | org.apache.maven.plugins 212 | maven-gpg-plugin 213 | 1.6 214 | 215 | 216 | sign-artifacts 217 | verify 218 | 219 | sign 220 | 221 | 222 | 223 | 224 | 42439C055D7D7C9D056A5648BF6FAF8B3E8AF437 225 | 226 | 227 | 228 | org.sonatype.plugins 229 | nexus-staging-maven-plugin 230 | 1.6.7 231 | true 232 | 233 | ossrh 234 | https://oss.sonatype.org/ 235 | true 236 | 237 | 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /publish.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import subprocess 3 | import logging 4 | import argparse 5 | import os 6 | import os.path 7 | import re 8 | import lxml.etree 9 | import pathlib 10 | 11 | logging.basicConfig(level=logging.DEBUG) 12 | 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument('version') 15 | args = parser.parse_args() 16 | if not re.match('\\d+\\.\\d+\\.\\d+', args.version): 17 | args.error('version must be in the format N.N.N') 18 | 19 | java = '/usr/lib/jvm/java-11-openjdk' 20 | 21 | jenv = os.environ.copy() 22 | jenv['JAVA_HOME'] = java 23 | subprocess.check_call(['mvn', 'clean', 'verify', '-P', 'prerelease'], env=jenv) 24 | 25 | if subprocess.call([ 26 | 'git', 'diff', '--exit-code', '--quiet', 'HEAD' 27 | ]) != 0: 28 | raise RuntimeError('Working directory must be clean.') 29 | 30 | 31 | def replace(path, *replacements): 32 | with open(path, 'r') as source: 33 | text = source.read() 34 | for a, b in replacements: 35 | text = re.sub(a, b, text, flags=re.S | re.M) 36 | temp = '{}.1'.format(path) 37 | with open(temp, 'w') as dest: 38 | dest.write(text) 39 | os.rename(temp, path) 40 | 41 | 42 | artifacts = [] 43 | 44 | # Set publishing version numbers of all modules in this pom to the new version 45 | for root, dirs, files in os.walk('.'): 46 | if 'classes-9' in dirs: 47 | classes = (pathlib.Path(root) / 'classes') 48 | found = next(classes.glob('**/*.class'), None) 49 | if found: 50 | found = found.relative_to(classes) 51 | vers = subprocess.check_output( 52 | [ 53 | java + '/bin/javap', 54 | '-verbose', 55 | str(found.parent / found.stem).replace('/', '.') 56 | ], 57 | cwd=classes 58 | ).decode('utf-8') 59 | if 'major version: 52' not in vers: 60 | raise RuntimeError( 61 | 'Multiversion jar, but {} is not java 8'.format( 62 | found 63 | ) 64 | ) 65 | for f in files: 66 | if f != 'pom.xml': 67 | continue 68 | path = os.path.join(root, f) 69 | doc = lxml.etree.parse(path) 70 | root = doc.getroot() 71 | pomver = root.find('version', namespaces=root.nsmap) 72 | if pomver.text == '0.0.0': 73 | continue 74 | artifacts.append(root.find('artifactId', namespaces=root.nsmap).text) 75 | pomver.text = args.version 76 | doc.write(path) 77 | 78 | # Update version numbers in docs 79 | if os.path.exists('readme.md'): 80 | replace('readme.md', *[ 81 | [ 82 | '{}([^<]*)[^<]+'.format(artifact), # noqa 83 | '{}\\1{}'.format(artifact, args.version), # noqa 84 | ] for artifact in artifacts 85 | ]) 86 | 87 | subprocess.check_call([ 88 | 'git', 'commit', '-a', '-m', 'Update version: {}'.format(args.version)]) 89 | 90 | # Push tag 91 | subprocess.check_call(['git', 'tag', 'v' + args.version]) 92 | subprocess.check_call(['git', 'push', '--tags']) 93 | subprocess.check_call(['git', 'push']) 94 | 95 | # Build and publish 96 | subprocess.check_call([ 97 | 'mvn', 'clean', 'deploy', '-P', 'prerelease,release'], env=jenv) 98 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Coroutines 2 | 3 | This is a fully functional framework/tool-agnostic coroutines implementation for Java. 4 | 5 | Now supports Java 9! 6 | 7 | ``` java 8 | Coroutine co = new Coroutine(() -> { 9 | System.out.println("Taking a break!\n"); 10 | Coroutine.yield(); 11 | System.out.println("Break time over.\n"); 12 | System.out.format("%s\n", asyncMethod()); 13 | }); 14 | 15 | co.process(); 16 | System.out.format("1st break!\n"); 17 | co.process(); 18 | System.out.format("2nd break!\n"); 19 | co.process(); 20 | System.out.format("Coroutine done!\n"); 21 | 22 | public static int asyncMethod() throws SuspendExecution { 23 | System.out.println("Need another break.\n"); 24 | Coroutine.yield(); 25 | System.out.println("Okay, let's get started.\n"); 26 | return 3; 27 | } 28 | ``` 29 | 30 | ``` 31 | Taking a break! 32 | 1st break! 33 | Break time over. 34 | Need another break. 35 | 2nd break! 36 | Okay, let's get started. 37 | 3 38 | Coroutine done! 39 | ``` 40 | 41 | ## Maven 42 | 43 | ``` xml 44 | 45 | com.zarbosoft 46 | coroutines 47 | 0.1.1 48 | 49 | ``` 50 | 51 | ## Usage 52 | 53 | A `Coroutine` runs a `SuspendableRunnable`, similar to how `Thread` runs a `Runnable`. When the coroutine's runnable 54 | suspends it can be started again from the point it suspended by another call to `process`. 55 | 56 | Methods that throw `SuspendExcecution` are suspendable. Suspendable methods can be called from other suspendable 57 | methods. Coroutines uses `SuspendExecution` exceptions to do the suspension, but don't worry about running 58 | suspendable methods in `try` blocks - as long as you don't catch `SuspendExecution` explicitly there's no problem. 59 | 60 | Running suspendable code takes a few additional steps. Follow 61 | [these instructions](https://github.com/rendaw/java-coroutines-core#running-your-code) to get going. 62 | 63 | ## Additional features 64 | 65 | Aside from suspending and resuming, you can... 66 | 67 | ### Inject values and exceptions into coroutines when resuming 68 | 69 | Make async callback-based apis synchronous, or use them as generators. 70 | 71 | ``` java 72 | Coroutine c = Coroutine.getActiveCoroutine(); 73 | byte[] data = Coroutine.yieldThen(() -> { 74 | slowOperationWithCallback(result -> c.process(result)); 75 | }); 76 | display(data); 77 | work2(data); 78 | ``` 79 | 80 | ### Run blocking code in a coroutine 81 | 82 | ``` java 83 | static ExecutorService executor = ...; 84 | 85 | static void asyncCode() throws SuspendExecution { 86 | ... 87 | Cohelp.unblock(executor, () -> { 88 | double value = 0; 89 | for (long x = 0; x < Math.pow(10, 10); ++x) { 90 | value += 1; 91 | } 92 | }); 93 | ... 94 | } 95 | ``` 96 | 97 | ### Run a coroutine in blocking code 98 | 99 | ``` java 100 | public static void main(String[] args) { 101 | Cohelp.block(() -> { 102 | asyncDownloadValues(); 103 | asyncRunProcess(); 104 | }); 105 | } 106 | ``` 107 | 108 | ### Turn a CompletableFuture into a suspending async call 109 | 110 | ``` java 111 | public static void asyncCode() throws SuspendExecution { 112 | ... 113 | JsonNode response = Cohelp.unblock(rpc.call("get_history", "me", "shadowhawk4949")); 114 | ... 115 | } 116 | ``` 117 | 118 | ### Sleep 119 | 120 | ``` java 121 | static ScheduledExecutorService executor = ...; 122 | 123 | public static void asyncCode() throws SuspendExecution { 124 | System.out.println("sleeping"); 125 | Cohelp.sleep(executor, 1, MINUTES); 126 | System.out.println("waking"); 127 | } 128 | ``` 129 | 130 | ### Timers 131 | 132 | ``` java 133 | static ScheduledExecutorService executor = ...; 134 | 135 | public static void main(String[] args) { 136 | ... 137 | Cohelp.timer(executor, 1, HOURS, () -> { 138 | System.out.println(downloadWeather()); 139 | }); 140 | ... 141 | } 142 | ``` 143 | 144 | ### Asynchronous critical sections (think async synchronized blocks) 145 | 146 | ``` java 147 | CriticalSection critical = new CriticalSection<>() { 148 | private volatile int counter = 0; 149 | 150 | @Override 151 | protected int execute(int argument) throws SuspendExecution { 152 | ... 153 | return ++counter; 154 | } 155 | }; 156 | 157 | public int accessService(int argument) { 158 | return critical.execute(argument); 159 | } 160 | ``` 161 | 162 | ### And more! (but not much more) 163 | 164 | This is a wrapper around [coroutines-core](https://github.com/rendaw/java-coroutines-core) providing some utilities 165 | to improve compatibility with other libraries and make it easier to use. If you want a minimal coroutines 166 | implementation, see that project. 167 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/Blocking.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | /** 4 | * Like Supplier, but can raise a checked exception. 5 | * 6 | * @param 7 | */ 8 | @FunctionalInterface 9 | public interface Blocking { 10 | T run() throws Exception; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/Cohelp.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | import com.zarbosoft.coroutinescore.SuspendableRunnable; 5 | import com.zarbosoft.rendaw.common.Assertion; 6 | import org.slf4j.Logger; 7 | 8 | import java.time.*; 9 | import java.time.temporal.ChronoUnit; 10 | import java.time.temporal.Temporal; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import java.util.concurrent.*; 14 | 15 | import static com.zarbosoft.rendaw.common.Common.uncheck; 16 | import static java.time.temporal.ChronoUnit.DAYS; 17 | import static java.time.temporal.ChronoUnit.WEEKS; 18 | import static org.slf4j.LoggerFactory.getLogger; 19 | 20 | public class Cohelp { 21 | public static Logger logger = getLogger("cohelp"); 22 | 23 | /** 24 | * Standard uncaught error resolution. 25 | * 26 | * @param executor 27 | * @param e 28 | */ 29 | public static void fatal(final ExecutorService executor, final Throwable e) { 30 | logger.error("Uncaught error in executor; shutting down", e); 31 | executor.shutdown(); 32 | } 33 | 34 | /** 35 | * Pause coroutine execution for the given time. 36 | * 37 | * @param executor Coroutine is resumed in this executor after the duration has passed. 38 | * @param time Time to sleep; multiple of unit 39 | * @param unit 40 | * @throws SuspendExecution 41 | */ 42 | public static void sleep( 43 | final ScheduledExecutorService executor, final int time, final TimeUnit unit 44 | ) throws SuspendExecution { 45 | final Coroutine coroutine = Coroutine.getActiveCoroutine(); 46 | Coroutine.yieldThen(() -> { 47 | executor.schedule(new Runnable() { 48 | @Override 49 | public void run() { 50 | try { 51 | coroutine.process(null); 52 | } catch (final Throwable e) { 53 | fatal(executor, e); 54 | } 55 | } 56 | }, time, unit); 57 | }); 58 | } 59 | 60 | /** 61 | * Run synchronous work asynchronously in a coroutine by offloading it to another thread. 62 | * 63 | * @param executor Coroutine is resumed in this executor after the work is complete. 64 | * @param runnable Work to offload. 65 | * @param Work return value. 66 | * @return Return value from work. 67 | * @throws SuspendExecution 68 | */ 69 | public static T unblock( 70 | final ExecutorService executor, final Blocking runnable 71 | ) throws SuspendExecution { 72 | final Coroutine self = Coroutine.getActiveCoroutine(); 73 | return Coroutine.yieldThen(() -> { 74 | executor.submit(() -> { 75 | try { 76 | self.process(runnable.run()); 77 | } catch (final Exception e) { 78 | self.processThrow(uncheck(e)); 79 | } 80 | }); 81 | }); 82 | } 83 | 84 | /** 85 | * Run synchronous work asynchronously in a coroutine by offloading it to another thread. 86 | * 87 | * @param executor Coroutine is resumed in this executor after the work is complete. 88 | * @param runnable Work to offload. 89 | * @throws SuspendExecution 90 | */ 91 | public static void unblock( 92 | final ExecutorService executor, final NullaryBlocking runnable 93 | ) throws SuspendExecution { 94 | final Coroutine self = Coroutine.getActiveCoroutine(); 95 | Coroutine.yieldThen(() -> { 96 | executor.submit(() -> { 97 | try { 98 | runnable.run(); 99 | self.process(null); 100 | } catch (final Exception e) { 101 | self.processThrow(uncheck(e)); 102 | } 103 | }); 104 | }); 105 | } 106 | 107 | /** 108 | * Asynchronously wait for a future to complete. 109 | * 110 | * @param future 111 | * @param 112 | * @return Result of future. 113 | * @throws SuspendExecution 114 | */ 115 | public static T unblock(final CompletableFuture future) throws SuspendExecution { 116 | final Coroutine self = Coroutine.getActiveCoroutine(); 117 | return Coroutine.yieldThen(() -> { 118 | future.thenAccept(v -> { 119 | self.process(v); 120 | }).exceptionally(e -> { 121 | self.processThrow(uncheck((Exception) e)); 122 | return null; 123 | }); 124 | }); 125 | } 126 | 127 | /** 128 | * Run asynchronous work synchronously. 129 | * 130 | * @param runnable Asynchronous work 131 | * @param 132 | * @return Return value of asynchronous work 133 | */ 134 | public static T block(final SuspendableSupplier runnable) { 135 | final CompletableFuture blocker = new CompletableFuture<>(); 136 | new Coroutine(new SuspendableRunnable() { 137 | @Override 138 | public void run() throws SuspendExecution { 139 | try { 140 | blocker.complete(runnable.get()); 141 | } catch (final Exception e) { 142 | blocker.completeExceptionally(e); 143 | } 144 | } 145 | }).process(null); 146 | while (true) { 147 | try { 148 | return blocker.get(); 149 | } catch (final InterruptedException e) { 150 | } catch (final ExecutionException e) { 151 | throw uncheck((RuntimeException) e.getCause()); 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * Block a synchronous method on asynchronous work. Note: Asynchronous work must happen in a different thread 158 | * (coroutines must be resumed by some external process). 159 | * 160 | * @param runnable Asynchronous work 161 | */ 162 | public static void block(final SuspendableRunnable runnable) { 163 | final CompletableFuture blocker = new CompletableFuture<>(); 164 | new Coroutine(new SuspendableRunnable() { 165 | @Override 166 | public void run() throws SuspendExecution { 167 | try { 168 | runnable.run(); 169 | blocker.complete(null); 170 | } catch (final Exception e) { 171 | blocker.completeExceptionally(e); 172 | } 173 | } 174 | }).process(null); 175 | while (true) { 176 | try { 177 | blocker.get(); 178 | return; 179 | } catch (final InterruptedException e) { 180 | } catch (final ExecutionException e) { 181 | throw uncheck((RuntimeException) e.getCause()); 182 | } 183 | } 184 | } 185 | 186 | private static TimeUnit timeUnit(final ChronoUnit unit) { 187 | switch (unit) { 188 | case NANOS: 189 | return TimeUnit.NANOSECONDS; 190 | case MICROS: 191 | return TimeUnit.MICROSECONDS; 192 | case MILLIS: 193 | return TimeUnit.MILLISECONDS; 194 | case SECONDS: 195 | return TimeUnit.SECONDS; 196 | case MINUTES: 197 | return TimeUnit.MINUTES; 198 | case HOURS: 199 | return TimeUnit.HOURS; 200 | case DAYS: 201 | return TimeUnit.DAYS; 202 | default: 203 | throw new Assertion(); 204 | } 205 | } 206 | 207 | /** 208 | * Run an asynchronous method at a fixed interval. The method will not be invoked multiple times simultaneously if 209 | * the method takes longer than the interval to complete. If an error propagates out of the runnable it is logged 210 | * and the executor is shut down. 211 | * 212 | * The first execution of the runnable will be after 1 interval has elapsed from the call to repeat. 213 | * 214 | * @param executor Method is run in this executor. 215 | * @param time Interval, multiple of unit. 216 | * @param unit 217 | * @param runnable The method to run periodically. 218 | */ 219 | public static void repeat( 220 | final ScheduledExecutorService executor, 221 | final int time, 222 | final ChronoUnit unit, 223 | final SuspendableRunnable runnable 224 | ) { 225 | executor.scheduleAtFixedRate(new Runnable() { 226 | CompletableFuture lastDone = null; 227 | 228 | @Override 229 | public void run() { 230 | final CompletableFuture done = this.lastDone; 231 | if (done != null && !done.isDone()) 232 | return; 233 | this.lastDone = new CompletableFuture<>(); 234 | try { 235 | new Coroutine(new SuspendableRunnable() { 236 | @Override 237 | public void run() throws SuspendExecution { 238 | try { 239 | runnable.run(); 240 | } catch (final Throwable e) { 241 | fatal(executor, e); 242 | } finally { 243 | lastDone.complete(null); 244 | } 245 | } 246 | }).process(null); 247 | } catch (final Throwable e) { 248 | fatal(executor, e); 249 | } 250 | } 251 | }, time, time, timeUnit(unit)); 252 | } 253 | 254 | /** 255 | * Defines a schedule to use with scheduleTimer 256 | * 257 | * @param temporal type (LocalDateTime or ZonedDateTime) 258 | */ 259 | public interface Schedule { 260 | public T now(); 261 | 262 | /** 263 | * Get the time of an occurance relative to the current time 264 | * 265 | * @param now current time 266 | * @param offset occurances after now; if 0 the occurance immediately before now 267 | * @return moment of the occurance 268 | */ 269 | public T get(T now, int offset); 270 | } 271 | 272 | /** 273 | * Run the runnable according to the schedule. Can handle complex schedules 274 | * (based around calendar or regional clocks). 275 | * 276 | * @param executor Method is run in this executor 277 | * @param schedule The schedule to run on 278 | * @param runnable The method to run 279 | * @param 280 | */ 281 | public static void calendarRepeat( 282 | final ScheduledExecutorService executor, final Schedule schedule, final SuspendableRunnable runnable 283 | ) { 284 | final Runnable inner = new Runnable() { 285 | T last; 286 | 287 | @Override 288 | public void run() { 289 | final T now = schedule.now(); 290 | final List times = Arrays.asList(schedule.get(now, 0), schedule.get(now, 1)); 291 | final Duration scale = Duration.between(times.get(0), times.get(1)); 292 | final Duration epsilon = scale.dividedBy(100); 293 | for (final T next : times) { 294 | final Duration until = Duration.between(now, next); 295 | if (until.abs().minus(epsilon).isNegative() && 296 | (last == null || Duration.between(last, now).toMinutes() > 30)) { 297 | new Coroutine(new SuspendableRunnable() { 298 | @Override 299 | public void run() throws SuspendExecution { 300 | try { 301 | runnable.run(); 302 | } catch (final Throwable e) { 303 | fatal(executor, e); 304 | } 305 | } 306 | }).process(null); 307 | last = now; 308 | break; 309 | } 310 | if (until.isNegative()) 311 | continue; 312 | executor.schedule(this, until.toMillis(), TimeUnit.MILLISECONDS); 313 | break; 314 | } 315 | } 316 | }; 317 | inner.run(); 318 | } 319 | 320 | public static void scheduleDailyUTC( 321 | final ScheduledExecutorService executor, final LocalTime time, final SuspendableRunnable runnable 322 | ) { 323 | calendarRepeat(executor, new Schedule() { 324 | @Override 325 | public ZonedDateTime now() { 326 | return ZonedDateTime.now(ZoneOffset.UTC); 327 | } 328 | 329 | @Override 330 | public ZonedDateTime get(final ZonedDateTime now, int offset) { 331 | final ZonedDateTime basis = now.toLocalDate().atTime(time).atZone(ZoneOffset.UTC); 332 | if (basis.isAfter(now)) { 333 | offset -= 1; 334 | } 335 | return basis.plus(offset, DAYS); 336 | } 337 | }, runnable); 338 | } 339 | 340 | public static void scheduleWeeklyUTC( 341 | final ScheduledExecutorService executor, 342 | final DayOfWeek day, 343 | final LocalTime time, 344 | final SuspendableRunnable runnable 345 | ) { 346 | calendarRepeat(executor, new Schedule() { 347 | @Override 348 | public ZonedDateTime now() { 349 | return ZonedDateTime.now(ZoneOffset.UTC); 350 | } 351 | 352 | @Override 353 | public ZonedDateTime get(final ZonedDateTime now, int offset) { 354 | final LocalDate today = now.toLocalDate(); 355 | final ZonedDateTime basis = today 356 | .minus(today.getDayOfWeek().getValue() - day.getValue(), DAYS) 357 | .atTime(time) 358 | .atZone(ZoneOffset.UTC); 359 | if (basis.isAfter(now)) { 360 | offset -= 1; 361 | } 362 | return basis.plus(offset, WEEKS); 363 | } 364 | }, runnable); 365 | } 366 | 367 | /** 368 | * Run an asynchronous method in an executor. 369 | * 370 | * @param executor Method is run in this executor. 371 | * @param runnable Method to run. 372 | */ 373 | public static void submit(final ExecutorService executor, final SuspendableRunnable runnable) { 374 | executor.submit(() -> { 375 | try { 376 | new Coroutine(new SuspendableRunnable() { 377 | @Override 378 | public void run() throws SuspendExecution { 379 | try { 380 | runnable.run(); 381 | } catch (final Throwable e) { 382 | fatal(executor, e); 383 | } 384 | } 385 | }).process(null); 386 | } catch (final Throwable e) { 387 | fatal(executor, e); 388 | } 389 | }); 390 | } 391 | 392 | /** 393 | * Run an asynchronous method in an executor after a period. 394 | * 395 | * @param executor 396 | * @param executor Method is run in this executor. 397 | * @param time 398 | * @param unit 399 | * @param runnable Method to run. 400 | * @return 401 | */ 402 | public static ScheduledFuture delay( 403 | final ScheduledExecutorService executor, 404 | final int time, 405 | final ChronoUnit unit, 406 | final SuspendableRunnable runnable 407 | ) { 408 | return executor.schedule(() -> { 409 | try { 410 | new Coroutine(new SuspendableRunnable() { 411 | @Override 412 | public void run() throws SuspendExecution { 413 | try { 414 | runnable.run(); 415 | } catch (final Throwable e) { 416 | fatal(executor, e); 417 | } 418 | } 419 | }).process(null); 420 | } catch (final Throwable e) { 421 | fatal(executor, e); 422 | } 423 | }, time, timeUnit(unit)); 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/Coroutine.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | import com.zarbosoft.coroutinescore.SuspendableRunnable; 5 | 6 | /** 7 | * Create a coroutine. A coroutine is roughly a method that can be paused at any point and then resumed from that 8 | * point. Additionally, any call at any depth in the call tree of the method can suspend. This is useful for 9 | * multiplexing multiple tasks in a single thread. 10 | */ 11 | public class Coroutine { 12 | private static class InnerCoroutine extends com.zarbosoft.coroutinescore.Coroutine { 13 | private final Coroutine outer; 14 | 15 | public InnerCoroutine(final Coroutine outer, final SuspendableRunnable runnable) { 16 | super(runnable); 17 | this.outer = outer; 18 | } 19 | 20 | public InnerCoroutine(final Coroutine outer, final SuspendableRunnable runnable, final int stackSize) { 21 | super(runnable, stackSize); 22 | this.outer = outer; 23 | } 24 | } 25 | 26 | private final InnerCoroutine inner; 27 | Object inValue = null; 28 | Runnable runAfter = null; 29 | private RuntimeException inException = null; 30 | 31 | /** 32 | * Creates a coroutine for the provided method. Nothing is run until process is called. 33 | * 34 | * @param runnable 35 | */ 36 | public Coroutine(final SuspendableRunnable runnable) { 37 | inner = new InnerCoroutine(this, runnable); 38 | } 39 | 40 | public Coroutine(final SuspendableRunnable runnable, final int stackSize) { 41 | inner = new InnerCoroutine(this, runnable, stackSize); 42 | } 43 | 44 | /** 45 | * Start or resume the coroutine. 46 | */ 47 | public final void process() { 48 | process(null); 49 | } 50 | 51 | /** 52 | * Start or resume the coroutine. 53 | * 54 | * @param value This will be returned from the call to yeild if the coroutine was suspended. Otherwise, ignored. 55 | */ 56 | public final void process(final Object value) { 57 | inValue = value; 58 | inner.run(); 59 | if (runAfter != null) { 60 | final Runnable runAfter = this.runAfter; 61 | this.runAfter = null; 62 | runAfter.run(); 63 | } 64 | } 65 | 66 | /** 67 | * Resume the coroutine by raising an exception from the suspension point. 68 | * 69 | * @param exception Exception to raise. 70 | */ 71 | public final void processThrow(final RuntimeException exception) { 72 | inException = exception; 73 | inner.run(); 74 | if (runAfter != null) { 75 | final Runnable runAfter = this.runAfter; 76 | this.runAfter = null; 77 | runAfter.run(); 78 | } 79 | } 80 | 81 | /** 82 | * Suspend the coroutine running in the current thread. 83 | * 84 | * @throws SuspendExecution 85 | */ 86 | public static T yield() throws SuspendExecution { 87 | com.zarbosoft.coroutinescore.Coroutine.yield(); 88 | final Coroutine self = getActiveCoroutine(); 89 | if (self.inException != null) { 90 | final RuntimeException e = self.inException; 91 | self.inException = null; 92 | throw e; 93 | } 94 | return (T) self.inValue; 95 | } 96 | 97 | /** 98 | * Suspend the coroutine and run the method after the suspension is complete. This is useful when scheduling 99 | * the coroutine to be run on another thread which would otherwise cause a race condition (suspension completion vs 100 | * resumption start). 101 | *

102 | * Any exceptions that escape the runnable will propagate to the coroutine runner rather than raise an error 103 | * in the coroutine. This is typically not the desired behavior, so if an exception might occur you should 104 | * catch it and send it back to the coroutine (perhaps submitting to an executor to prevent stack growth). 105 | * 106 | * @param runAfter 107 | * @param 108 | * @return Value supplied to Coroutine.process when resumed. 109 | * @throws SuspendExecution 110 | */ 111 | public static T yieldThen(final Runnable runAfter) throws SuspendExecution { 112 | final Coroutine self = getActiveCoroutine(); 113 | self.runAfter = runAfter; 114 | com.zarbosoft.coroutinescore.Coroutine.yield(); 115 | self.runAfter = null; 116 | if (self.inException != null) { 117 | final RuntimeException e = self.inException; 118 | self.inException = null; 119 | throw e; 120 | } 121 | return (T) self.inValue; 122 | } 123 | 124 | /** 125 | * May only be called while executing a coroutine, from the same thread. 126 | * 127 | * @return The current coroutine. 128 | */ 129 | public static Coroutine getActiveCoroutine() { 130 | return ((InnerCoroutine) com.zarbosoft.coroutinescore.Coroutine.getActiveCoroutine()).outer; 131 | } 132 | 133 | /** 134 | * @return true if coroutine is finished 135 | */ 136 | public boolean isFinished() { 137 | return inner.getState() == com.zarbosoft.coroutinescore.Coroutine.State.FINISHED; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/CriticalSection.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | 5 | import java.util.ArrayDeque; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.locks.ReentrantLock; 8 | 9 | /** 10 | * Create a critical section around a method that will stop coroutines rather than blocking them. 11 | */ 12 | public class CriticalSection { 13 | private final ReentrantLock lock = new ReentrantLock(); 14 | private boolean locked = false; 15 | private final ArrayDeque queue = new ArrayDeque<>(); 16 | 17 | /** 18 | * Run the method if no other coroutine is currently executing it. Otherwise suspend, and resume when the other 19 | * coroutine has finished. 20 | * 21 | * @param executor Worker to resume coroutine on if this suspends. 22 | * @return Wrapped method's return. 23 | * @throws SuspendExecution 24 | */ 25 | public R call(final ExecutorService executor, SuspendableSupplier method) throws SuspendExecution { 26 | lock.lock(); 27 | if (locked) { 28 | Coroutine coroutine = Coroutine.getActiveCoroutine(); 29 | return Coroutine.yieldThen(() -> { 30 | queue.add(new Waiting(executor, coroutine, method)); 31 | lock.unlock(); 32 | }); 33 | } 34 | locked = true; 35 | lock.unlock(); 36 | 37 | try { 38 | return method.get(); 39 | } finally { 40 | iterate(); 41 | } 42 | } 43 | 44 | private void iterate() throws SuspendExecution { 45 | lock.lock(); 46 | Waiting next = queue.poll(); 47 | if (next == null) { 48 | locked = false; 49 | } 50 | lock.unlock(); 51 | if (next != null) { 52 | Cohelp.submit(next.executor, () -> { 53 | try { 54 | final Object out = next.method.get(); 55 | iterate(); 56 | next.coroutine.process(out); 57 | } catch (final RuntimeException e) { 58 | iterate(); 59 | next.coroutine.processThrow(e); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | static class Waiting { 66 | public final ExecutorService executor; 67 | public final Coroutine coroutine; 68 | public final SuspendableSupplier method; 69 | 70 | public Waiting(final ExecutorService executor, final Coroutine coroutine, final SuspendableSupplier method) { 71 | this.executor = executor; 72 | this.coroutine = coroutine; 73 | this.method = method; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/Generator.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | import com.zarbosoft.coroutinescore.SuspendableRunnable; 5 | 6 | import java.util.Iterator; 7 | import java.util.Spliterator; 8 | import java.util.Spliterators; 9 | import java.util.stream.Stream; 10 | import java.util.stream.StreamSupport; 11 | 12 | import static com.zarbosoft.coroutines.Coroutine.yield; 13 | 14 | public class Generator { 15 | private final static Object END = new Object(); 16 | 17 | private Generator() { 18 | } 19 | 20 | private T value; 21 | 22 | public void yieldValue(T value) throws SuspendExecution { 23 | this.value = value; 24 | yield(); 25 | } 26 | 27 | public static Stream stream(SuspendableConsumer> runnable) { 28 | Generator g = new Generator(); 29 | Coroutine c = new Coroutine(new SuspendableRunnable() { 30 | @Override 31 | public void run() throws SuspendExecution { 32 | runnable.apply(g); 33 | } 34 | }); 35 | return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Iterator() { 36 | @Override 37 | public boolean hasNext() { 38 | return !c.isFinished(); 39 | } 40 | 41 | @Override 42 | public Object next() { 43 | c.process(); 44 | if (c.isFinished()) 45 | return END; 46 | return g.value; 47 | } 48 | }, Spliterator.ORDERED), false).filter(v -> v != END); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/ManualExecutor.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | import com.zarbosoft.rendaw.common.Assertion; 5 | 6 | import java.time.Duration; 7 | import java.time.LocalDateTime; 8 | import java.time.ZoneOffset; 9 | import java.time.temporal.ChronoUnit; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.Comparator; 13 | import java.util.List; 14 | import java.util.concurrent.*; 15 | 16 | public class ManualExecutor implements ScheduledExecutorService { 17 | private static class Scheduled { 18 | final LocalDateTime at; 19 | final Runnable runnable; 20 | final Duration repeat; 21 | 22 | private Scheduled(final LocalDateTime at, final Runnable runnable, final Duration repeat) { 23 | this.at = at; 24 | this.runnable = runnable; 25 | this.repeat = repeat; 26 | } 27 | } 28 | 29 | public List scheduled = new ArrayList<>(); 30 | public LocalDateTime now = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC); 31 | 32 | private void sortScheduled() { 33 | this.scheduled.sort(new Comparator() { 34 | @Override 35 | public int compare(final Scheduled o1, final Scheduled o2) { 36 | return o1.at.compareTo(o2.at); 37 | } 38 | }); 39 | } 40 | 41 | private ScheduledFuture push(final Scheduled event) { 42 | this.scheduled.add(event); 43 | sortScheduled(); 44 | return new ScheduledFuture() { 45 | @Override 46 | public long getDelay(final TimeUnit unit) { 47 | throw new AssertionError(); 48 | } 49 | 50 | @Override 51 | public int compareTo(final Delayed o) { 52 | throw new AssertionError(); 53 | } 54 | 55 | @Override 56 | public boolean cancel(final boolean mayInterruptIfRunning) { 57 | return scheduled.remove(event); 58 | } 59 | 60 | @Override 61 | public boolean isCancelled() { 62 | return scheduled.contains(event); 63 | } 64 | 65 | @Override 66 | public boolean isDone() { 67 | throw new AssertionError(); 68 | } 69 | 70 | @Override 71 | public V get() throws InterruptedException, ExecutionException { 72 | throw new AssertionError(); 73 | } 74 | 75 | @Override 76 | public V get( 77 | final long timeout, final TimeUnit unit 78 | ) throws InterruptedException, ExecutionException, TimeoutException { 79 | throw new AssertionError(); 80 | } 81 | }; 82 | } 83 | 84 | public void advance(final Duration amount) throws SuspendExecution { 85 | final Coroutine coroutine = Coroutine.getActiveCoroutine(); 86 | Coroutine.yieldThen(() -> { 87 | now = now.plus(amount); 88 | final List reschedule = new ArrayList<>(); 89 | for (final Scheduled event : scheduled) { 90 | if (event.at.compareTo(now) <= 0) { 91 | event.runnable.run(); 92 | if (event.repeat != null) 93 | reschedule.add(event); 94 | } else { 95 | reschedule.add(event); 96 | } 97 | } 98 | scheduled = reschedule; 99 | sortScheduled(); 100 | submit(() -> { 101 | try { 102 | coroutine.process(null); 103 | } catch (final Throwable e) { 104 | this.shutdown(); 105 | } 106 | }); 107 | }); 108 | } 109 | 110 | private ChronoUnit toChronoUnit(final TimeUnit unit) { 111 | switch (unit) { 112 | case NANOSECONDS: 113 | case MICROSECONDS: 114 | return ChronoUnit.MICROS; 115 | case MILLISECONDS: 116 | return ChronoUnit.MILLIS; 117 | case SECONDS: 118 | return ChronoUnit.SECONDS; 119 | case MINUTES: 120 | return ChronoUnit.MINUTES; 121 | case HOURS: 122 | return ChronoUnit.HOURS; 123 | case DAYS: 124 | return ChronoUnit.DAYS; 125 | default: 126 | throw new AssertionError(); 127 | } 128 | } 129 | 130 | @Override 131 | public ScheduledFuture schedule(final Runnable command, final long delay, final TimeUnit unit) { 132 | return push(new Scheduled(now.plus(delay, toChronoUnit(unit)), command, null)); 133 | } 134 | 135 | @Override 136 | public ScheduledFuture schedule(final Callable callable, final long delay, final TimeUnit unit) { 137 | return push(new Scheduled(now.plus(delay, toChronoUnit(unit)), () -> { 138 | try { 139 | callable.call(); 140 | } catch (final Exception e) { 141 | e.printStackTrace(); 142 | } 143 | }, null)); 144 | } 145 | 146 | @Override 147 | public ScheduledFuture scheduleAtFixedRate( 148 | final Runnable command, final long initialDelay, final long period, final TimeUnit unit 149 | ) { 150 | return push(new Scheduled(now.plus(initialDelay, toChronoUnit(unit)), 151 | command, 152 | Duration.of(period, toChronoUnit(unit)) 153 | )); 154 | } 155 | 156 | @Override 157 | public ScheduledFuture scheduleWithFixedDelay( 158 | final Runnable command, final long initialDelay, final long delay, final TimeUnit unit 159 | ) { 160 | return push(new Scheduled(now.plus(initialDelay, toChronoUnit(unit)), 161 | command, 162 | Duration.of(delay, toChronoUnit(unit)) 163 | )); 164 | } 165 | 166 | @Override 167 | public void shutdown() { 168 | throw new AssertionError("Shutdown"); 169 | } 170 | 171 | @Override 172 | public List shutdownNow() { 173 | throw new Assertion(); 174 | } 175 | 176 | @Override 177 | public boolean isShutdown() { 178 | throw new Assertion(); 179 | } 180 | 181 | @Override 182 | public boolean isTerminated() { 183 | throw new Assertion(); 184 | } 185 | 186 | @Override 187 | public boolean awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException { 188 | throw new Assertion(); 189 | } 190 | 191 | @Override 192 | public Future submit(final Callable task) { 193 | throw new Assertion(); 194 | } 195 | 196 | @Override 197 | public Future submit(final Runnable task, final T result) { 198 | throw new Assertion(); 199 | } 200 | 201 | @Override 202 | public Future submit(final Runnable task) { 203 | task.run(); 204 | return null; 205 | } 206 | 207 | @Override 208 | public List> invokeAll(final Collection> tasks) throws InterruptedException { 209 | throw new Assertion(); 210 | } 211 | 212 | @Override 213 | public List> invokeAll( 214 | final Collection> tasks, final long timeout, final TimeUnit unit 215 | ) throws InterruptedException { 216 | throw new Assertion(); 217 | } 218 | 219 | @Override 220 | public T invokeAny(final Collection> tasks) throws InterruptedException, ExecutionException { 221 | throw new Assertion(); 222 | } 223 | 224 | @Override 225 | public T invokeAny( 226 | final Collection> tasks, final long timeout, final TimeUnit unit 227 | ) throws InterruptedException, ExecutionException, TimeoutException { 228 | throw new Assertion(); 229 | } 230 | 231 | @Override 232 | public void execute(final Runnable command) { 233 | command.run(); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/NullaryBlocking.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | /** 4 | * Like runnable but can raise a checked exception. 5 | */ 6 | @FunctionalInterface 7 | public interface NullaryBlocking { 8 | void run() throws Exception; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/RWCriticalSection.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | import com.zarbosoft.coroutinescore.SuspendableRunnable; 5 | 6 | import java.util.ArrayDeque; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.locks.ReentrantLock; 9 | 10 | /** 11 | * Prioritizes reads 12 | */ 13 | public class RWCriticalSection { 14 | final ReentrantLock lock = new ReentrantLock(); 15 | final static int STATE_WRITING = -1; 16 | final static int STATE_UNLOCKED = 0; 17 | int state = STATE_UNLOCKED; 18 | ArrayDeque readQueue = new ArrayDeque<>(); 19 | final ArrayDeque writeQueue = new ArrayDeque<>(); 20 | 21 | public R read(final ExecutorService executor, SuspendableSupplier method) throws SuspendExecution { 22 | lock.lock(); 23 | if (state == STATE_WRITING) { 24 | Coroutine coroutine = Coroutine.getActiveCoroutine(); 25 | return Coroutine.yieldThen(() -> { 26 | readQueue.add(new CriticalSection.Waiting(executor, coroutine, method)); 27 | lock.unlock(); 28 | }); 29 | } 30 | state += 1; 31 | lock.unlock(); 32 | 33 | try { 34 | return method.get(); 35 | } finally { 36 | lock.lock(); 37 | state -= 1; 38 | if (state == STATE_UNLOCKED && !writeQueue.isEmpty()) { 39 | state = STATE_WRITING; 40 | } 41 | lock.unlock(); 42 | if (state == STATE_WRITING) 43 | iterate(); 44 | } 45 | } 46 | 47 | public R write(final ExecutorService executor, SuspendableSupplier method) throws SuspendExecution { 48 | lock.lock(); 49 | if (state != STATE_UNLOCKED) { 50 | Coroutine coroutine = Coroutine.getActiveCoroutine(); 51 | return Coroutine.yieldThen(() -> { 52 | writeQueue.add(new CriticalSection.Waiting(executor, coroutine, method)); 53 | lock.unlock(); 54 | }); 55 | } 56 | state = STATE_WRITING; 57 | lock.unlock(); 58 | 59 | try { 60 | return method.get(); 61 | } finally { 62 | iterate(); 63 | } 64 | } 65 | 66 | public boolean tryUniqueWrite(ExecutorService executor, SuspendableRunnable method) throws SuspendExecution { 67 | lock.lock(); 68 | if (state != STATE_UNLOCKED) { 69 | if (state != STATE_WRITING && writeQueue.isEmpty()) { 70 | Coroutine coroutine = Coroutine.getActiveCoroutine(); 71 | Coroutine.yieldThen(() -> { 72 | writeQueue.add(new CriticalSection.Waiting(executor, coroutine, () -> { 73 | method.run(); 74 | return null; 75 | })); 76 | lock.unlock(); 77 | }); 78 | return true; 79 | } else { 80 | lock.unlock(); 81 | return false; 82 | } 83 | } 84 | state = STATE_WRITING; 85 | lock.unlock(); 86 | 87 | try { 88 | method.run(); 89 | return true; 90 | } finally { 91 | iterate(); 92 | } 93 | } 94 | 95 | void submit(CriticalSection.Waiting next) { 96 | Cohelp.submit(next.executor, () -> { 97 | try { 98 | final Object out = next.method.get(); 99 | iterate(); 100 | next.coroutine.process(out); 101 | } catch (final RuntimeException e) { 102 | iterate(); 103 | next.coroutine.processThrow(e); 104 | } 105 | }); 106 | } 107 | 108 | /** 109 | * Release pending readers, or else the next writer. 110 | * 111 | * @throws SuspendExecution 112 | */ 113 | void iterate() throws SuspendExecution { 114 | ArrayDeque readers; 115 | ArrayDeque temp = new ArrayDeque<>(); 116 | 117 | lock.lock(); 118 | // Tally new readers, reduce by completed reader 119 | state = (state == STATE_WRITING ? 0 : state - 1) + readQueue.size(); 120 | 121 | // Drain the reader queue for dispatch later here 122 | readers = readQueue; 123 | readQueue = temp; 124 | 125 | // If no readers, prep the next writer 126 | CriticalSection.Waiting writer; 127 | if (state == 0) { 128 | writer = writeQueue.poll(); 129 | if (writer != null) { 130 | state = STATE_WRITING; 131 | } 132 | } else 133 | writer = null; 134 | lock.unlock(); 135 | 136 | if (state == STATE_WRITING) 137 | submit(writer); 138 | else 139 | for (CriticalSection.Waiting reader : readers) 140 | submit(reader); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/SuspendableConsumer.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | 5 | /** 6 | * Like Consumer but can suspend. 7 | * 8 | * @param 9 | */ 10 | @FunctionalInterface 11 | public interface SuspendableConsumer { 12 | void apply(T t) throws SuspendExecution; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/SuspendableFunction.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | 5 | /** 6 | * Like Function but can suspend. 7 | * 8 | * @param 9 | * @param 10 | */ 11 | @FunctionalInterface 12 | public interface SuspendableFunction { 13 | R apply(T t) throws SuspendExecution; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/SuspendableSupplier.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | 5 | /** 6 | * Like Supplier but can suspend. 7 | * 8 | * @param 9 | */ 10 | @FunctionalInterface 11 | public interface SuspendableSupplier { 12 | T get() throws SuspendExecution; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/zarbosoft/coroutines/WRCriticalSection.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | 5 | import java.util.concurrent.ExecutorService; 6 | 7 | /** 8 | * Prioritizes a single write, releasing batches of queued reads in between 9 | */ 10 | public class WRCriticalSection extends RWCriticalSection { 11 | public R read(final ExecutorService executor, SuspendableSupplier method) throws SuspendExecution { 12 | lock.lock(); 13 | if (!writeQueue.isEmpty()) { 14 | Coroutine coroutine = Coroutine.getActiveCoroutine(); 15 | return Coroutine.yieldThen(() -> { 16 | readQueue.add(new CriticalSection.Waiting(executor, coroutine, method)); 17 | lock.unlock(); 18 | }); 19 | } 20 | state += 1; 21 | lock.unlock(); 22 | 23 | try { 24 | return method.get(); 25 | } finally { 26 | lock.lock(); 27 | state -= 1; 28 | CriticalSection.Waiting next; 29 | if (state == STATE_UNLOCKED) { 30 | next = writeQueue.poll(); 31 | if (next != null) { 32 | state = STATE_WRITING; 33 | } 34 | } else next = null; 35 | lock.unlock(); 36 | if (state == STATE_WRITING) { 37 | submit(next); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/zarbosoft/coroutines/TestCriticalSection.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | import com.zarbosoft.rendaw.common.Common; 5 | import org.junit.Test; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.concurrent.ExecutorService; 12 | 13 | import static org.hamcrest.CoreMatchers.equalTo; 14 | import static org.hamcrest.number.OrderingComparison.greaterThan; 15 | import static org.junit.Assert.assertThat; 16 | import static org.junit.Assert.assertTrue; 17 | 18 | public class TestCriticalSection { 19 | private final TestCriticalSection.Gate gate; 20 | private final ManualExecutor executor; 21 | private final CriticalSection critical; 22 | 23 | public TestCriticalSection() { 24 | this.gate = new TestCriticalSection.Gate(); 25 | this.executor = new ManualExecutor(); 26 | this.critical = new CriticalSection(); 27 | } 28 | 29 | public int invoke(int arg) throws SuspendExecution { 30 | return (Integer) critical.call(executor, () -> { 31 | assertThat(arg, greaterThan(4)); 32 | gate.stop(arg); 33 | return arg * 2; 34 | }); 35 | } 36 | 37 | @Test 38 | public void testCriticalSectionSingle() { 39 | final Coroutine coroutine1 = new Coroutine(() -> { 40 | assertThat(invoke(13), equalTo(26)); 41 | }); 42 | coroutine1.process(); 43 | gate.start(13); 44 | assertTrue(coroutine1.isFinished()); 45 | } 46 | 47 | @Test 48 | public void testCriticalSectionSingleNoSuspend() { 49 | final Coroutine coroutine1 = new Coroutine(() -> { 50 | assertThat(critical.call(executor, () -> 26), equalTo(26)); 51 | }); 52 | coroutine1.process(); 53 | assertTrue(coroutine1.isFinished()); 54 | } 55 | 56 | @Test 57 | public void testCriticalSectionSequential() { 58 | final Coroutine coroutine1 = new Coroutine(() -> { 59 | assertThat(invoke(13), equalTo(26)); 60 | assertThat(invoke(14), equalTo(28)); 61 | }); 62 | coroutine1.process(); 63 | gate.start(13); 64 | gate.start(14); 65 | assertTrue(coroutine1.isFinished()); 66 | } 67 | 68 | @Test 69 | public void testCriticalSectionNoContention() { 70 | final Coroutine coroutine1 = new Coroutine(() -> { 71 | assertThat(invoke(7), equalTo(14)); 72 | }); 73 | final Coroutine coroutine2 = new Coroutine(() -> { 74 | assertThat(invoke(8), equalTo(16)); 75 | }); 76 | coroutine1.process(); 77 | gate.start(7); 78 | assertTrue(coroutine1.isFinished()); 79 | coroutine2.process(); 80 | gate.start(8); 81 | assertTrue(coroutine2.isFinished()); 82 | } 83 | 84 | @Test 85 | public void testCriticalSectionContention() { 86 | final Coroutine coroutine1 = new Coroutine(() -> { 87 | assertThat(invoke(7), equalTo(14)); 88 | }); 89 | final Coroutine coroutine2 = new Coroutine(() -> { 90 | assertThat(invoke(8), equalTo(16)); 91 | }); 92 | coroutine1.process(); 93 | coroutine2.process(); 94 | gate.start(7); 95 | assertTrue(coroutine1.isFinished()); 96 | assertTrue(!coroutine2.isFinished()); 97 | gate.start(8); 98 | assertTrue(coroutine2.isFinished()); 99 | } 100 | 101 | static class Gate { 102 | Map gates = new HashMap<>(); 103 | 104 | public void stop(int id) throws SuspendExecution { 105 | System.out.format("Arrived at gate %s\n", id); 106 | if (gates.containsKey(id)) 107 | throw new AssertionError(); 108 | gates.put(id, Coroutine.getActiveCoroutine()); 109 | Coroutine.yield(); 110 | } 111 | 112 | public void start(int id) { 113 | System.out.format("Leaving gate %s\n", id); 114 | Coroutine coroutine = gates.remove(id); 115 | if (coroutine == null) 116 | throw new NotAtGateFailure(); 117 | coroutine.process(); 118 | } 119 | } 120 | 121 | static class NotAtGateFailure extends RuntimeException { 122 | 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/com/zarbosoft/coroutines/TestGeneral.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import org.junit.Test; 4 | 5 | import static junit.framework.TestCase.assertTrue; 6 | import static org.hamcrest.CoreMatchers.equalTo; 7 | import static org.junit.Assert.assertThat; 8 | 9 | public class TestGeneral { 10 | 11 | @Test 12 | public void testBaseline() { 13 | final Coroutine coroutine = new Coroutine(() -> { 14 | Coroutine.yield(); 15 | }); 16 | coroutine.process(); 17 | coroutine.process(); 18 | assertTrue(coroutine.isFinished()); 19 | } 20 | 21 | @Test(expected = com.zarbosoft.coroutinescore.Coroutine.Error.class) 22 | public void testCantRestartFinished() { 23 | final Coroutine coroutine = new Coroutine(() -> { 24 | }); 25 | coroutine.process(); 26 | coroutine.process(); 27 | } 28 | 29 | @Test 30 | public void testInject() { 31 | final Coroutine coroutine = new Coroutine(() -> { 32 | assertThat(Coroutine.yield(), equalTo(4)); 33 | }); 34 | coroutine.process(); 35 | coroutine.process(4); 36 | assertTrue(coroutine.isFinished()); 37 | } 38 | 39 | @Test 40 | public void testInjectException() { 41 | class TestError extends RuntimeException { 42 | 43 | } 44 | final Coroutine coroutine = new Coroutine(() -> { 45 | try { 46 | Coroutine.yield(); 47 | } catch (final TestError e) { 48 | return; 49 | } 50 | throw new AssertionError(); 51 | }); 52 | coroutine.process(); 53 | coroutine.processThrow(new TestError()); 54 | assertTrue(coroutine.isFinished()); 55 | } 56 | 57 | @Test 58 | public void testYieldThen() { 59 | final Coroutine coroutine = new Coroutine(() -> { 60 | final Coroutine coroutine1 = Coroutine.getActiveCoroutine(); 61 | Coroutine.yieldThen(() -> { 62 | coroutine1.process(); 63 | }); 64 | }); 65 | coroutine.process(); 66 | assertTrue(coroutine.isFinished()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/zarbosoft/coroutines/TestRWCriticalSection.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | import com.zarbosoft.rendaw.common.Common; 5 | import org.junit.Test; 6 | 7 | import java.util.*; 8 | 9 | import static org.hamcrest.CoreMatchers.equalTo; 10 | import static org.hamcrest.CoreMatchers.is; 11 | import static org.hamcrest.Matchers.greaterThan; 12 | import static org.junit.Assert.assertThat; 13 | import static org.junit.Assert.assertTrue; 14 | 15 | public class TestRWCriticalSection { 16 | private final TestCriticalSection.Gate gate; 17 | private final ManualExecutor executor; 18 | private final RWCriticalSection critical; 19 | 20 | public TestRWCriticalSection() { 21 | this.gate = new TestCriticalSection.Gate(); 22 | this.executor = new ManualExecutor(); 23 | this.critical = new RWCriticalSection(); 24 | } 25 | 26 | public int invokeRead(int arg) throws SuspendExecution { 27 | return (Integer) critical.read(executor, () -> { 28 | assertThat(arg, greaterThan(4)); 29 | gate.stop(arg); 30 | return arg * 2; 31 | }); 32 | } 33 | 34 | public int invokeWrite(int arg) throws SuspendExecution { 35 | return (Integer) critical.write(executor, () -> { 36 | assertThat(arg, greaterThan(4)); 37 | gate.stop(arg); 38 | return arg * 2; 39 | }); 40 | } 41 | 42 | public boolean invokeTryUniqueWrite(int arg) throws SuspendExecution { 43 | return critical.tryUniqueWrite(executor, () -> { 44 | assertThat(arg, greaterThan(4)); 45 | gate.stop(arg); 46 | }); 47 | } 48 | 49 | @Test 50 | public void testOneReader() { 51 | final Coroutine coroutine1 = new Coroutine(() -> { 52 | assertThat(invokeRead(13), equalTo(26)); 53 | }); 54 | coroutine1.process(); 55 | gate.start(13); 56 | assertTrue(coroutine1.isFinished()); 57 | } 58 | 59 | @Test 60 | public void testOneReaderNoSuspend() { 61 | final Coroutine coroutine1 = new Coroutine(() -> { 62 | assertThat(critical.read(executor, () -> 26), equalTo(26)); 63 | }); 64 | coroutine1.process(); 65 | assertTrue(coroutine1.isFinished()); 66 | } 67 | 68 | @Test 69 | public void testSequentialReaders() { 70 | Common.Mutable value = new Common.Mutable<>(0); 71 | final Coroutine coroutine1 = new Coroutine(() -> { 72 | assertThat(invokeRead(13), equalTo(26)); 73 | assertThat(invokeRead(14), equalTo(28)); 74 | value.value = 3; 75 | }); 76 | coroutine1.process(); 77 | gate.start(13); 78 | gate.start(14); 79 | assertThat(value.value, equalTo(3)); 80 | assertTrue(coroutine1.isFinished()); 81 | } 82 | 83 | @Test 84 | public void testParallelReaders() { 85 | List value = new ArrayList<>(); 86 | final Coroutine coroutine1 = new Coroutine(() -> { 87 | assertThat(invokeRead(13), equalTo(26)); 88 | value.add(3); 89 | }); 90 | final Coroutine coroutine2 = new Coroutine(() -> { 91 | assertThat(invokeRead(14), equalTo(28)); 92 | value.add(4); 93 | }); 94 | coroutine1.process(); 95 | coroutine2.process(); 96 | gate.start(14); 97 | gate.start(13); 98 | assertThat(value, equalTo(Arrays.asList(4, 3))); 99 | assertTrue(coroutine1.isFinished()); 100 | assertTrue(coroutine2.isFinished()); 101 | } 102 | 103 | @Test 104 | public void testOneWriter() { 105 | Common.Mutable value = new Common.Mutable<>(0); 106 | final Coroutine coroutine1 = new Coroutine(() -> { 107 | assertThat(invokeWrite(13), equalTo(26)); 108 | value.value = 3; 109 | }); 110 | coroutine1.process(); 111 | gate.start(13); 112 | assertThat(value.value, equalTo(3)); 113 | assertTrue(coroutine1.isFinished()); 114 | } 115 | 116 | @Test 117 | public void testOneWriterNoSuspend() { 118 | final Coroutine coroutine1 = new Coroutine(() -> { 119 | assertThat(critical.write(executor, () -> 26), equalTo(26)); 120 | }); 121 | coroutine1.process(); 122 | assertTrue(coroutine1.isFinished()); 123 | } 124 | 125 | @Test 126 | public void testSequentialWriters() { 127 | List value = new ArrayList<>(); 128 | final Coroutine coroutine1 = new Coroutine(() -> { 129 | assertThat(invokeWrite(13), equalTo(26)); 130 | value.add(3); 131 | }); 132 | final Coroutine coroutine2 = new Coroutine(() -> { 133 | assertThat(invokeWrite(14), equalTo(28)); 134 | value.add(4); 135 | }); 136 | coroutine1.process(); 137 | gate.start(13); 138 | coroutine2.process(); 139 | gate.start(14); 140 | assertTrue(coroutine1.isFinished()); 141 | assertTrue(coroutine2.isFinished()); 142 | assertThat(value, equalTo(Arrays.asList(3, 4))); 143 | } 144 | 145 | @Test 146 | public void testBlockingWriters() { 147 | List value = new ArrayList<>(); 148 | final Coroutine coroutine1 = new Coroutine(() -> { 149 | assertThat(invokeWrite(13), equalTo(26)); 150 | value.add(3); 151 | }); 152 | final Coroutine coroutine2 = new Coroutine(() -> { 153 | assertThat(invokeWrite(14), equalTo(28)); 154 | value.add(4); 155 | }); 156 | coroutine1.process(); 157 | coroutine2.process(); 158 | gate.start(13); 159 | gate.start(14); 160 | assertTrue(coroutine1.isFinished()); 161 | assertTrue(coroutine2.isFinished()); 162 | assertThat(value, equalTo(Arrays.asList(3, 4))); 163 | } 164 | 165 | @Test(expected = TestCriticalSection.NotAtGateFailure.class) 166 | public void testBlockingWritersBlocked() { 167 | List value = new ArrayList<>(); 168 | final Coroutine coroutine1 = new Coroutine(() -> { 169 | assertThat(invokeWrite(13), equalTo(26)); 170 | value.add(3); 171 | }); 172 | final Coroutine coroutine2 = new Coroutine(() -> { 173 | assertThat(invokeWrite(14), equalTo(28)); 174 | value.add(4); 175 | }); 176 | coroutine1.process(); 177 | coroutine2.process(); 178 | gate.start(14); 179 | } 180 | 181 | @Test 182 | public void testTryWritePass() { 183 | final Coroutine coroutine1 = new Coroutine(() -> { 184 | assertThat(invokeTryUniqueWrite(8), is(true)); 185 | }); 186 | coroutine1.process(); 187 | gate.start(8); 188 | assertTrue(coroutine1.isFinished()); 189 | } 190 | 191 | @Test 192 | public void testWriteTryWriteBlocked() { 193 | List value = new ArrayList<>(); 194 | final Coroutine coroutine1 = new Coroutine(() -> { 195 | assertThat(invokeTryUniqueWrite(7), is(true)); 196 | value.add(3); 197 | }); 198 | final Coroutine coroutine2 = new Coroutine(() -> { 199 | assertThat(invokeTryUniqueWrite(9), is(false)); 200 | value.add(4); 201 | }); 202 | coroutine1.process(); 203 | coroutine2.process(); 204 | assertTrue(coroutine2.isFinished()); 205 | gate.start(7); 206 | assertTrue(coroutine1.isFinished()); 207 | assertThat(value, equalTo(Arrays.asList(4, 3))); 208 | } 209 | 210 | @Test 211 | public void testReadBlockWrite() { 212 | List value = new ArrayList<>(); 213 | final Coroutine coroutine1 = new Coroutine(() -> { 214 | assertThat(invokeRead(13), equalTo(26)); 215 | value.add(3); 216 | }); 217 | final Coroutine coroutine2 = new Coroutine(() -> { 218 | assertThat(invokeWrite(14), equalTo(28)); 219 | value.add(4); 220 | }); 221 | coroutine1.process(); 222 | coroutine2.process(); 223 | gate.start(13); 224 | gate.start(14); 225 | assertThat(value, equalTo(Arrays.asList(3, 4))); 226 | assertTrue(coroutine1.isFinished()); 227 | assertTrue(coroutine2.isFinished()); 228 | } 229 | 230 | @Test(expected = TestCriticalSection.NotAtGateFailure.class) 231 | public void testReadBlockWriteBlocked() { 232 | List value = new ArrayList<>(); 233 | final Coroutine coroutine1 = new Coroutine(() -> { 234 | assertThat(invokeRead(13), equalTo(26)); 235 | value.add(3); 236 | }); 237 | final Coroutine coroutine2 = new Coroutine(() -> { 238 | assertThat(invokeWrite(14), equalTo(28)); 239 | value.add(4); 240 | }); 241 | coroutine1.process(); 242 | coroutine2.process(); 243 | gate.start(14); 244 | } 245 | 246 | @Test 247 | public void testReadTryWritePass() { 248 | List value = new ArrayList<>(); 249 | final Coroutine coroutine1 = new Coroutine(() -> { 250 | assertThat(invokeRead(13), equalTo(26)); 251 | value.add(3); 252 | }); 253 | final Coroutine coroutine2 = new Coroutine(() -> { 254 | assertThat(invokeTryUniqueWrite(14), is(true)); 255 | value.add(4); 256 | }); 257 | coroutine1.process(); 258 | coroutine2.process(); 259 | gate.start(13); 260 | gate.start(14); 261 | assertTrue(coroutine1.isFinished()); 262 | assertTrue(coroutine2.isFinished()); 263 | assertThat(value, equalTo(Arrays.asList(3, 4))); 264 | } 265 | 266 | @Test 267 | public void testReadTryWriteBlocked() { 268 | List value = new ArrayList<>(); 269 | final Coroutine coroutine1 = new Coroutine(() -> { 270 | assertThat(invokeRead(13), equalTo(26)); 271 | value.add(3); 272 | }); 273 | final Coroutine coroutine2 = new Coroutine(() -> { 274 | assertThat(invokeWrite(14), equalTo(28)); 275 | value.add(4); 276 | }); 277 | final Coroutine coroutine3 = new Coroutine(() -> { 278 | assertThat(invokeTryUniqueWrite(15), is(false)); 279 | value.add(5); 280 | }); 281 | coroutine1.process(); 282 | coroutine2.process(); 283 | coroutine3.process(); 284 | assertTrue(coroutine3.isFinished()); 285 | gate.start(13); 286 | assertTrue(coroutine1.isFinished()); 287 | gate.start(14); 288 | assertTrue(coroutine2.isFinished()); 289 | assertThat(value, equalTo(Arrays.asList(5, 3, 4))); 290 | } 291 | 292 | /** 293 | * Additional reads will run while a read is in progress even if write blocked. 294 | */ 295 | @Test 296 | public void testAdditionalReadBlockWrite() { 297 | List value = new ArrayList<>(); 298 | final Coroutine coroutine1 = new Coroutine(() -> { 299 | assertThat(invokeRead(13), equalTo(26)); 300 | value.add(3); 301 | }); 302 | final Coroutine coroutine2 = new Coroutine(() -> { 303 | assertThat(invokeWrite(14), equalTo(28)); 304 | value.add(4); 305 | }); 306 | final Coroutine coroutine3 = new Coroutine(() -> { 307 | assertThat(invokeRead(15), equalTo(30)); 308 | value.add(5); 309 | }); 310 | coroutine1.process(); 311 | coroutine2.process(); 312 | coroutine3.process(); 313 | gate.start(13); 314 | gate.start(15); 315 | gate.start(14); 316 | assertThat(value, equalTo(Arrays.asList(3, 5, 4))); 317 | assertTrue(coroutine1.isFinished()); 318 | assertTrue(coroutine2.isFinished()); 319 | assertTrue(coroutine3.isFinished()); 320 | } 321 | 322 | 323 | @Test 324 | public void testWriteBlockRead() { 325 | List value = new ArrayList<>(); 326 | final Coroutine coroutine1 = new Coroutine(() -> { 327 | assertThat(invokeRead(13), equalTo(26)); 328 | value.add(3); 329 | }); 330 | final Coroutine coroutine2 = new Coroutine(() -> { 331 | assertThat(invokeWrite(14), equalTo(28)); 332 | value.add(4); 333 | }); 334 | coroutine2.process(); 335 | coroutine1.process(); 336 | gate.start(14); 337 | gate.start(13); 338 | assertThat(value, equalTo(Arrays.asList(4, 3))); 339 | assertTrue(coroutine1.isFinished()); 340 | assertTrue(coroutine2.isFinished()); 341 | } 342 | 343 | @Test(expected = TestCriticalSection.NotAtGateFailure.class) 344 | public void testWriteBlockReadBlocked() { 345 | List value = new ArrayList<>(); 346 | final Coroutine coroutine1 = new Coroutine(() -> { 347 | assertThat(invokeRead(13), equalTo(26)); 348 | value.add(3); 349 | }); 350 | final Coroutine coroutine2 = new Coroutine(() -> { 351 | assertThat(invokeWrite(14), equalTo(28)); 352 | value.add(4); 353 | }); 354 | coroutine2.process(); 355 | coroutine1.process(); 356 | gate.start(13); 357 | } 358 | 359 | @Test 360 | public void testReadBetweenWrites() { 361 | List value = new ArrayList<>(); 362 | final Coroutine coroutine1 = new Coroutine(() -> { 363 | assertThat(invokeWrite(13), equalTo(26)); 364 | value.add(3); 365 | }); 366 | final Coroutine coroutine2 = new Coroutine(() -> { 367 | assertThat(invokeWrite(14), equalTo(28)); 368 | value.add(4); 369 | }); 370 | final Coroutine coroutine3 = new Coroutine(() -> { 371 | assertThat(invokeRead(15), equalTo(30)); 372 | value.add(5); 373 | }); 374 | coroutine1.process(); 375 | coroutine2.process(); 376 | coroutine3.process(); 377 | gate.start(13); 378 | gate.start(15); 379 | gate.start(14); 380 | assertThat(value, equalTo(Arrays.asList(3, 5, 4))); 381 | assertTrue(coroutine1.isFinished()); 382 | assertTrue(coroutine2.isFinished()); 383 | assertTrue(coroutine3.isFinished()); 384 | } 385 | 386 | /** 387 | * Move to additional reads block write state after initial write finishes. 388 | */ 389 | @Test 390 | public void testTransitionReadBlockWrite() { 391 | List value = new ArrayList<>(); 392 | final Coroutine coroutine1 = new Coroutine(() -> { 393 | assertThat(invokeWrite(13), equalTo(26)); 394 | value.add(3); 395 | }); 396 | final Coroutine coroutine2 = new Coroutine(() -> { 397 | assertThat(invokeWrite(14), equalTo(28)); 398 | value.add(4); 399 | }); 400 | final Coroutine coroutine3 = new Coroutine(() -> { 401 | assertThat(invokeRead(15), equalTo(30)); 402 | value.add(5); 403 | }); 404 | final Coroutine coroutine4 = new Coroutine(() -> { 405 | assertThat(invokeRead(16), equalTo(32)); 406 | value.add(6); 407 | }); 408 | coroutine1.process(); 409 | coroutine2.process(); 410 | coroutine3.process(); 411 | gate.start(13); 412 | coroutine4.process(); 413 | gate.start(16); 414 | gate.start(15); 415 | gate.start(14); 416 | assertThat(value, equalTo(Arrays.asList(3, 6, 5, 4))); 417 | assertTrue(coroutine1.isFinished()); 418 | assertTrue(coroutine2.isFinished()); 419 | assertTrue(coroutine3.isFinished()); 420 | assertTrue(coroutine4.isFinished()); 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/test/java/com/zarbosoft/coroutines/TestWRCriticalSection.java: -------------------------------------------------------------------------------- 1 | package com.zarbosoft.coroutines; 2 | 3 | import com.zarbosoft.coroutinescore.SuspendExecution; 4 | import org.junit.Test; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import static org.hamcrest.Matchers.equalTo; 11 | import static org.hamcrest.Matchers.greaterThan; 12 | import static org.junit.Assert.assertThat; 13 | import static org.junit.Assert.assertTrue; 14 | 15 | public class TestWRCriticalSection { 16 | private final TestCriticalSection.Gate gate; 17 | private final ManualExecutor executor; 18 | private final WRCriticalSection critical; 19 | 20 | public TestWRCriticalSection() { 21 | this.gate = new TestCriticalSection.Gate(); 22 | this.executor = new ManualExecutor(); 23 | this.critical = new WRCriticalSection(); 24 | } 25 | 26 | public int invokeRead(int arg) throws SuspendExecution { 27 | return (Integer) critical.read(executor, () -> { 28 | assertThat(arg, greaterThan(4)); 29 | gate.stop(arg); 30 | return arg * 2; 31 | }); 32 | } 33 | 34 | public int invokeWrite(int arg) throws SuspendExecution { 35 | return (Integer) critical.write(executor, () -> { 36 | assertThat(arg, greaterThan(4)); 37 | gate.stop(arg); 38 | return arg * 2; 39 | }); 40 | } 41 | 42 | @Test 43 | public void testOneRead() { 44 | final Coroutine coroutine1 = new Coroutine(() -> { 45 | assertThat(invokeRead(13), equalTo(26)); 46 | }); 47 | coroutine1.process(); 48 | gate.start(13); 49 | assertTrue(coroutine1.isFinished()); 50 | } 51 | 52 | @Test 53 | public void testParallelRead() { 54 | List value = new ArrayList<>(); 55 | final Coroutine coroutine1 = new Coroutine(() -> { 56 | assertThat(invokeRead(13), equalTo(26)); 57 | value.add(3); 58 | }); 59 | final Coroutine coroutine2 = new Coroutine(() -> { 60 | assertThat(invokeRead(14), equalTo(28)); 61 | value.add(4); 62 | }); 63 | coroutine1.process(); 64 | coroutine2.process(); 65 | gate.start(14); 66 | gate.start(13); 67 | assertThat(value, equalTo(Arrays.asList(4, 3))); 68 | assertTrue(coroutine1.isFinished()); 69 | assertTrue(coroutine2.isFinished()); 70 | } 71 | 72 | @Test 73 | public void testWriteBlockRead() { 74 | List value = new ArrayList<>(); 75 | final Coroutine coroutine1 = new Coroutine(() -> { 76 | assertThat(invokeRead(13), equalTo(26)); 77 | value.add(3); 78 | }); 79 | final Coroutine coroutine2 = new Coroutine(() -> { 80 | assertThat(invokeWrite(14), equalTo(28)); 81 | value.add(4); 82 | }); 83 | final Coroutine coroutine3 = new Coroutine(() -> { 84 | assertThat(invokeRead(15), equalTo(30)); 85 | value.add(5); 86 | }); 87 | coroutine1.process(); 88 | coroutine2.process(); 89 | coroutine3.process(); 90 | gate.start(13); 91 | gate.start(14); 92 | gate.start(15); 93 | assertThat(value, equalTo(Arrays.asList(3, 4, 5))); 94 | assertTrue(coroutine1.isFinished()); 95 | assertTrue(coroutine2.isFinished()); 96 | assertTrue(coroutine3.isFinished()); 97 | } 98 | 99 | @Test 100 | public void testReleaseReadBetweenWrite() { 101 | List value = new ArrayList<>(); 102 | final Coroutine coroutine1 = new Coroutine(() -> { 103 | assertThat(invokeRead(13), equalTo(26)); 104 | value.add(3); 105 | }); 106 | final Coroutine coroutine2 = new Coroutine(() -> { 107 | assertThat(invokeWrite(14), equalTo(28)); 108 | value.add(4); 109 | }); 110 | final Coroutine coroutine3 = new Coroutine(() -> { 111 | assertThat(invokeWrite(15), equalTo(30)); 112 | value.add(5); 113 | }); 114 | final Coroutine coroutine4 = new Coroutine(() -> { 115 | assertThat(invokeRead(16), equalTo(32)); 116 | value.add(6); 117 | }); 118 | coroutine1.process(); 119 | coroutine2.process(); 120 | coroutine3.process(); 121 | coroutine4.process(); 122 | gate.start(13); 123 | assertTrue(coroutine1.isFinished()); 124 | gate.start(14); 125 | assertTrue(coroutine2.isFinished()); 126 | gate.start(16); 127 | assertTrue(coroutine4.isFinished()); 128 | gate.start(15); 129 | assertTrue(coroutine3.isFinished()); 130 | assertThat(value, equalTo(Arrays.asList(3, 4, 6, 5))); 131 | } 132 | } 133 | --------------------------------------------------------------------------------