├── .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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
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 |
51 |
52 |
53 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
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 |
51 |
52 |
53 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
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 |
51 |
52 |
53 |
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 |
--------------------------------------------------------------------------------