├── .gitignore ├── LICENSE ├── README.md ├── flixel └── LimeThreadPoolLoader │ ├── .gitignore │ ├── .vscode │ ├── launch.json │ └── tasks.json │ ├── Project.xml │ ├── README.md │ ├── hxformat.json │ └── source │ ├── Main.hx │ ├── ParallelLoader.hx │ └── PlayState.hx ├── haxe-threading-examples.code-workspace ├── haxe ├── .vscode │ ├── launch.json │ └── tasks.json ├── countingsemaphore-cpp.hxml ├── countingsemaphore.hxml ├── producerconsumer-cpp.hxml ├── producerconsumer.hxml ├── simplereaderwriter.hxml ├── src │ ├── CountingSemaphore.hx │ ├── ProducerConsumer.hx │ ├── SimpleReaderWriter.hx │ └── ThreadMessage.hx └── threadmessage.hxml └── lime ├── README.md ├── simple-futures ├── .vscode │ ├── launch.json │ └── tasks.json ├── Assets │ └── .gitignore ├── SimpleFutures.hxproj ├── Source │ └── SimpleFutures.hx └── project.xml ├── simple-promises ├── .vscode │ └── launch.json ├── Assets │ └── .gitignore ├── SimplePromises.hxproj ├── Source │ └── SimplePromise.hx └── project.xml └── simple-threadpool ├── Assets └── .gitignore ├── SimpleThreadpool.hxproj ├── Source └── SimpleThreadpool.hx └── project.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.hl 2 | hl/ 3 | cpp/ 4 | .haxelib 5 | Export/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 47rooks 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # haxe-threading-examples 2 | Haxe threading examples 3 | 4 | This repo is devoted to examining threading in Haxe and Haxe libraries and game 5 | frameworks. 6 | 7 | Initial examples are Haxe itself and then Lime Futures. 8 | 9 | The target will most commonly be Hashlink as sys.thread requires a sys target. 10 | -------------------------------------------------------------------------------- /flixel/LimeThreadPoolLoader/.gitignore: -------------------------------------------------------------------------------- 1 | assets/images/tests 2 | export/ 3 | dump/ -------------------------------------------------------------------------------- /flixel/LimeThreadPoolLoader/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "HashLink (debugger)", 9 | "type": "hl", 10 | "request": "launch", 11 | "hl": "D:\\Program Files\\HashLink1.14\\hl.exe", 12 | "cwd": "${workspaceFolder}\\export\\hl\\bin", 13 | "program": "${workspaceFolder}\\export\\hl\\bin\\hlboot.dat" 14 | }, 15 | ] 16 | } -------------------------------------------------------------------------------- /flixel/LimeThreadPoolLoader/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "lime", 6 | "command": "test", 7 | "targetConfiguration": "HashLink / Debug", 8 | "problemMatcher": [ 9 | "$haxe-absolute", 10 | "$haxe", 11 | "$haxe-error", 12 | "$haxe-trace" 13 | ], 14 | "label": "lime: test hl -debug", 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "lime", 22 | "command": "build", 23 | "targetConfiguration": "HashLink / Debug", 24 | "problemMatcher": [ 25 | "$haxe-absolute", 26 | "$haxe", 27 | "$haxe-error", 28 | "$haxe-trace" 29 | ], 30 | "label": "lime: build hl -debug" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /flixel/LimeThreadPoolLoader/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 27 | 29 | 30 | 31 | 33 | 34 | 35 | 37 | 38 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 59 | 61 | 62 | 64 | 66 | 67 | 68 | 69 | 71 | 73 | 74 | 76 | 78 | 79 | 81 | 83 | 84 | 86 | 87 | 89 | 90 | 92 | 93 | 95 | 97 | 98 | 100 | 105 | 106 | 108 | 110 | 111 | 113 | 114 | 115 | 116 | 117 | 119 | -------------------------------------------------------------------------------- /flixel/LimeThreadPoolLoader/README.md: -------------------------------------------------------------------------------- 1 | # Parallel Loader Example for HaxeFlixel 2 | 3 | - [Parallel Loader Example for HaxeFlixel](#parallel-loader-example-for-haxeflixel) 4 | - [Background](#background) 5 | - [Know the Problem](#know-the-problem) 6 | - [Asset Loading in HaxeFlixel (HF)](#asset-loading-in-haxeflixel-hf) 7 | - [A Parallel Loader](#a-parallel-loader) 8 | - [From waffle to concrete](#from-waffle-to-concrete) 9 | - [Running the Example](#running-the-example) 10 | - [Steps](#steps) 11 | - [Running Serial](#running-serial) 12 | - [Reset](#reset) 13 | - [Parallel Loading](#parallel-loading) 14 | - [Some Results](#some-results) 15 | 16 | ## Background 17 | 18 | This question comes up from time to time - how to speed up asset loads. In 19 | general this isn't really a problem and people use various tricks like 20 | preloaders and loading before the games, during scene switching and so on 21 | to hide the load time. And on HTML5 async loads are possible. But it's a 22 | interesting problem on sys targets. So I thought I would see what I could do 23 | using a Lime ThreadPool to improve the overall load time for a bunch of assets. 24 | 25 | Before I say anything, this is not a production ready solution. If it were I 26 | would probably put it in a lib. But it provides a solid example of how to 27 | use a thread pool to do this sort of thing, and would be a good start on a 28 | production version. 29 | 30 | ## Know the Problem 31 | 32 | A lot of times a problem which seems to be a candidate for a MT solution and 33 | people just dive in. But what problem are you actually going to solve, what 34 | gets threaded, what remains on the main thread, and why. The first thing to 35 | determine is if you actually have a problem. So: 36 | 37 | * measure your asset load times 38 | * figure out if you can move the loads out of the fast path, like into a preloader 39 | * are your assets just too big 40 | 41 | and likely other considerations. 42 | 43 | Once you're convinced you have a problem and you think threading might help try to get a feel for what to thread and what benefit you might get. So with asset loading what can you parallelize and what can you not, and what benefit will you get. 44 | 45 | So what is the most expensive part of loading an asset ? At this point you need to know basic ratios of things in computers. In loading images you have to allocate memory and read the bytes off the disk from the image file, decode the file format and put the bytes into the memory you allocated. In general, disk is slow, very slow. Even SSD is slow when compared to CPU cache line memory, or RAM. So it would be a fair guess that reading off disk is the slow bit. 46 | 47 | Next get some numbers. Performance problems are all about numbers. If the initial numbers are not bad enough there is not enough to gain by changing anything. I read an 8MB PNG off disk into a FlxSprite using `loadGraphic()` and it took over a second. This is an i7 with 16GB RAM and SSD storage. Hmmm.... After profiling a bit turns out PNG decoding is expensive. Cool - we learned something. My hardware is showing its age and there is a lot of time here which mean optimization might help - perhaps a lot. 48 | 49 | ### Asset Loading in HaxeFlixel (HF) 50 | 51 | For the purpose of this example we will discuss only image assets and all comments here are made with that in mind. 52 | 53 | Assets are loaded into a sprites basically via `FlxSprite.loadGraphic*` calls. But they are also loaded into a cache unless you explicitly tell it not to. A cache is generally a shared resource. If you modify a shared resource from multiple threads without concurrency controls you will have problems, random corruptions, lost writes, crashes and so on. Because of the way HF is written, being essentially single threaded there is no documentation of concurrency models and a huge amount of shared state. This just makes it ill-advised to parallelise `FlxSprite.loadGraphic*()` calls themselves. To do so safely goes beyond the scope of this example. 54 | 55 | So let's look lower in the stack. HF sprites use OpenFL BitmapDatas for the image content. These can be loaded directly and then a BitmapData can be loaded into a FlxSprite. Now there is also caching in OpenFL so there could be issues here but we will have to see. 56 | 57 | Finally, Lime also provides a way to load image data and in fact it's what the HF and OpenFL load routines use at the bottom. It is possible to load a Lime Image object directly from an a low level API and convert it into a BitmapData and that into a FlxSprite. The lower level routines don't necessarily use shared resources and so could very likely be implemented more safely. 58 | 59 | A possible model. At this point we have two possible models to try out and to compare. 60 | 61 | 1. Load Lime Images in parallel and then convert those into BitmapData and FlxSprites serially on the main thread. 62 | 2. Load OpenFL BitmapDatas in parallel and then convert these into FlxSprites serially on the main thead. 63 | 64 | Cool. 65 | 66 | ## A Parallel Loader 67 | 68 | Ok so now we have a some numbers, some understanding of the problem, and some possible designs to try. 69 | 70 | ### From waffle to concrete 71 | 72 | At this point you need to go and look and threading models and job pools and work queues and so on and see what kind of design you want. I'll save you the trouble for the minute and say that we are going to use the still unreleased Lime 8.2.0 ThreadPools. Of course, you'll go and research these things for yourself later, right ? Lime threadpools have been greatly enhanced and provide a job submission model with completion, error and progress callbacks. Refer to my https://github.com/47rooks/haxe-threading-examples/tree/main/lime/simple-threadpool for a simple example of how to use this. 73 | 74 | In this part of the haxe-threading-examples repo then we have a `ParallelLoader.hx` class which uses Lime `ThreadPool` to provide a way to load assets in parallel. I won't detail it here. It is fully commented. But it supports configurable number of threads to use, and a choice of loading OpenFL BitmapDatas or Lime Images. 75 | 76 | On the front of it there is a `PlayState.hx` which provides controls to select the number of threads, show the loading progress and provides simple timing display so you can create a list of comparative results. Performance - it's all about the data ! How often do I need to say that ? 77 | 78 | ## Running the Example 79 | 80 | I do not supply any assets to load. You need to provide your own. Why ? Well every game or application is different and will have different assets and the improvement you get with one set will differ from that with another. Also, I don't want to bloat the repo with tons of test load resources. 81 | 82 | When the app boots on the left are the controls and on the right there is an open area where thumbnails of the loaded assets are displayed. 83 | 84 | Note, I've only actually run this on Windows, HL and CPP builds. 85 | 86 | ### Steps 87 | 88 | * Clone the repo 89 | * Build for either HashLink or CPP 90 | * Copy your test assets into `export\windows\bin\assets\images\tests` or `export\hl\bin\assets\images\tests` 91 | * You may need to create these directories 92 | * Run the executable using `lime run hl` or `lime run windows` 93 | 94 | The actual UI is super primitive but should be obvious enough. 95 | 96 | Bear in mind this is not a game and the loads are taking place in the loop which means the application may appear hung or unresponsive when long operations are being done on the main application thread. Proper integration of such operations into the game loop is left as an exercise for the reader, as they say. 97 | 98 | #### Running Serial 99 | 100 | Leave the `Number of threads` slider at 0 and hit the `Load` button. This will run a serial load on the main application thread. It does not use the ThreadPool at all. This will serve as a baseline measurement that you can compare different numbers of threads against. The `Load Time (seconds)` will show the time taken to load. 101 | 102 | Run a number of runs of this ideally, exiting the app and restarting it each time. This will get rid of any unforeseen caching effects as much as possible. 103 | 104 | Finally note that the serial load does not update the progress bar because it is all done in one frame. 105 | 106 | #### Reset 107 | 108 | You can use the Reset button and do a new run but on HL memory is not properly deallocated, at least not in a timely way on Windows. I do not yet know why. I have logged an issue on this in case it is an HL GC issue but there is no final resolution on that yet. For the Windows CPP build this works ok and memory is released between runs. But I have seen another after several runs like this - a hang of some kind. This is yet to be investigated. 109 | 110 | #### Parallel Loading 111 | 112 | To run in parallel slide the `Number of threads` slider over to the number you want. Then select whether you want to load the assets as Lime Images, checked, or to load OpenFL BitmapDatas, unchecked. Then click Load. 113 | 114 | The `Current Thread Count` will report the number of threads and the progress bar will indicate the proportion of assets loaded. The `Load Time (seconds)` will show the time taken to load. 115 | 116 | Note, that the load time is the time from starting the load process to the point where all the FlxSprites have been created. It does not include the rendering time in the display of the loaded sprites. 117 | 118 | Again do multiple runs and compare the loading as Image vs Bitmap and you can build up an understanding of how much benefit you may get from a parallel load. 119 | 120 | ## Some Results 121 | 122 | Tested on MSI laptop with Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz 2.80 GHz, 16GB RAM and a Samsung EVO 840 SSD, running Windows 10. Before testing the application was rebuilt in release mode on both HL and CPP. Relevant particularly to threading, the CPU has 4 hyper threaded cores - 8 threads of execution. 123 | 124 | The data to be loaded was taken from FNF-PsychEngine merely grabbing all the PNGs. It does not necessarily represent a real load that would be done in a real game. It only serves to provide a set of data points to see how the loader behaves. For a test representative of your own situation you would need to use the set of resources you will load together. 125 | 126 | |Target|# threads|Image or BitmapData|Load time (s) 127 | |-|-|-|-| 128 | |HL|Serial|N/A|22.08| 129 | |HL|16|Image|12.056| 130 | |HL|16|BitmapData|8.143| 131 | |HL|64|Image|10.179| 132 | |HL|64|BitmapData|6.402| 133 | |cpp|Serial|N/A|22.337| 134 | |cpp|16|Image|12.573| 135 | |cpp|16|BitmapData|7.879| 136 | |cpp|64|Image|11.331| 137 | |cpp|64|BitmapData|7.797| 138 | 139 | I will note that during the BitmapData load tests on cpp that there were what appeared to be two instances of hangs. This would suggest perhaps a concurrency problem in cpp when loading BitmapData. Image loading was fine. If there is a concurreny problem with BitmapData loads which I suspect due to caching it is likely that it is not entirely safe to use this loader mode. But it will take more investigation to determine for sure. Why HL does not hit it if there is such a problem is also unknown. 140 | 141 | Also of note is that the cpp and HL load times are very similar HL beating cpp on a number of occasions. This was unexpected but interesting. It should als be noted that these were single runs for each data point. It would be better for a serious study to do multiple runs and average the results. -------------------------------------------------------------------------------- /flixel/LimeThreadPoolLoader/hxformat.json: -------------------------------------------------------------------------------- 1 | { 2 | "lineEnds": { 3 | "leftCurly": "both", 4 | "rightCurly": "both", 5 | "objectLiteralCurly": { 6 | "leftCurly": "after" 7 | } 8 | }, 9 | "sameLine": { 10 | "ifElse": "next", 11 | "doWhile": "next", 12 | "tryBody": "next", 13 | "tryCatch": "next" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /flixel/LimeThreadPoolLoader/source/Main.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import flixel.FlxGame; 4 | import openfl.display.Sprite; 5 | 6 | class Main extends Sprite 7 | { 8 | public function new() 9 | { 10 | super(); 11 | addChild(new FlxGame(0, 0, PlayState)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /flixel/LimeThreadPoolLoader/source/ParallelLoader.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.Exception; 4 | import lime.graphics.Image; 5 | import lime.system.ThreadPool; 6 | import lime.system.WorkOutput; 7 | import openfl.display.BitmapData; 8 | 9 | /** 10 | * The current state of the work function. This simply contains the 11 | * path to the image to load. As this task is not interruptible, as it 12 | * is basically just a file read, there is no state to save. 13 | */ 14 | @:structInit class LoaderState 15 | { 16 | public var imageToLoad:String; // Image to load 17 | } 18 | 19 | enum LoadedType 20 | { 21 | BITMAP_DATA(img:BitmapData); 22 | IMAGE(img:Image); 23 | } 24 | 25 | /** 26 | * This is the final result object. When the job completes it will 27 | * send one of these object via the `sendComplete()` function. 28 | * The `onComplete` function must understand this object. 29 | */ 30 | @:structInit class LoaderResult 31 | { 32 | public var imagePath:String; // Image to load 33 | public var img:LoadedType; 34 | } 35 | 36 | /** 37 | * Data structure reporting the progress of current load request. Only one 38 | * request may be active at a time, though this is not currently enforced. 39 | */ 40 | @:structInit class LoaderProgress 41 | { 42 | public var numLoaded:Int; 43 | public var total:Int; 44 | } 45 | 46 | /** 47 | * Data structure reporting an error from the loading thread, including the 48 | * image that was being loaded and the error encountered. 49 | */ 50 | @:structInit class LoaderError 51 | { 52 | public var imageToLoad:String; // path to image that failed to load 53 | public var error:String; // error message 54 | } 55 | 56 | /** 57 | * An example parallel asset loader for loading OpenFL BitmapData objects. 58 | * It uses a Lime ThreadPool for parallelisation. 59 | * An array of assets is loaded with one job being used per assets. Progress 60 | * may be reported on the basis of number of jobs completed (assets loaded) 61 | * against the number of assets to be loaded in total. 62 | */ 63 | class ParallelLoader 64 | { 65 | var _numThreads:Int; 66 | var _tp:ThreadPool; 67 | var _loadImages:Bool; 68 | 69 | /* Progress metrics - only updated on the main application thread. Do 70 | * not update these from the work function or you may see concurrency 71 | * related issues, such as undercounting. 72 | */ 73 | var _numToLoad:Int; 74 | var _numLoaded:Int; 75 | 76 | var _completionCbk:(result:LoaderResult) -> Void; 77 | var _progressCbk:(progress:LoaderProgress) -> Void; 78 | var _errorCbk:(progress:LoaderError) -> Void; 79 | 80 | /** 81 | * Constructor 82 | * @param numThreads maximum number of threads in the pool. 83 | * @param loadImages load lime.graphics.Images if true, 84 | * openfl.display.BitmapData otherwise. 85 | * @param completionCbk the completion callback to carry individual job load 86 | * results back to the caller. 87 | * @param progressCbk the progress callback to report the number of loads 88 | * completed so far. 89 | */ 90 | public function new(numThreads:Int = 1, loadImages:Bool = true, completionCbk:(result:LoaderResult) -> Void = null, 91 | progressCbk:(progress:LoaderProgress) -> Void = null, errorCbk:(error:LoaderError) -> Void = null) 92 | { 93 | _numThreads = numThreads; 94 | _loadImages = loadImages; 95 | _completionCbk = completionCbk; 96 | _progressCbk = progressCbk; 97 | _errorCbk = errorCbk; 98 | 99 | // Create threadpool 100 | _tp = new ThreadPool(0, _numThreads, MULTI_THREADED); 101 | /* Register job completion and error callbacks. Progress is only 102 | * reported at completion of each job, because it job reads a 103 | * single asset from disk. So progress is reported at each job complete 104 | * as a number of assets loaded vs the total number requested in 105 | * the call to `load()`. 106 | */ 107 | _tp.onComplete.add(onComplete); 108 | _tp.onError.add(onError); 109 | } 110 | 111 | /** 112 | * Load the assets in parallel. 113 | * 114 | * Runs in: main application thread. 115 | * 116 | * @param assetsToLoad a function that returns an Array of paths to 117 | * assets to load. 118 | */ 119 | public function load(assetsToLoad:() -> Array):Void 120 | { 121 | // Create jobs serially for loading each file 122 | var files = assetsToLoad(); 123 | _numToLoad = files.length; 124 | _numLoaded = 0; 125 | for (f in files) 126 | { 127 | var s:LoaderState = { 128 | imageToLoad: f 129 | }; 130 | if (_loadImages) { 131 | _tp.run(loadImage, s); 132 | } else { 133 | _tp.run(loadBitmapData, s); 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * Cancel outstanding jobs. 140 | */ 141 | public function cancel():Void 142 | { 143 | _tp.cancel(); 144 | } 145 | 146 | /** 147 | * Load an individual assets as a Lime Image. 148 | * 149 | * Runs in: a threadpool thread. 150 | * 151 | * @param state the loader state structure, which contains the file path 152 | * to load. 153 | * @param output the thread pool output object for communicating with 154 | * the main application thread. 155 | */ 156 | function loadImage(state:LoaderState, output:WorkOutput):Void 157 | { 158 | var img = Image.fromFile(state.imageToLoad); 159 | if (img == null) 160 | { 161 | output.sendError({imageToLoad: state.imageToLoad, error: 'Image load failed'}); 162 | return; 163 | } 164 | var result:LoaderResult = {imagePath: state.imageToLoad, img: LoadedType.IMAGE(img)}; 165 | output.sendComplete(result); 166 | } 167 | 168 | /** 169 | * Load an individual assets as an OpenFL BitmapData. 170 | * 171 | * Runs in: a threadpool thread. 172 | * 173 | * @param state the loader state structure, which contains the file path 174 | * to load. 175 | * @param output the thread pool output object for communicating with 176 | * the main application thread. 177 | */ function loadBitmapData(state:LoaderState, output:WorkOutput):Void 178 | { 179 | var bmd = BitmapData.fromFile(state.imageToLoad); 180 | if (bmd == null) 181 | { 182 | output.sendError({imageToLoad: state.imageToLoad, error: 'BitmapData load failed'}); 183 | return; 184 | } 185 | var result:LoaderResult = {imagePath: state.imageToLoad, img: LoadedType.BITMAP_DATA(bmd)}; 186 | output.sendComplete(result); 187 | } 188 | 189 | /** 190 | * Completion callback for Lime ThreadPool. 191 | * 192 | * Runs in: main application thread. 193 | * 194 | * @param result The loader result to be passed on to the caller. 195 | */ 196 | function onComplete(result:LoaderResult) 197 | { 198 | // Send progress message 199 | _numLoaded++; 200 | if (_progressCbk != null) 201 | { 202 | var p:LoaderProgress = {numLoaded: _numLoaded, total: _numToLoad}; 203 | _progressCbk(p); 204 | } 205 | // Report the completion 206 | if (_completionCbk != null) 207 | _completionCbk(result); 208 | } 209 | 210 | /** 211 | * This is the main thread error handling function. In this case it 212 | * handles the custom FibonacciError structure or the regular Haxe exception. 213 | * 214 | * Runs in: main application thread 215 | * 216 | * @param errorInfo this is a Dynamic and must be dynamically checked for correct 217 | * handling, because there are two possibilities in this example. 218 | */ 219 | function onError(errorInfo:Dynamic):Void 220 | { 221 | var error = ""; 222 | trace('type=${Type.typeof(errorInfo)}, error=${errorInfo}'); 223 | if (errorInfo is Exception) 224 | { 225 | error = '(ERROR) Job ${_tp.activeJob.id} Got exception ${Type.typeof(errorInfo)}:${errorInfo}'; 226 | trace(error); 227 | } 228 | else if (Reflect.hasField(errorInfo, 'id') && Reflect.hasField(errorInfo, 'exception')) 229 | { 230 | trace('(ERROR) Job ${_tp.activeJob.id} Got application error ${errorInfo.id}: ${errorInfo.exception}'); 231 | error = '${errorInfo}'; 232 | trace('errorInfo=${error}'); 233 | } 234 | else 235 | { 236 | error = '${errorInfo}'; 237 | trace('(ERROR) Job ${_tp.activeJob.id} Got unknown error type: ${error}'); 238 | } 239 | _numLoaded++; 240 | 241 | // Call the client error callback 242 | _errorCbk({ 243 | imageToLoad: _tp.activeJob.state.imageToLoad, 244 | error: error 245 | }); 246 | } 247 | 248 | public function getCurrentNumThreads():Int 249 | { 250 | return _tp.currentThreads; 251 | } 252 | 253 | public function getTotalImageToLoad():Int 254 | { 255 | return _numToLoad; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /flixel/LimeThreadPoolLoader/source/PlayState.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import ParallelLoader.LoaderError; 4 | import ParallelLoader.LoaderProgress; 5 | import ParallelLoader.LoaderResult; 6 | import flixel.FlxCamera; 7 | import flixel.FlxG; 8 | import flixel.FlxSprite; 9 | import flixel.FlxState; 10 | import flixel.addons.ui.FlxSlider; 11 | import flixel.addons.ui.FlxUIBar; 12 | import flixel.addons.ui.FlxUICheckBox; 13 | import flixel.group.FlxSpriteGroup; 14 | import flixel.text.FlxText; 15 | import flixel.ui.FlxBar; 16 | import flixel.ui.FlxButton; 17 | import flixel.util.FlxColor; 18 | import haxe.Timer; 19 | import haxe.ValueException; 20 | import lime.graphics.Image; 21 | import lime.system.System; 22 | import openfl.display.BitmapData; 23 | import sys.FileSystem; 24 | 25 | class PlayState extends FlxState 26 | { 27 | final IMAGES_DIR:String; 28 | final TEST_IMAGES_DIR:String; 29 | final MAX_THREADS = 256; 30 | 31 | var _controlsCamera:FlxCamera; 32 | var _controls:Controls; 33 | var _progressBar:FlxBar; 34 | var _currentThreads:FlxText; 35 | 36 | var _pl:ParallelLoader; 37 | 38 | var _title:FlxText; 39 | var _numThreads = 0; 40 | 41 | var _startTime:Float; // Starting time of the load 42 | var _endTime:Float; // Ending time of the load 43 | 44 | var _numimagesLoaded:Int; 45 | var _numLoadsErrored:Int; 46 | var _percentLoaded:Float; 47 | var _currentNumThreads:Float; 48 | var _numImagesToLoad:Float; 49 | var _loadTime:FlxText; 50 | var _loadJustCompleted:Bool = false; 51 | 52 | /** 53 | * If checked load Lime Images, else load OpenFL BitmapData. 54 | * Only applies to parallel loads 55 | */ 56 | var _loadImages:FlxUICheckBox; 57 | 58 | var _loadedImages:Array; // the loaded Lime Images 59 | var _loadedBitmapDatas:Array; // the loaded OpenFL BitmapDatas 60 | var _loadedSprites:Array; // the loaded Sprites 61 | 62 | var _loadButton:FlxButton; 63 | var _resetButton:FlxButton; 64 | 65 | public function new() 66 | { 67 | super(); 68 | IMAGES_DIR = 'assets/images'; 69 | TEST_IMAGES_DIR = IMAGES_DIR + '/tests'; 70 | } 71 | 72 | override public function create() 73 | { 74 | super.create(); 75 | 76 | bgColor = FlxColor.CYAN; 77 | 78 | _title = new FlxText(Controls.LINE_X, 0, FlxG.width, "Parallel Loader Example", 48); 79 | _title.setFormat(null, 48, FlxColor.BLACK, FlxTextAlign.LEFT); 80 | add(_title); 81 | 82 | // Create a second camera for the controls so they will not be affected by filters. 83 | _controlsCamera = new FlxCamera(0, 0, FlxG.width, FlxG.height); 84 | _controlsCamera.bgColor = FlxColor.TRANSPARENT; 85 | FlxG.cameras.add(_controlsCamera, false); 86 | add(_controlsCamera); 87 | 88 | var numThreadsSlider = new FlxSlider(this, "_numThreads", Controls.LINE_X, 100.0, 0, MAX_THREADS, 450, 15, 3, FlxColor.BLACK, FlxColor.BLACK); 89 | numThreadsSlider.setTexts("Number of threads", true, "0", '${MAX_THREADS}', 12); 90 | 91 | _progressBar = new FlxUIBar(Controls.LINE_X, 170.0, LEFT_TO_RIGHT, 450, 15, this, "_percentLoaded", 0, 100, true); 92 | var progressBarName = new FlxText(Controls.LINE_X, 190.0, "Percentage of assets loaded", 12); 93 | progressBarName.setFormat(null, 12, FlxColor.BLACK, FlxTextAlign.LEFT); 94 | 95 | var currentThreadsLabel = new FlxText(Controls.LINE_X, 250.0, 200, "Current Thread Count", 12); 96 | currentThreadsLabel.setFormat(null, 12, FlxColor.BLACK, FlxTextAlign.LEFT); 97 | _currentThreads = new FlxText(Controls.LINE_X + 210, 250.0, "0", 12); 98 | _currentThreads.setFormat(null, 12, FlxColor.BLACK, FlxTextAlign.LEFT); 99 | 100 | var loadImagesLabel = new FlxText(Controls.LINE_X + 50, 280.0, 300, "Load lime.graphics.Image", 12); 101 | loadImagesLabel.setFormat(null, 12, FlxColor.BLACK, FlxTextAlign.LEFT); 102 | _loadImages = new FlxUICheckBox(Controls.LINE_X, 280, null, null, "", 100); 103 | _loadImages.checked = true; 104 | 105 | _loadButton = new FlxButton(Controls.LINE_X + 50, 310.0, "Load", _loadCbk); 106 | _resetButton = new FlxButton(Controls.LINE_X + 150, 310.0, "Reset", _resetCbk); 107 | 108 | var loadTimeLabel = new FlxText(Controls.LINE_X, 350.0, 200, "Load Time (seconds)", 12); 109 | loadTimeLabel.setFormat(null, 12, FlxColor.BLACK, FlxTextAlign.LEFT); 110 | _loadTime = new FlxText(Controls.LINE_X + 210, 350.0, "-", 12); 111 | _loadTime.setFormat(null, 12, FlxColor.BLACK, FlxTextAlign.LEFT); 112 | 113 | _controls = new Controls(20, 100, 550, 760, [ 114 | 115 | // Add slider for the pixel box height 116 | numThreadsSlider, 117 | // Add progress bar 118 | _progressBar, 119 | progressBarName, 120 | // Add current thread count 121 | currentThreadsLabel, 122 | _currentThreads, 123 | // Load lime Images or openfl BitmapData 124 | _loadImages, 125 | loadImagesLabel, 126 | // Add buttons 127 | _loadButton, 128 | _resetButton, 129 | // Display the Load time 130 | loadTimeLabel, 131 | _loadTime 132 | ], _controlsCamera); 133 | 134 | add(_controls._controls); 135 | } 136 | 137 | override public function update(elapsed:Float) 138 | { 139 | super.update(elapsed); 140 | 141 | if (_pl != null) 142 | { 143 | _currentNumThreads = _pl.getCurrentNumThreads(); 144 | _currentThreads.text = '${_currentNumThreads}'; 145 | } 146 | 147 | if (_loadJustCompleted) 148 | { 149 | // Compute load time. Note that this can be off by as 150 | // much as one frame time. 151 | _loadTime.text = '${_endTime - _startTime}'; 152 | _renderLoadedSprites(); 153 | _loadJustCompleted = false; 154 | } 155 | 156 | if (FlxG.keys.justReleased.ESCAPE) 157 | { 158 | // Cancel any remaining jobs in the thread pool if there is one 159 | if (_pl != null) 160 | { 161 | trace('cancelling tp'); 162 | _pl.cancel(); 163 | } 164 | // If you use Sys.exit() the application will hang. This is due 165 | // to issue https://github.com/openfl/lime/issues/1803. Until that 166 | // is resolved we use System.exit(). 167 | System.exit(0); 168 | // Sys.exit(0); 169 | } 170 | } 171 | 172 | /** 173 | * Load button callback. This initiates the load. 174 | */ 175 | function _loadCbk():Void 176 | { 177 | _loadButton.active = false; 178 | _doLoad(); 179 | } 180 | 181 | /** 182 | * Reset button callback. This resets the application for a new load. 183 | */ 184 | function _resetCbk():Void 185 | { 186 | trace('cache size=${_cacheSize()}'); 187 | FlxG.resetState(); 188 | } 189 | 190 | inline function _resetLoadMetrics():Void 191 | { 192 | _numimagesLoaded = 0; 193 | _numLoadsErrored = 0; 194 | _percentLoaded = 0.0; 195 | _startTime = 0.0; 196 | _endTime = 0.0; 197 | _loadJustCompleted = false; 198 | } 199 | 200 | function updateLoadProgress(total:Float):Void 201 | { 202 | // Update progress bar. 203 | if (_numimagesLoaded + _numLoadsErrored == total) 204 | { 205 | _percentLoaded = 100.0; 206 | } 207 | } 208 | 209 | /** 210 | * The main load function. 211 | * 212 | * For serial loads this function does the entire load. 213 | * For parallel loads this functions creates a ParallelLoader which 214 | * will perform the loads using a Lime ThreadPool. 215 | */ 216 | private function _doLoad():Void 217 | { 218 | _numimagesLoaded = 0; 219 | 220 | _loadedImages = new Array(); 221 | _loadedBitmapDatas = new Array(); 222 | _loadedSprites = new Array(); 223 | 224 | if (_numThreads == 0) 225 | { 226 | // Serial load in main thread 227 | trace('serial load start'); 228 | 229 | _startTime = Timer.stamp(); 230 | 231 | if (FileSystem.exists(TEST_IMAGES_DIR)) 232 | { 233 | var imgToLoad = FileSystem.readDirectory(TEST_IMAGES_DIR); 234 | _numImagesToLoad = imgToLoad.length; 235 | for (inf in imgToLoad) 236 | { 237 | var fn = TEST_IMAGES_DIR + '/' + inf; 238 | 239 | var s = new FlxSprite(); 240 | s.loadGraphic(fn); 241 | _loadedSprites.push(s); 242 | _numimagesLoaded++; 243 | } 244 | } 245 | 246 | _checkCompletion(); 247 | _endTime = Timer.stamp(); 248 | } 249 | else 250 | { 251 | // Parallel load with thread pool of numThreads threads 252 | trace('parallel load start'); 253 | _currentNumThreads = 0; 254 | _currentThreads.text = '0'; 255 | _startTime = Timer.stamp(); 256 | _pl = new ParallelLoader(_numThreads, _loadImages.checked, _processResult, _reportProgress, _handleError); 257 | _pl.load(() -> 258 | { 259 | /** 260 | * Append path to the actual asset name as Lime does 261 | * not know anything about asset paths and must have 262 | * a path relative to the current working directory. 263 | */ 264 | var rv = new Array(); 265 | for (p in FileSystem.readDirectory(TEST_IMAGES_DIR)) 266 | { 267 | rv.push('${TEST_IMAGES_DIR}/${p}'); 268 | } 269 | return rv; 270 | }); 271 | } 272 | } 273 | 274 | /** 275 | * Process the results from the ParallelLoader, as appropriate to the 276 | * returned image type. 277 | * 278 | * @param result the loader result which may contain BitmapDatas or Images. 279 | */ 280 | function _processResult(result:LoaderResult):Void 281 | { 282 | switch (result.img) 283 | { 284 | case BITMAP_DATA(i): 285 | _loadedBitmapDatas.push(i); 286 | case IMAGE(i): 287 | _loadedImages.push(i); 288 | } 289 | _numimagesLoaded++; 290 | _checkCompletion(); 291 | if (_loadJustCompleted) 292 | { 293 | // In order to compare like with like times for parallel 294 | // and single threaded loads, we need to convert all 295 | // loaded Images to sprites here. 296 | for (img in _loadedImages) 297 | { 298 | var b = BitmapData.fromImage(img); 299 | var s = new FlxSprite().loadGraphic(b); 300 | _loadedSprites.push(s); 301 | } 302 | for (bmd in _loadedBitmapDatas) 303 | { 304 | var s = new FlxSprite().loadGraphic(bmd); 305 | _loadedSprites.push(s); 306 | } 307 | // Stamp completion time as soon as it is known 308 | _endTime = Timer.stamp(); 309 | } 310 | } 311 | 312 | /** 313 | * Populate the right hand half of the display with thumbnail images of 314 | * the loaded sprites. This serves really only to demonstrate that the 315 | * load worked and as a quick visual clue for any errors. 316 | */ 317 | function _renderLoadedSprites():Void 318 | { 319 | trace('rendering loaded sprites'); 320 | var rowColCount = Math.ceil(Math.sqrt(_numimagesLoaded)); 321 | final displayLEFT = 600; 322 | final displayTOP = 75; 323 | final displayRIGHT = FlxG.width - 50; 324 | final displayBOTTOM = FlxG.height - 50; 325 | 326 | var cellWidth = Math.ceil((displayRIGHT - displayLEFT) / rowColCount); 327 | var cellHeight = Math.ceil((displayBOTTOM - displayTOP) / rowColCount); 328 | for (i => s in _loadedSprites) 329 | { 330 | s.setGraphicSize(cellWidth, cellHeight); 331 | s.setSize(cellWidth, cellHeight); 332 | s.updateHitbox(); 333 | 334 | s.x = displayLEFT + (i % rowColCount) * cellWidth; 335 | s.y = displayTOP + Math.floor(i / rowColCount) * cellHeight; 336 | add(s); 337 | } 338 | } 339 | 340 | /** 341 | * Update the loader progress reporting to the UI. 342 | * @param progress the loader progress including the number of assets 343 | * to load in total and the number already loaded. 344 | */ 345 | function _reportProgress(progress:LoaderProgress):Void 346 | { 347 | _percentLoaded = progress.numLoaded * 100 / progress.total; 348 | 349 | // Strictly this only needs to be done once for a given load 350 | // but this will have to do for the demo. 351 | _numImagesToLoad = progress.total; 352 | } 353 | 354 | /** 355 | * An example error handling function. 356 | * A real error handler would have to figure out needed to be done 357 | * to recover or retry. This merely updates the count of load errors 358 | * and traces it out. 359 | * 360 | * @param error the loader error object 361 | */ 362 | function _handleError(error:LoaderError):Void 363 | { 364 | _numLoadsErrored++; 365 | _checkCompletion(); 366 | trace('Image load for ${error.imageToLoad} errored with ${error.error}'); 367 | } 368 | 369 | /** 370 | * Check for completion and set the just completed flag. The point here 371 | * is that after the load completes we need to render the thumbnails but 372 | * only want to do this once. 373 | */ 374 | function _checkCompletion():Void 375 | { 376 | if (_numImagesToLoad == _numLoadsErrored + _numimagesLoaded) 377 | { 378 | _loadJustCompleted = true; 379 | } 380 | } 381 | 382 | /** 383 | * A little test function to check the bitmap cache size. 384 | * @return Int the number of entries in the cache. 385 | */ 386 | @:access(flixel.system.frontEnds.BitmapFrontEnd._cache) 387 | inline function _cacheSize():Int 388 | { 389 | var numKeys = 0; 390 | for (k in FlxG.bitmap._cache.keys()) 391 | numKeys++; 392 | return numKeys; 393 | } 394 | 395 | /** 396 | * A little debug function to dump a stack trace at the current location. 397 | * This is useful for figuring out how the code got to a certain point. 398 | */ 399 | function dumpStackAtCurrentLocation():Void 400 | { 401 | var ex = new ValueException('get a stack'); 402 | trace('---- Current Stack START ----'); 403 | #if hl 404 | trace('thread name: ${sys.thread.Thread.current().getName()}'); 405 | #end 406 | trace(ex.stack); 407 | trace('---- Current Stack END ----'); 408 | } 409 | } 410 | 411 | /** 412 | * Controls provide a sprite group which can contain a collection of controls 413 | * which can control the various aspects of the shader active in the demo. 414 | */ 415 | class Controls 416 | { 417 | public static final LINE_X = 50; 418 | public static final BASE_FONT_SIZE = 16; 419 | 420 | public var _controls(default, null):FlxSpriteGroup; 421 | 422 | var _controlbg:FlxSprite; 423 | 424 | /** 425 | * Create a new Controls object. 426 | * @param xLoc the x position to place the group at. 427 | * @param yLoc the y position to place the group at. 428 | * @param xSize the width of the controls pane. 429 | * @param ySize the height of the controls pane. 430 | * @param uiElts an Array of FlxSprites to add to the control pane 431 | */ 432 | public function new(xLoc:Float, yLoc:Float, xSize:Int, ySize:Int, uiElts:Array, camera:FlxCamera) 433 | { 434 | // Put a semi-transparent background in 435 | _controlbg = new FlxSprite(10, 10); 436 | _controlbg.makeGraphic(xSize, ySize, FlxColor.BLUE); 437 | _controlbg.alpha = 0.2; 438 | _controlbg.cameras = [camera]; 439 | 440 | _controls = new FlxSpriteGroup(xLoc, yLoc); 441 | _controls.cameras = [camera]; 442 | 443 | _controls.add(_controlbg); 444 | 445 | // Add controls 446 | for (ui in uiElts) 447 | { 448 | ui.cameras = [camera]; 449 | _controls.add(ui); 450 | } 451 | 452 | var returnPrompt = new FlxText(LINE_X, ySize - 40, "Hit to exit", BASE_FONT_SIZE); 453 | returnPrompt.setFormat(null, 12, FlxColor.BLACK, FlxTextAlign.LEFT); 454 | _controls.add(returnPrompt); 455 | } 456 | 457 | /** 458 | * Check if mouse overlaps the control area. 459 | * @return Bool true if mouse overlaps control area, false otherwise. 460 | */ 461 | public function mouseOverlaps():Bool 462 | { 463 | return FlxG.mouse.overlaps(_controlbg); 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /haxe-threading-examples.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "flixel", 5 | "path": "flixel" 6 | }, 7 | { 8 | "name": "lime", 9 | "path": "lime" 10 | }, 11 | { 12 | "name": "haxe", 13 | "path": "haxe" 14 | }, 15 | { 16 | "name": "Haxe Threading Examples Project", 17 | "path": "." 18 | } 19 | ], 20 | "settings": { 21 | "lime.projectFile": "..\\flixel\\LimeThreadPoolLoader\\Project.xml", 22 | "files.exclude": { 23 | "haxe": true, 24 | "lime": true, 25 | "flixel": true 26 | }, 27 | "lime.targets": [ 28 | 29 | ], 30 | "lime.targetConfigurations": [ 31 | 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /haxe/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "HashLink (launch)", 10 | "request": "launch", 11 | "type": "hl", 12 | "hl": "D:\\Program Files\\HashLink1.14\\hl.exe", 13 | "cwd": "${workspaceFolder}", 14 | "preLaunchTask": "hxml: countingsemaphore.hxml" 15 | }, 16 | { 17 | "name": "HL ProducerConsumer (launch)", 18 | "request": "launch", 19 | "type": "hl", 20 | "hl": "D:\\Program Files\\HashLink1.14\\hl.exe", 21 | "cwd": "${workspaceFolder}" 22 | }, 23 | { 24 | "name": "HL ProducerConsumer (attach)", 25 | "request": "attach", 26 | "port": 6112, 27 | "type": "hl", 28 | "cwd": "${workspaceFolder}", 29 | "preLaunchTask": "hxml: producerconsumer.hxml" 30 | }, 31 | { 32 | "name": "HashLink (attach)", 33 | "request": "attach", 34 | "port": 6112, 35 | "type": "hl", 36 | "cwd": "${workspaceFolder}", 37 | "preLaunchTask": { 38 | "type": "haxe", 39 | "args": "active configuration" 40 | } 41 | }, 42 | { 43 | "name": "HL SimpleFutures (attach)", 44 | "request": "attach", 45 | "port": 6112, 46 | "type": "hl", 47 | "cwd": "${workspaceFolder}", 48 | "preLaunchTask": "lime: test hl -debug" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /haxe/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "hxml", 6 | "file": "countingsemaphore.hxml", 7 | "problemMatcher": [ 8 | "haxe-fake" 9 | ], 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | }, 14 | "label": "hxml: countingsemaphore.hxml" 15 | }, 16 | { 17 | "type": "hxml", 18 | "file": "producerconsumer.hxml", 19 | "problemMatcher": [ 20 | "haxe-fake" 21 | ], 22 | "group": { 23 | "kind": "build", 24 | "isDefault": true 25 | }, 26 | "label": "hxml: producerconsumer.hxml" 27 | }, 28 | { 29 | "type": "hxml", 30 | "file": "producerconsumer-cpp.hxml", 31 | "problemMatcher": [ 32 | "haxe-fake" 33 | ], 34 | "group": { 35 | "kind": "build", 36 | "isDefault": true 37 | }, 38 | "label": "hxml: producerconsumer-cpp.hxml" 39 | }, 40 | { 41 | "type": "haxe", 42 | "args": "active configuration", 43 | "problemMatcher": [], 44 | "group": "build", 45 | "label": "haxe: active configuration" 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /haxe/countingsemaphore-cpp.hxml: -------------------------------------------------------------------------------- 1 | -cpp cpp 2 | -cp src 3 | -main CountingSemaphore 4 | 5 | --next 6 | --cmd cpp\CountingSemaphore.exe -------------------------------------------------------------------------------- /haxe/countingsemaphore.hxml: -------------------------------------------------------------------------------- 1 | -hl hl\main.hl 2 | -cp src 3 | -main CountingSemaphore 4 | 5 | --next 6 | 7 | --cmd "D:\Program Files\HashLink1.14\hl.exe" hl\main.hl -------------------------------------------------------------------------------- /haxe/producerconsumer-cpp.hxml: -------------------------------------------------------------------------------- 1 | -cpp cpp 2 | -main ProducerConsumer 3 | --class-path src 4 | 5 | --next 6 | --run .\cpp\ProducerConsumer.exe -------------------------------------------------------------------------------- /haxe/producerconsumer.hxml: -------------------------------------------------------------------------------- 1 | # Note: currently this hangs on HL. 2 | # See https://github.com/HaxeFoundation/hashlink/issues/663 3 | 4 | -hl hl\main.hl 5 | -cp src 6 | -main ProducerConsumer 7 | 8 | --next 9 | --cmd "D:\Program Files\HashLink1.14\hl.exe" hl\main.hl -------------------------------------------------------------------------------- /haxe/simplereaderwriter.hxml: -------------------------------------------------------------------------------- 1 | -hl hl\main.hl 2 | -cp src 3 | -main SimpleReaderWriter 4 | 5 | --next 6 | 7 | --cmd "D:\Program Files\HashLink1.14\hl.exe" bin\main.hl -------------------------------------------------------------------------------- /haxe/src/CountingSemaphore.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import sys.thread.Lock; 4 | import sys.thread.Mutex; 5 | import sys.thread.Semaphore; 6 | import sys.thread.Thread; 7 | 8 | final ITERATIONS = 1000; 9 | 10 | /** 11 | * Set the QUEUE_SIZE to 0 for an unlimited queue size. 12 | * Comparing the printed lengths with a 0 and a non-0 queue size. 13 | * 5 is a good example limit as the natural size is about 10-50 odd 14 | * on my machine but that will vary machine to machine. If you set 15 | * the limit you should not see a print of queue length above that. 16 | */ 17 | final QUEUE_SIZE = 0; 18 | 19 | final MUTEXED = true; 20 | 21 | /** 22 | * A multi-thread safe FIFO queue with optional size limiting. 23 | * 24 | * This uses a regular Haxe Array and adds put() and get() 25 | * operations that provide FIFO behaviour protected by a mutex. 26 | * This makes addition and removal thread safe. It further will 27 | * enforce a size limit using counting semphores. 28 | */ 29 | class MTFIFOQueue { 30 | var _mutex = new Mutex(); // Mutex put and get operations 31 | var _q = new Array(); // the queue data 32 | var _sizeLimited:Bool = false; 33 | var _emptyCount:Semaphore; // count of currently empty slots 34 | var _fullCount:Semaphore; // count of currently occupied slots 35 | 36 | /** 37 | * Create a new MT FIFO queue with optional size limited. 38 | * @param maxSize the maximum size of the queue, 0 is unlimited 39 | */ 40 | public function new(?maxSize:Int = 0) { 41 | if (maxSize != 0) { 42 | _emptyCount = new Semaphore(maxSize); 43 | _fullCount = new Semaphore(0); 44 | _sizeLimited = true; 45 | } 46 | } 47 | 48 | /** 49 | * Put an item into the queue at the head. 50 | * If the size is limited and the queue is full the put 51 | * operation will block until space becomes available. 52 | * 53 | * @param value value to put in the queue. 54 | */ 55 | public function put(value:Int):Void { 56 | if (_sizeLimited) { 57 | _emptyCount.acquire(); 58 | } 59 | 60 | if (MUTEXED) 61 | _mutex.acquire(); 62 | 63 | _q.unshift(value); 64 | 65 | var l = _q.length; 66 | #if !hl // Only include this line if not HL due to hang bug 67 | trace('length of _q=${l}'); 68 | #end 69 | 70 | // Verify that we do not exceed the QUEUE_SIZE 71 | // This is not functionally necessary in a queue but it is here 72 | // for demonstration purposes. If you disable the sizing semaphores 73 | // you will see this print. 74 | if (_sizeLimited && l > QUEUE_SIZE) { 75 | trace('queue length exceeded ' + QUEUE_SIZE + '. length=' + Std.string(l)); 76 | } 77 | 78 | if (MUTEXED) 79 | _mutex.release(); 80 | 81 | if (_sizeLimited) 82 | _fullCount.release(); 83 | } 84 | 85 | /** 86 | * Get the value from the tail of the queue. 87 | * If there size is limited and there is no value this 88 | * operation will block until there is a value available. 89 | * 90 | * This may return null if the size is not limited and there 91 | * is no value. This is a consequence of the fact that Array.pop() 92 | * will return null if the queue is empty. This could be turned 93 | * into a blocking wait too with a little more code if desired. 94 | * 95 | * @return Null the value or in the none size limited case, null if 96 | * there is no value in the queue. 97 | */ 98 | public function get():Null { 99 | if (_sizeLimited) 100 | _fullCount.acquire(); 101 | 102 | if (MUTEXED) 103 | _mutex.acquire(); 104 | 105 | var rv = _q.pop(); 106 | 107 | if (MUTEXED) 108 | _mutex.release(); 109 | 110 | if (_sizeLimited) 111 | _emptyCount.release(); 112 | return rv; 113 | } 114 | } 115 | 116 | /** 117 | * The Consumer class reads values from the queue. 118 | * In this example the values are expected to be a sequence of numbers 119 | * from 0 going up in steps of 1. The Consumer verifies this. This is 120 | * strictly not necessary and even undesirable in some applications. 121 | * It is done here simply to verify that we are not dropping values. 122 | */ 123 | class Consumer { 124 | var _q:MTFIFOQueue; 125 | 126 | /** 127 | * Create a Consumer with the specified queue. 128 | * 129 | * @param q the queue to consume values from. 130 | */ 131 | public function new(q:MTFIFOQueue) { 132 | _q = q; 133 | } 134 | 135 | /** 136 | * The consumer thread runs this method to pull values 137 | * from the queue and verify we do not drop any. 138 | */ 139 | public function run():Void { 140 | var nextExpected = 0; 141 | var i = 0; 142 | while (i < ITERATIONS) { 143 | var rv = _q.get(); 144 | 145 | if (rv != null) { 146 | if (nextExpected == rv) { 147 | nextExpected++; 148 | } else { 149 | trace('Terminating: missed value at i (${i}) expected (${nextExpected}) received (${rv})'); 150 | return; 151 | } 152 | i++; 153 | } 154 | } 155 | } 156 | } 157 | 158 | /** 159 | * The Producer inserts values into the queue up to the 160 | * size limit of the queue if there is one. If not 161 | * it will put in as many as it can each time it is scheduled. 162 | */ 163 | class Producer { 164 | var _q:MTFIFOQueue; 165 | 166 | /** 167 | * Create a Producer with the specified queue. 168 | * 169 | * @param q the queue to publish values into. 170 | */ 171 | public function new(q:MTFIFOQueue) { 172 | _q = q; 173 | } 174 | 175 | /** 176 | * The producer runs this mathod to publish values 177 | * into the queue. 178 | */ 179 | public function run():Void { 180 | var next = 0; 181 | var i = 0; 182 | while (i++ < ITERATIONS) { 183 | _q.put(next++); 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * Strictly speaking Haxe doesn't have a binary sempahore. It only has 190 | * counting semaphores. But it has a mutex which serves the same 191 | * purpose as a binary semaphore. 192 | * 193 | * This example shows the use of a counting semaphore to synchronize a 194 | * producer consumer program. 195 | */ 196 | class CountingSemaphore { 197 | static public function main() { 198 | // The MT queue with the specified size. 199 | var q = new MTFIFOQueue(QUEUE_SIZE); 200 | 201 | // Create Producer and Consumer 202 | var p = new Producer(q); 203 | var c = new Consumer(q); 204 | 205 | // Create a lock instance so that main() knows 206 | // when the reader and writer are both done and 207 | // can exit. 208 | var l = new Lock(); 209 | 210 | // Create threads to run 211 | var r = Thread.create(() -> { 212 | trace('Producer.run starting'); 213 | p.run(); 214 | trace('Producer.run ending'); 215 | 216 | // Notify Main.main() that I am done 217 | l.release(); 218 | }); 219 | 220 | var w = Thread.create(() -> { 221 | trace('Consumer.run starting'); 222 | c.run(); 223 | trace('Consumer.run ending'); 224 | 225 | // Notify Main.main() that I am done 226 | l.release(); 227 | }); 228 | 229 | // Wait for both threads to complete. 230 | l.wait(); 231 | l.wait(); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /haxe/src/ProducerConsumer.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import sys.thread.Lock; 4 | import sys.thread.Semaphore; 5 | import sys.thread.Thread; 6 | 7 | /** 8 | * An very simple example of the classic producer consumer model 9 | * using semaphores. 10 | */ 11 | // Sempahore to indicate that a value has been produced 12 | var produced = new Semaphore(0); 13 | 14 | // Semphore to indicate that the value has been consumed 15 | var consumed = new Semaphore(1); 16 | 17 | // Buffer to pass the value between threads 18 | var buffer = 0; 19 | final NUM_ITERATIONS = 100; 20 | 21 | /** 22 | * The Consumer simply waits for a new value to be produced 23 | * as indicated by the `produced` semaphore. Once it reads it 24 | * it just prints it out and indicates that it has consumed 25 | * the value via the `consumed` semaphore. 26 | */ 27 | class Consumer { 28 | public function new() {} 29 | 30 | public function run():Void { 31 | var i = 0; 32 | while (i++ < NUM_ITERATIONS) { 33 | produced.acquire(); 34 | trace('CONSUMER:this is iteration ${i} and buffer contains ${buffer}'); 35 | consumed.release(); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * The Producer waits for the previous value to be consumed as 42 | * indicated by the `consumed` semaphore. It then publishes the 43 | * next value, and then indicates a new value has been published 44 | * by releasing the `produced` semaphore. 45 | */ 46 | class Producer { 47 | public function new() {} 48 | 49 | public function run():Void { 50 | var i = 0; 51 | while (i++ < NUM_ITERATIONS) { 52 | consumed.acquire(); 53 | trace('PRODUCER:this is iteration ${i} and setting buffer to ${buffer}'); 54 | buffer = i; 55 | produced.release(); 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * ProducerConsumer runs a very simple two thread producer consumer 62 | * example. The key things to note are the two semaphores and the 63 | * initial values of each. `produced` is initially 0 which will block 64 | * the Consumer until a value is produced. `consumed` is initially 65 | * 1 which allows the Producer to proceed immediately to publish the 66 | * initial value. 67 | * 68 | * In the traces you should see that each process does one iteration and 69 | * waits for the other. The traces alternate between Producer and Consumer. 70 | * 71 | * Note also, that while the buffer and semaphores are globals you would 72 | * not normally do it this way. A production implementation would 73 | * provide a way to pass the buffer and semaphores to the Producer and 74 | * Consumer. 75 | * 76 | * Finally note also the Lock() object. This is used to prevent the 77 | * main thread from exiting before the other threads are finished. 78 | * If the main thread exits the program will end. This simple 79 | * lock wait prevents that and each thread releases the lock when 80 | * it completes. 81 | */ 82 | class ProducerConsumer { 83 | public static function main() { 84 | var c = new Consumer(); 85 | var p = new Producer(); 86 | 87 | var l = new Lock(); 88 | var tThread = Thread.create(() -> { 89 | trace('Consumer.run starting'); 90 | c.run(); 91 | trace('Consumer.run ending'); 92 | 93 | // Notify Main.main() that I am done 94 | l.release(); 95 | }); 96 | 97 | var pThread = Thread.create(() -> { 98 | trace('Producer.run starting'); 99 | p.run(); 100 | trace('Producer.run ending'); 101 | 102 | // Notify Main.main() that I am done 103 | l.release(); 104 | }); 105 | 106 | // Wait on threads to complete 107 | l.wait(); 108 | l.wait(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /haxe/src/SimpleReaderWriter.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.Timer; 4 | import sys.thread.Lock; 5 | import sys.thread.Mutex; 6 | import sys.thread.Thread; 7 | 8 | /** 9 | * Enable or disable the use of the Data.mutex by the reader and writer classes. 10 | * If both are not enabled the reader will detect an inconsistency between 11 | * Data.x and Data.y, and exit. 12 | */ 13 | final USE_WRITE_MUTEX = true; 14 | 15 | final USE_READ_MUTEX = true; 16 | final ITERATIONS = 1000000; 17 | 18 | /** 19 | * Data is a simple two value data structure which also contains a Mutex. 20 | */ 21 | class Data { 22 | public var mutex:Mutex; 23 | public var x:Int; 24 | public var y:Int; 25 | 26 | public function new() { 27 | mutex = new Mutex(); 28 | } 29 | } 30 | 31 | /** 32 | * Example reader class which simpler reads the shared data object 33 | * and compares the x and y values. If they are the same then 34 | * we have a consistent Data object. If they are not then one was 35 | * updated after we read the first one and thus we have an incomplete 36 | * update, split write, corruption, whatever you want to call it. 37 | * 38 | * Enable the use of the mutex on the critical section by setting the global 39 | * USE_READ_MUTEX to true. 40 | */ 41 | class Reader { 42 | var _d:Data; 43 | 44 | public function new(d:Data) { 45 | _d = d; 46 | } 47 | 48 | public function run(iterations:Int) { 49 | trace('Reader.run starting'); 50 | while (iterations-- > 0) { 51 | // Read the data values unprotected and make sure they are the same 52 | // The values have to be copied into local variables so that the 53 | // trace statement does not have to read them again while the writer 54 | // is still updating. 55 | if (USE_READ_MUTEX) { 56 | _d.mutex.acquire(); 57 | } 58 | var x = _d.x; 59 | var y = _d.y; 60 | if (USE_READ_MUTEX) { 61 | _d.mutex.release(); 62 | } 63 | if (x != y) { 64 | trace('x (${x}) != y (${y}) differ at iteration ${ITERATIONS - iterations}'); 65 | return; 66 | } 67 | } 68 | trace('Reader.run ending'); 69 | } 70 | } 71 | 72 | /** 73 | * The Writer class generates a random number and sets both x and y in Data to 74 | * that value. 75 | * 76 | * Enable the use of the mutex on the critical section by setting the global 77 | * USE_WRITE_MUTEX to true. 78 | */ 79 | class Writer { 80 | var _d:Data; 81 | 82 | public function new(d:Data) { 83 | _d = d; 84 | } 85 | 86 | public function run(iterations:Int) { 87 | while (iterations-- > 0) { 88 | var r = Math.round(Math.random() * 100.0); 89 | if (USE_WRITE_MUTEX) { 90 | _d.mutex.acquire(); 91 | } 92 | _d.x = r; 93 | _d.y = r; 94 | if (USE_WRITE_MUTEX) { 95 | _d.mutex.release(); 96 | } 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * SimpleReaderWrite runs a multithreaded test where the 103 | * objective is for the reader to see only matching Data.x 104 | * and Data.y while the writer changes the values to a new 105 | * random value each iteration. 106 | * 107 | * Only with mutexing enabled will this actually pass 108 | * through all iterations. Of course lowering the iteration 109 | * count will reduce the probability of failure but it won't 110 | * make it thread safe. 111 | */ 112 | class SimpleReaderWriter { 113 | public static function main() { 114 | var start = Timer.stamp(); 115 | trace('main() starting at ${start}'); 116 | 117 | // Create a data class 118 | var d = new Data(); 119 | d.x = d.y = 0; 120 | 121 | // Create a Reader 122 | var reader = new Reader(d); 123 | 124 | // Create a Writer 125 | var writer = new Writer(d); 126 | 127 | // Create a lock instance so that main() knows 128 | // when the reader and writer are both done and 129 | // can exit. 130 | var l = new Lock(); 131 | 132 | // Create two threads 133 | var r = Thread.create(() -> { 134 | trace('Reader.run starting'); 135 | reader.run(ITERATIONS); 136 | trace('Reader.run ending'); 137 | 138 | // Notify Main.main() that I am done 139 | l.release(); 140 | }); 141 | 142 | var w = Thread.create(() -> { 143 | trace('Writer.run starting'); 144 | writer.run(ITERATIONS); 145 | trace('Writer.run ending'); 146 | 147 | // Notify Main.main() that I am done 148 | l.release(); 149 | }); 150 | 151 | // Wait for both threads to complete. 152 | l.wait(); 153 | l.wait(); 154 | 155 | var end = Timer.stamp(); 156 | trace('main() ending at ${end}'); 157 | trace('elapsed=${end - start}'); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /haxe/src/ThreadMessage.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import sys.thread.Lock; 4 | import sys.thread.Thread; 5 | 6 | /** 7 | * Work is a simple work request message sent to a worker thread. 8 | */ 9 | typedef Work = { 10 | /** 11 | * The id of the request. This is useful for tracking and debugging, 12 | * for indicating which tasks failed and need retrying and so on. 13 | */ 14 | var id:Int; 15 | 16 | /** 17 | * This is the thread to which a response must be sent. 18 | * In a thread pool the submitter would just reap the response when it 19 | * was ready. Here the thread needs to know where to send the response. 20 | */ 21 | var sender:Thread; 22 | 23 | /** 24 | * This is the type of operation to perform. There are only two operations, 25 | * ADD and STOP, in this example. 26 | */ 27 | var type:String; 28 | 29 | /** 30 | * The first parameter for the task. 31 | */ 32 | var param1:Int; 33 | 34 | /** 35 | * The second parameter for the task. 36 | */ 37 | var param2:Int; 38 | } 39 | 40 | /** 41 | * Response is a simple response message to the sender. 42 | */ 43 | typedef Response = { 44 | /** 45 | * The task id for which this is the response. This allows the sender 46 | * to match the response to its request. 47 | */ 48 | var id:Int; 49 | 50 | /** 51 | * The type of the response. In this case there is only RESULT but in 52 | * a normal system you would also need ERROR and possibly other types. 53 | */ 54 | var type:String; 55 | 56 | /** 57 | * The result of the operation. This is only a Float type to permit 58 | * other mathematical operations such as division. 59 | */ 60 | var result:Float; 61 | } 62 | 63 | /** 64 | * ThreadMessage is a simple demonstration of use of Thread sendMessage() 65 | * and readMessage() functions between threads. There is no error handling 66 | * and the work requests are trivial so load is simulated with Sys.sleep(). 67 | */ 68 | class ThreadMessage { 69 | public static function main() { 70 | /* Create a worker thread that can respond to requests */ 71 | var t = Thread.create(workerMain); 72 | 73 | // Call driver function in main thread 74 | driverMain(t); 75 | } 76 | 77 | /** 78 | * This is the main driver function, basically the program main and 79 | * it runs in the main haxe thread. It is a free-running while loop 80 | * and thus if you watch this in a tool like Process Explorer or top 81 | * you will see this thread consuming a whole hardware thread. 82 | * 83 | * @param worker this is the worker thread to which requests can be sent. 84 | * In a proper program this might well be done in a different way but 85 | * passing it in directly is simple. 86 | */ 87 | static function driverMain(worker:Thread) { 88 | var exit = false; 89 | var numbers = [2, 2, 3, 4, 5, 6, 7, 8, 9, 1]; 90 | var i = 0; 91 | var responseCount = 0; 92 | 93 | while (!exit) { 94 | // Enqueue work requests 95 | if (i < numbers.length / 2) { 96 | worker.sendMessage({ 97 | id: i, 98 | sender: Thread.current(), 99 | type: 'ADD', 100 | param1: numbers[2 * i], 101 | param2: numbers[2 * i + 1] 102 | }); 103 | } 104 | 105 | // Reap responses 106 | var r:Response = Thread.readMessage(false); 107 | if (r != null) { 108 | trace('At iteration ${i} got result for operation (${r.id}): ${r.result}'); 109 | responseCount++; 110 | } 111 | 112 | // Exit condition 113 | if (responseCount >= numbers.length / 2) { 114 | exit = true; 115 | } 116 | 117 | i++; 118 | } 119 | } 120 | 121 | /** 122 | * This is the worker main function. It runs in a separate thread which is 123 | * started just after program start in main(). It could be started at any 124 | * suitable time but for this demonstration that is the simplest. 125 | * 126 | * It provides a simple blocking read message, do operation loop. The 127 | * blocking read means the thread uses basically no cpu until there is 128 | * work to do. In Process Explorer or top this thread will appear completely 129 | * idle because it uses so little cpu. In a real application this would 130 | * not be so. 131 | * 132 | * Note, there is no error handling. 133 | */ 134 | static function workerMain():Void { 135 | var exit = false; 136 | while (!exit) { 137 | var m:Work = Thread.readMessage(true); 138 | switch m { 139 | case {type: "STOP"}: 140 | exit = true; 141 | case { 142 | id: id, 143 | sender: s, 144 | type: "ADD", 145 | param1: x, 146 | param2: y 147 | }: 148 | // Simulate time working 149 | Sys.sleep(Math.random()); 150 | 151 | // Send computation result 152 | s.sendMessage({type: "RESULT", result: x + y, id: id}); 153 | case _: 154 | trace('unknown message type'); 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /haxe/threadmessage.hxml: -------------------------------------------------------------------------------- 1 | -hl hl\main.hl 2 | -cp src 3 | -main ThreadMessage 4 | 5 | --next 6 | 7 | --cmd "D:\Program Files\HashLink1.14\hl.exe" hl\main.hl -------------------------------------------------------------------------------- /lime/README.md: -------------------------------------------------------------------------------- 1 | # Lime Examples 2 | 3 | ## Prerequisites 4 | 5 | Note that for Lime Futures related examples you will need to use Lime 8.2.0-Dev branch versions of Lime. 6 | 8.2.0 seriously updated the Futures and ThreadPool support making it quite usable for task scheduler. 7 | https://github.com/openfl/lime/tree/develop?tab=readme-ov-file describes how to get development nightly build 8 | versions of Lime. Refer to the Development Builds section. While this link will age https://github.com/openfl/lime/actions/runs/7848199962 has lime-haxelib.zip that works. 9 | 10 | ## References 11 | 12 | There is an article posted on Lime threading https://player03.com/openfl/threads-guide/. The article is helpful on a number of fronts. 13 | 14 | -------------------------------------------------------------------------------- /lime/simple-futures/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "HL SimpleFutures (attach)", 9 | "request": "attach", 10 | "port": 6112, 11 | "type": "hl", 12 | "cwd": "${workspaceFolder}", 13 | "preLaunchTask": "lime: test hl -debug" 14 | }, 15 | { 16 | "name": "Build + Debug", 17 | "type": "lime", 18 | "request": "launch" 19 | }, 20 | { 21 | "name": "Debug", 22 | "type": "lime", 23 | "request": "launch", 24 | "preLaunchTask": null 25 | }, 26 | { 27 | "name": "HashLink (debugger)", 28 | "type": "hl", 29 | "request": "launch", 30 | "hl": "D:\\Program Files\\HashLink1.14\\hl.exe", 31 | "cwd": "Export\\hl\\bin", 32 | "program": "hl\\bin\\hlboot.dat" 33 | }, 34 | ] 35 | } -------------------------------------------------------------------------------- /lime/simple-futures/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "haxe", 6 | "args": "active configuration", 7 | "problemMatcher": [], 8 | "group": "build", 9 | "label": "haxe: active configuration" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /lime/simple-futures/Assets/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/47rooks/haxe-threading-examples/e62ccab44d7b78c4030503711f2a125d2c777ca3/lime/simple-futures/Assets/.gitignore -------------------------------------------------------------------------------- /lime/simple-futures/SimpleFutures.hxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | "$(CompilerPath)/haxelib" run lime build "$(OutputFile)" $(TargetBuild) -$(BuildConfig) -Dfdb 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /lime/simple-futures/Source/SimpleFutures.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.ValueException; 4 | import lime.app.Application; 5 | import lime.app.Future; 6 | import lime.system.System; 7 | 8 | /** 9 | * TaskState defines parameters to be passed to 10 | * the task function by Future.withEventualValue(). 11 | */ 12 | typedef TaskState = { 13 | var fid:Int; 14 | var throwError:Bool; 15 | } 16 | 17 | /** 18 | * SimpleFutures is an application that shows a very simple 19 | * use of Lime Futures with a multi-threaded thread pool. 20 | * 21 | * This queues NUM_TASKS tasks with up to MAX_THREADS threads 22 | * in the FutureWork pool. The tasks are trivially simple only 23 | * doing a sleep and emitting an integer. This is simply to 24 | * make it clear that there are multiple threads running 25 | * concurrently as can be seen from the ordering of the traces. 26 | * All Futures set async to true. This approach to creating 27 | * futures in multi-threaded code is deprecated but, it is 28 | * simpler to follow. 29 | * 30 | * Note, while Future supports progress indication this requires 31 | * the use of a ThreadPool and will be taken up in another 32 | * example. 33 | */ 34 | class SimpleFutures extends Application { 35 | final MAX_THREADS = 5; 36 | final NUM_TASKS = 10; 37 | var jobsQueued = false; // Make sure we only queue the jobs once 38 | var numCompleted = 0; 39 | var numErrored = 0; 40 | 41 | public function new() { 42 | super(); 43 | } 44 | 45 | override public function update(deltaTime:Int):Void { 46 | super.update(deltaTime); 47 | 48 | /* Check termination condition. This is only for the demo 49 | * as otherwise you have to go and shutdown the lime 50 | * application window. 51 | */ 52 | if ((numCompleted + numErrored) == NUM_TASKS) { 53 | trace('All tasks completed. Exiting'); 54 | System.exit(0); 55 | } 56 | 57 | /* If this is the first time through then enqueue all 58 | * the tasks. Note that the Future takes the actual work 59 | * function as its first parameter. 60 | * 61 | * To the Future are then added callbacks for completion and 62 | * error handling. The completion and error handlers are 63 | * called in the main thread. The work function itself is 64 | * called in the threadpool thread. 65 | */ 66 | if (!jobsQueued) { 67 | FutureWork.maxThreads = MAX_THREADS; 68 | for (i in 0...NUM_TASKS) { 69 | var f = Future.withEventualValue(genNumber, {fid: i, throwError: i == (NUM_TASKS - 1) ? true : false}); 70 | f.onComplete(futureComplete.bind(i)); 71 | f.onError(futureError.bind(i)); 72 | } 73 | 74 | jobsQueued = true; 75 | } 76 | } 77 | 78 | /** 79 | * `genNumber` traces out some debug messages so it is easy to see 80 | * what is happening. It then sleeps for a bit and prints another message 81 | * before returning the final completion message. 82 | * 83 | * @param fid this is a simple ID to track what is going on 84 | * @param throwError if true this task will throw an exception 85 | * @return String 86 | */ 87 | function genNumber(state:TaskState):String { 88 | trace('I am a thread starting ${state.fid}'); 89 | 90 | /* Sleep a random amount - this will help alter thread progress 91 | * which will show interleaving and concurrency more convincingly. 92 | */ 93 | Sys.sleep(Math.round(Math.random() * 5)); 94 | if (state.throwError) { 95 | throw new ValueException('I hit an error'); 96 | } 97 | trace('I am a thread completing ${state.fid}'); 98 | return '${state.fid} completed'; 99 | } 100 | 101 | /** 102 | * Completion handling function 103 | * @param futureId the future that called this function 104 | * @param message the completed result from the task 105 | */ 106 | function futureComplete(futureId:Int, message:String) { 107 | trace('COMPLETE(${futureId}):Got int = ${message}'); 108 | numCompleted++; 109 | } 110 | 111 | /** 112 | * Error handling function. 113 | * @param futureId the future that called this function 114 | * @param error the error that was raised. Note that the 115 | * error function must know how to handle whatever the 116 | * `error` Dynamic is. 117 | */ 118 | function futureError(futureId:Int, error:Dynamic) { 119 | trace('ERROR(${futureId}):Got int from Future = ${error}'); 120 | numErrored++; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lime/simple-futures/project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lime/simple-promises/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Build + Debug", 9 | "type": "lime", 10 | "request": "launch" 11 | }, 12 | { 13 | "name": "Debug", 14 | "type": "lime", 15 | "request": "launch", 16 | "preLaunchTask": null 17 | }, 18 | { 19 | "name": "HashLink (debugger)", 20 | "type": "hl", 21 | "request": "launch", 22 | "hl": "D:\\Program Files\\HashLink1.14\\hl.exe", 23 | "cwd": "Export\\hl\\bin", 24 | "program": "hl\\bin\\hlboot.dat" 25 | }, 26 | ] 27 | } -------------------------------------------------------------------------------- /lime/simple-promises/Assets/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/47rooks/haxe-threading-examples/e62ccab44d7b78c4030503711f2a125d2c777ca3/lime/simple-promises/Assets/.gitignore -------------------------------------------------------------------------------- /lime/simple-promises/SimplePromises.hxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | "$(CompilerPath)/haxelib" run lime build "$(OutputFile)" $(TargetBuild) -$(BuildConfig) -Dfdb 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /lime/simple-promises/Source/SimplePromise.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.Exception; 4 | import haxe.Timer; 5 | import haxe.ValueException; 6 | import lime.app.Application; 7 | import lime.app.Future; 8 | import lime.app.Promise; 9 | import lime.system.System; 10 | import lime.ui.KeyCode; 11 | import lime.ui.KeyModifier; 12 | 13 | /** 14 | * SimplePromise demonstrates use of a Promise to update the state 15 | * of a Future. This is the purpose of a Promise and they are used 16 | * by Futures to drive Future updates from threads, or similarly 17 | * in ThreadPools themselves. 18 | * 19 | * Note that Promises are not themselves multi-threaded. They are a 20 | * tool that can be used in threads to manipulate Futures. 21 | */ 22 | class SimplePromise extends Application { 23 | final TOTAL_ITERATIONS = 10; 24 | var promisesCreated = false; 25 | var promise:Promise; 26 | var future:Future; 27 | var iteration:Int; 28 | var raiseError:Bool = false; 29 | 30 | public function new() { 31 | super(); 32 | } 33 | 34 | override public function update(deltaTime:Int):Void { 35 | super.update(deltaTime); 36 | 37 | /* Check termination condition. This is only for the demo 38 | * as otherwise you have to go and shutdown the lime 39 | * application window. 40 | */ 41 | if (future != null && (future.isComplete || future.isError)) { 42 | trace('All tasks completed. Exiting'); 43 | System.exit(0); 44 | } 45 | 46 | /* If this is the first time through create the Promise 47 | * and the timer function that will use it to update 48 | * its Future. 49 | */ 50 | if (!promisesCreated) { 51 | promise = new Promise(); 52 | 53 | iteration = 0; 54 | future = promise.future; 55 | 56 | // Setup handler functions 57 | future.onComplete(promiseComplete); 58 | future.onProgress(promiseProgress); 59 | future.onError(promiseError); 60 | 61 | /* Create a closure over the Promise so it 62 | * can report it progress and outcome through it. 63 | * Note, if you breakpoint in a debugger in the 64 | * timer.run function below, you will see that there 65 | * is only one thread and that this function is 66 | * running in the main thread. 67 | */ 68 | var progress = 0; 69 | var total = 10; 70 | var timer = new Timer(1000); 71 | timer.run = function() { 72 | promise.progress(progress, total); 73 | progress++; 74 | 75 | if (raiseError) { 76 | promise.error(new ValueException('I got an error')); 77 | } 78 | 79 | if (progress == total) { 80 | promise.complete("Done!"); 81 | timer.stop(); 82 | } 83 | }; 84 | 85 | promisesCreated = true; 86 | } 87 | } 88 | 89 | /** 90 | * If you want to have the Promise cause the Future to error hit 'E'. 91 | * @param key the Lime key keycode. 92 | * @param modifier the Lime modifier key keycode. 93 | */ 94 | public override function onKeyUp(key:KeyCode, modifier:KeyModifier):Void { 95 | switch (key) { 96 | case E: 97 | raiseError = true; 98 | default: 99 | }; 100 | } 101 | 102 | /** 103 | * The Future completion handling function. 104 | * 105 | * 106 | * @param result the result of the completed Future. Note, that there 107 | * is an application level agreement on the type of the Promise's 108 | * Future and the parameter to this function. 109 | */ 110 | function promiseComplete(result:String):Void { 111 | trace('COMPLETE: result is ${result}'); 112 | } 113 | 114 | /** 115 | * The progress handling function for the Promise's Future. 116 | * 117 | * @param progress the amount of progress made. 118 | * @param total the total progress that will be made by completion. 119 | */ 120 | function promiseProgress(progress:Int, total:Int):Void { 121 | trace('PROGRESS: ${progress} of ${total}'); 122 | } 123 | 124 | /** 125 | * The error handling function for the Promise's Future. 126 | * 127 | * @param ex the exception. Note, that there is an application level 128 | * agreement on the type of the errors that the Promise may raise 129 | * and the parameter to this function. This is a Dynamic in the original 130 | * API. Here we use an Exception but it could be anything. 131 | */ 132 | function promiseError(ex:Exception):Void { 133 | trace('ERROR: got exception ${ex}'); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lime/simple-promises/project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lime/simple-threadpool/Assets/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/47rooks/haxe-threading-examples/e62ccab44d7b78c4030503711f2a125d2c777ca3/lime/simple-threadpool/Assets/.gitignore -------------------------------------------------------------------------------- /lime/simple-threadpool/SimpleThreadpool.hxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | "$(CompilerPath)/haxelib" run lime build "$(OutputFile)" $(TargetBuild) -$(BuildConfig) -Dfdb 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /lime/simple-threadpool/Source/SimpleThreadpool.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.Exception; 4 | import haxe.Timer; 5 | import haxe.ValueException; 6 | import lime.app.Application; 7 | import lime.system.System; 8 | import lime.system.ThreadPool; 9 | import lime.system.WorkOutput; 10 | 11 | /** 12 | * The current state of the work function. This one is just used 13 | * for the input values. See `CancellableFibonacciState` for 14 | * an example that stores intermediate state. 15 | */ 16 | @:structInit class FibonacciState { 17 | public var i1:Int; // Initial value 1 18 | public var i2:Int; // Initial value 2 19 | } 20 | 21 | /** 22 | * The current state of the work function. For cancellation to work 23 | * it is necessary for the function to return periodically. If it has 24 | * not been cancelled it will be called again with the same `State` 25 | * object so it can resume where it left off. In order for this to work 26 | * the job must stash its current state in this object before returning. 27 | */ 28 | @:structInit class CancellableFibonacciState { 29 | public var iteration:Int; // the current iteration 30 | public var partialResult:Array; // The current sequence values 31 | } 32 | 33 | /** 34 | * A custom error object if the job needs to return application specific 35 | * errors to the main thread. 36 | * The `onError()` function must understand this object. 37 | */ 38 | @:structInit class FibonacciError { 39 | public var id:Int; // Job id 40 | public var exception:Exception; 41 | } 42 | 43 | /** 44 | * A simple example progress object. 45 | * The `onProgress()` function must understand this object. 46 | */ 47 | @:structInit class FibonacciProgress { 48 | public var id:Int; // Job id 49 | public var iterationsCompleted:Int; 50 | } 51 | 52 | /** 53 | * This is the final result object. When the job completes it will 54 | * send one of these object via the `sendComplete()` function. 55 | * The `onComplete` function must understand this object. 56 | */ 57 | @:structInit class FibonacciResult { 58 | public var id:Int; // Job id 59 | public var sequence:Array; 60 | } 61 | 62 | /** 63 | * SimpleThreadPool attempts to explore the basic functions of ThreadPools 64 | * in running jobs. There are a number of jobs submitted all computing 65 | * the Fibonacci sequence. This is the work function, the function that 66 | * does the actual piece of work the application cares about. Now here all 67 | * threads do the same thing, while in a real application even if they all 68 | * used the same function they would likely use different data. 69 | * A more realistic example will follow now that this simple example has 70 | * gotten us used to how this framework works. 71 | * 72 | * This file is heavily commented so please read all the comments carefully 73 | * and hopefully it will all make sense. 74 | */ 75 | class SimpleThreadpool extends Application { 76 | /** 77 | * The number of jobs to schedule. 78 | * Note that the number should be > 4 as Job 3 79 | * is used to show cancellation behaviour. 80 | */ 81 | final NUM_JOBS = 10; 82 | 83 | /** 84 | * The maximum number of threads in the pool. 85 | * Vary this number to see the impact on elapsed time 86 | * of the run of all the jobs. A total elapsed time is 87 | * printed at the end. 88 | */ 89 | final MAX_THREADS = 10; 90 | 91 | /** 92 | * The total number of iterations of the Fibonacci calculations. 93 | * This is a trivial work function so the number of iterations 94 | * needs to be high. A more complex function like the Sieve of 95 | * Eratosthenes would no doubt have been a better choice for an 96 | * example. 97 | */ 98 | final NUM_ITERATIONS = 10000000; 99 | 100 | /** 101 | * In the job that throws an error, call sendError() with an 102 | * exception if this is true, else simply throw a ValueException. 103 | */ 104 | final SEND_ERROR = false; 105 | 106 | /** 107 | * Set this to true to use a properly cancellable work function. 108 | * Refer to `computeFibonacci()` and `cancellableComputeFibonacci()` 109 | * for details. 110 | */ 111 | final USE_CANCELLABLE_WORK_FUNCTION = true; 112 | 113 | /** 114 | * If you want to see a print of the number of threads currently 115 | * in the pool set this to true. It will of course impact overall 116 | * runtime. 117 | */ 118 | final MONITOR_THREADS_IN_POOL = false; 119 | 120 | var _tp:ThreadPool; 121 | var jobsStarted:Bool = false; 122 | var jobsCompleted = 0; 123 | var sTime:Float; 124 | 125 | public function new() { 126 | super(); 127 | } 128 | 129 | override public function update(deltaTime:Int):Void { 130 | super.update(deltaTime); 131 | 132 | /* Check termination condition. This is only for the demo 133 | * as otherwise you have to go and shutdown the lime 134 | * application window. 135 | */ 136 | if (jobsStarted && jobsCompleted == NUM_JOBS) { 137 | trace('All tasks completed. Exiting'); 138 | var eTime = Timer.stamp(); 139 | trace('End time=${eTime}'); 140 | trace('Elapsed time=${eTime - sTime} seconds.'); 141 | System.exit(0); 142 | } 143 | 144 | /** 145 | * Kick off jobs only if this hasn't already been done. 146 | * ThreadPool creation is done here because there is something 147 | * about creating the pool in the constructor that leads to a 148 | * null access exception. If that gets fixed I'll likely move this. 149 | */ 150 | if (!jobsStarted) { 151 | // Create threadpool and set handlers 152 | _tp = new ThreadPool(0, MAX_THREADS, MULTI_THREADED); 153 | _tp.onComplete.add(onComplete); 154 | _tp.onError.add(onError); 155 | _tp.onProgress.add(onProgress); 156 | 157 | /* Cache start time 158 | * If you change the number of threads in the pool 159 | * you can use the elapsed time print at the end of the 160 | * run to see the effect of MT on the execution time. 161 | */ 162 | sTime = Timer.stamp(); 163 | trace('Start time=${sTime}'); 164 | for (i in 0...NUM_JOBS) { 165 | var jobId = -1; 166 | // This is where the job itself is scheduled 167 | if (USE_CANCELLABLE_WORK_FUNCTION) { 168 | var s:CancellableFibonacciState = { 169 | iteration: 0, 170 | partialResult: null 171 | }; 172 | jobId = _tp.run(cancellableComputeFibonacci, s); 173 | } else { 174 | var s:FibonacciState = {i1: 1, i2: 1}; 175 | jobId = _tp.run(computeFibonacci, s); 176 | } 177 | trace('jobid=${jobId} started'); 178 | } 179 | jobsStarted = true; 180 | 181 | /* A quick example of cancelling a running job. 182 | * this requires that job 3 run for more than 200 milliseconds. 183 | * Note that, if you are running the computeFibonacci() work function 184 | * that cancellation will not actually stop the thread. That is why 185 | * job 3 outputs a message so that is obvious. If you use the 186 | * cancellableComputeFibonacci() work function it should stop soon 187 | * after the cancellation call is made. 188 | */ 189 | Timer.delay(() -> { 190 | trace('Timer cancelling job 3'); 191 | _tp.cancelJob(3); 192 | jobsCompleted++; 193 | }, 200); 194 | } 195 | 196 | // Monitor number of active threads 197 | if (MONITOR_THREADS_IN_POOL) { 198 | trace('num of threads in pool=${_tp.currentThreads}'); 199 | } 200 | } 201 | 202 | /** 203 | * A basic Fibonacci sequence calculator. 204 | * This also shows updating progress and throwing exceptions or 205 | * sending errors with sendError(). 206 | * @param state the initial values to start the sequence at. Strictly 207 | * for a true Fibonacci there are both 1, but here we can pick what 208 | * we want. 209 | * @param output this is the WorkOutput object for communicating with 210 | * the main thread. 211 | */ 212 | function computeFibonacci(state:FibonacciState, output:WorkOutput) { 213 | try { 214 | var rv = new Array(); 215 | rv.push(state.i1); 216 | rv.push(state.i2); 217 | for (i in 0...NUM_ITERATIONS) { 218 | rv[i + 2] = rv[i] + rv[i + 1]; 219 | 220 | if (output.activeJob.id == NUM_JOBS / 2 && i == NUM_ITERATIONS / 2) { 221 | if (SEND_ERROR) { 222 | output.sendError({id: output.activeJob.id, exception: new ValueException('computeFibonacci failed')}); 223 | // After calling sendError() the job must terminate 224 | return; 225 | } 226 | throw new ValueException('ooops'); 227 | } 228 | 229 | /* If this is jobid 3 then send progress reports every 5% of the way through 230 | * the job. The reason for the trace is that it demonstrates that even when this 231 | * job is cancelled it continues running. But while it continues running the 232 | * sendProgress() messages are cut off by the framework so the main thread will 233 | * not see these updates even though they are still being sent. 234 | */ 235 | if (output.activeJob.id == 3 && i % (NUM_ITERATIONS / 20) == 0) { 236 | var p:FibonacciProgress = {id: output.activeJob.id, iterationsCompleted: i}; 237 | output.sendProgress(p); 238 | trace('It is me 3 !'); 239 | } 240 | } 241 | 242 | // Send the final job completion output to the main thread. 243 | var c:FibonacciResult = {id: output.activeJob.id, sequence: rv}; 244 | output.sendComplete(c); 245 | } catch (e:Dynamic) { 246 | trace('getting an error=$e'); 247 | output.sendError(e); 248 | } 249 | } 250 | 251 | /** 252 | * A basic Fibonacci sequence calculator. 253 | * This version of the function shows how to make the job properly 254 | * cancellable. It periodically updates its `State` and returns 255 | * and restarts from where it left off until it finally completes. 256 | * 257 | * @param state the initial values to start the sequence at. Strictly 258 | * for a true Fibonacci there are both 1, but here we can pick what 259 | * we want. 260 | * @param output this is the WorkOutput object for communicating with 261 | * the main thread. 262 | */ 263 | function cancellableComputeFibonacci(state:CancellableFibonacciState, output:WorkOutput) { 264 | var rv = state.partialResult; 265 | if (rv == null) { 266 | // This is the first call to this work function, so initialize the sequence. 267 | rv = new Array(); 268 | rv.push(1); 269 | rv.push(1); 270 | } 271 | for (i in state.iteration...NUM_ITERATIONS) { 272 | rv[i + 2] = rv[i] + rv[i + 1]; 273 | 274 | /* If this is jobid 3 then send progress reports every 5% of the way through 275 | * the job. The reason for the trace is that it demonstrates that when this 276 | * job is cancelled it actually stops. This differs from `computeFibonacci()` 277 | * which continues running. 278 | */ 279 | if (output.activeJob.id == 3 && i % (NUM_ITERATIONS / 20) == 0) { 280 | var p:FibonacciProgress = {id: output.activeJob.id, iterationsCompleted: i}; 281 | output.sendProgress(p); 282 | trace('It is me 3 !'); 283 | } 284 | 285 | /* Check for cancellation */ 286 | if (i % (NUM_ITERATIONS / 20000) == 0) { 287 | // Stash the current state so we can restart. 288 | state.partialResult = rv; 289 | state.iteration = i + 1; 290 | return; 291 | } 292 | } 293 | 294 | // Send the final job completion output to the main thread. 295 | var c:FibonacciResult = {id: output.activeJob.id, sequence: rv}; 296 | output.sendComplete(c); 297 | } 298 | 299 | /** 300 | * This is the main thread completion function. It recieves the 301 | * result that the thread pool thread sent via `sendCompletion()`. 302 | * 303 | * @param result the resulting Fibonacci sequence including all NUM_ITERATIONS + 2 304 | * numbers. 305 | */ 306 | function onComplete(result:FibonacciResult):Void { 307 | trace('(COMPLETED) Job ${result.id} returned sequence starting at ${result.sequence[0]} and ending at ${result.sequence[result.sequence.length - 1]}'); 308 | if (result.sequence.length != NUM_ITERATIONS + 2) { 309 | trace('Job ${_tp.activeJob.id} return the wrong number of elements (${result.sequence.length})'); 310 | } 311 | jobsCompleted++; 312 | } 313 | 314 | /** 315 | * This is the main thread error handling function. In this case it 316 | * handles the custom FibonacciError structure or the regular Haxe exception. 317 | * 318 | * @param errorInfo this is a Dynamic and must be dynamically checked for correct 319 | * handling, because there are two possibilities in this example. 320 | */ 321 | function onError(errorInfo:Dynamic):Void { 322 | trace('type=${Type.typeof(errorInfo)}, error=${errorInfo}'); 323 | if (errorInfo is Exception) { 324 | trace('(ERROR) Job ${_tp.activeJob.id} Got exception ${Type.typeof(errorInfo)}:${errorInfo}'); 325 | } else if (Reflect.hasField(errorInfo, 'id') && Reflect.hasField(errorInfo, 'exception')) { 326 | trace('(ERROR) Job ${_tp.activeJob.id} Got application error ${errorInfo.id}: ${errorInfo.exception}'); 327 | trace('errorInfo=${errorInfo}'); 328 | } else { 329 | trace('(ERROR) Job ${_tp.activeJob.id} Got unknown error type: ${errorInfo}'); 330 | } 331 | jobsCompleted++; 332 | } 333 | 334 | /** 335 | * This is the main thread progress function. This simply reports the number 336 | * the job id and the number of iterations it has completed. 337 | * 338 | * @param progressInfo the custom progress object. 339 | */ 340 | function onProgress(progressInfo:FibonacciProgress):Void { 341 | trace('(PROGRESS) Job ${progressInfo.id}: ${progressInfo.iterationsCompleted}'); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /lime/simple-threadpool/project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------