Pārlūkot izejas kodu

Per-thread event loop (#9868)

Aleksandr Kuzmenko 5 gadi atpakaļ
vecāks
revīzija
3e4e6288f0
40 mainītis faili ar 1359 papildinājumiem un 388 dzēšanām
  1. 1 0
      src/macro/eval/evalHash.ml
  2. 1 0
      src/macro/eval/evalMain.ml
  3. 11 0
      src/macro/eval/evalStdLib.ml
  4. 1 0
      src/macro/eval/evalThread.ml
  5. 1 0
      src/macro/eval/evalValue.ml
  6. 0 6
      src/optimization/dce.ml
  7. 34 11
      src/typing/finalization.ml
  8. 139 16
      std/cpp/_std/sys/thread/Thread.hx
  9. 80 15
      std/cs/_std/sys/thread/Thread.hx
  10. 48 3
      std/eval/_std/sys/thread/Thread.hx
  11. 3 0
      std/eval/vm/NativeThread.hx
  12. 14 1
      std/haxe/EntryPoint.hx
  13. 14 29
      std/haxe/Timer.hx
  14. 122 38
      std/hl/_std/sys/thread/Thread.hx
  15. 126 51
      std/java/_std/sys/thread/Thread.hx
  16. 0 107
      std/jvm/_std/sys/thread/Thread.hx
  17. 125 74
      std/neko/_std/sys/thread/Thread.hx
  18. 92 16
      std/python/_std/sys/thread/Thread.hx
  19. 230 0
      std/sys/thread/EventLoop.hx
  20. 9 0
      std/sys/thread/NoEventLoopException.hx
  21. 39 3
      std/sys/thread/Thread.hx
  22. 2 0
      tests/eventLoop/.gitignore
  23. 5 0
      tests/eventLoop/build.hxml
  24. 11 0
      tests/eventLoop/src/Main.hx
  25. 103 0
      tests/eventLoop/src/cases/TestEvents.hx
  26. 92 0
      tests/eventLoop/src/cases/TestTimer.hx
  27. 3 0
      tests/eventLoop/src/import.hx
  28. 1 0
      tests/runci/Config.hx
  29. 6 0
      tests/runci/targets/Cpp.hx
  30. 4 0
      tests/runci/targets/Cs.hx
  31. 4 0
      tests/runci/targets/Java.hx
  32. 4 0
      tests/runci/targets/Jvm.hx
  33. 3 0
      tests/runci/targets/Macro.hx
  34. 4 0
      tests/runci/targets/Neko.hx
  35. 6 0
      tests/runci/targets/Python.hx
  36. 4 3
      tests/threads/src/cases/Issue3767.hx
  37. 4 4
      tests/threads/src/cases/Issue4878.hx
  38. 4 4
      tests/threads/src/cases/Issue8063.hx
  39. 4 4
      tests/threads/src/cases/TestThreads.hx
  40. 5 3
      tests/threads/src/cases/WeirdTreeSum.hx

+ 1 - 0
src/macro/eval/evalHash.ml

@@ -136,6 +136,7 @@ let key_sys_net_Mutex = hash "sys.thread.Mutex"
 let key_sys_net_Lock = hash "sys.thread.Lock"
 let key_sys_net_Tls = hash "sys.thread.Tls"
 let key_sys_net_Deque = hash "sys.thread.Deque"
+let key_sys_thread_EventLoop = hash "sys.thread.EventLoop"
 
 let key_mbedtls_Config = hash "mbedtls.Config"
 let key_mbedtls_CtrDrbg = hash "mbedtls.CtrDrbg"

+ 1 - 0
src/macro/eval/evalMain.ml

@@ -95,6 +95,7 @@ let create com api is_macro =
 	let thread = {
 		tthread = Thread.self();
 		tstorage = IntMap.empty;
+		tevents = vnull;
 		tdeque = EvalThread.Deque.create();
 	} in
 	let eval = EvalThread.create_eval thread in

+ 11 - 0
src/macro/eval/evalStdLib.ml

@@ -2733,6 +2733,15 @@ module StdThread = struct
 		vint (Thread.id (this vthis).tthread)
 	)
 
+	let get_events = vifun0 (fun vthis ->
+		(this vthis).tevents
+	)
+
+	let set_events = vifun1 (fun vthis v ->
+		(this vthis).tevents <- v;
+		v
+	)
+
 	let join = vfun1 (fun thread ->
 		Thread.join (this thread).tthread;
 		vnull
@@ -3665,6 +3674,8 @@ let init_standard_library builtins =
 		"yield",StdThread.yield;
 	] [
 		"id",StdThread.id;
+		"get_events",StdThread.get_events;
+		"set_events",StdThread.set_events;
 		"kill",StdThread.kill;
 		"sendMessage",StdThread.sendMessage;
 	];

+ 1 - 0
src/macro/eval/evalThread.ml

@@ -111,6 +111,7 @@ let spawn ctx f =
 	let thread = {
 		tthread = Obj.magic ();
 		tstorage = IntMap.empty;
+		tevents = vnull;
 		tdeque = Deque.create();
 	} in
 	thread.tthread <- Thread.create f thread;

+ 1 - 0
src/macro/eval/evalValue.ml

@@ -203,6 +203,7 @@ and venum_value = {
 and vthread = {
 	mutable tthread : Thread.t;
 	tdeque : vdeque;
+	mutable tevents : value;
 	mutable tstorage : value IntMap.t;
 }
 

+ 0 - 6
src/optimization/dce.ml

@@ -860,12 +860,6 @@ let run com main mode =
 		features = Hashtbl.create 0;
 		curclass = null_class;
 	} in
-	begin match main with
-		| Some {eexpr = TCall({eexpr = TField(e,(FStatic(c,cf)))},_)} | Some {eexpr = TBlock ({ eexpr = TCall({eexpr = TField(e,(FStatic(c,cf)))},_)} :: _)} ->
-			cf.cf_meta <- (mk_keep_meta cf.cf_pos) :: cf.cf_meta
-		| _ ->
-			()
-	end;
 	List.iter (fun m ->
 		List.iter (fun (s,v) ->
 			if Hashtbl.mem dce.features s then Hashtbl.replace dce.features s (v :: Hashtbl.find dce.features s)

+ 34 - 11
src/typing/finalization.ml

@@ -43,20 +43,43 @@ let get_main ctx types =
 			| _ -> error ("Invalid -main : " ^ s_type_path path ^ " has invalid main function") c.cl_pos
 		in
 		if not (ExtType.is_void (follow r)) then error (Printf.sprintf "Return type of main function should be Void (found %s)" (s_type (print_context()) r)) f.cf_name_pos;
+		f.cf_meta <- (Dce.mk_keep_meta f.cf_pos) :: f.cf_meta;
 		let emain = type_module_type ctx (TClassDecl c) None null_pos in
 		let main = mk (TCall (mk (TField (emain,fmode)) ft null_pos,[])) r null_pos in
-		(* add haxe.EntryPoint.run() call *)
-		let main = (try
-			let et = List.find (fun t -> t_path t = (["haxe"],"EntryPoint")) types in
+		let call_static path method_name =
+			let et = List.find (fun t -> t_path t = path) types in
 			let ec = (match et with TClassDecl c -> c | _ -> die "" __LOC__) in
-			let ef = PMap.find "run" ec.cl_statics in
-			let p = null_pos in
-			let et = mk (TTypeExpr et) (mk_anon (ref (Statics ec))) p in
-			let call = mk (TCall (mk (TField (et,FStatic (ec,ef))) ef.cf_type p,[])) ctx.t.tvoid p in
-			mk (TBlock [main;call]) ctx.t.tvoid p
-		with Not_found ->
-			main
-		) in
+			let ef = PMap.find method_name ec.cl_statics in
+			let et = mk (TTypeExpr et) (mk_anon (ref (Statics ec))) null_pos in
+			mk (TCall (mk (TField (et,FStatic (ec,ef))) ef.cf_type null_pos,[])) ctx.t.tvoid null_pos
+		in
+		(* add haxe.EntryPoint.run() call *)
+		let add_entry_point_run main =
+			(try
+				main :: [call_static (["haxe"],"EntryPoint") "run"]
+			with Not_found ->
+				[main]
+			)
+		in
+		(* add calls for event loop *)
+		let add_event_loop main =
+			(try
+				let thread = (["sys";"thread";"_Thread"],"Thread_Impl_") in
+				call_static thread "initEventLoop" :: add_entry_point_run main @ [call_static thread "processEvents"]
+			with Not_found ->
+				[main]
+			)
+		in
+		let main =
+			(* Threaded targets run event loops per thread *)
+			let exprs =
+				if ctx.com.config.pf_supports_threads then add_event_loop main
+				else add_entry_point_run main
+			in
+			match exprs with
+			| [e] -> e
+			| _ -> mk (TBlock exprs) ctx.t.tvoid p
+		in
 		Some main
 
 let finalize ctx =

+ 139 - 16
std/cpp/_std/sys/thread/Thread.hx

@@ -22,37 +22,160 @@
 
 package sys.thread;
 
+abstract Thread(HaxeThread) from HaxeThread to HaxeThread {
+	public var events(get,never):EventLoop;
+
+	public inline function sendMessage(msg:Dynamic):Void {
+		this.sendMessage(msg);
+	}
+
+	public static inline function current():Thread {
+		return HaxeThread.current();
+	}
+
+	public static inline function create(job:()->Void):Thread {
+		return HaxeThread.create(job, false);
+	}
+
+	public static inline function runWithEventLoop(job:()->Void):Void {
+		HaxeThread.runWithEventLoop(job);
+	}
+
+	public static inline function createWithEventLoop(job:()->Void):Thread {
+		return HaxeThread.create(job, true);
+	}
+
+	public static function readMessage(block:Bool):Dynamic {
+		return HaxeThread.readMessage(block);
+	}
+
+	function get_events():EventLoop {
+		if(this.events == null)
+			throw new NoEventLoopException();
+		return this.events;
+	}
+
+	@:keep
+	static function initEventLoop() {
+		@:privateAccess HaxeThread.current().events = new EventLoop();
+	}
+
+	@:keep
+	static public function processEvents() {
+		HaxeThread.current().events.loop();
+	}
+}
+
 @:callable
 @:coreType
 private abstract ThreadHandle {}
 
-abstract Thread(ThreadHandle) {
-	inline function new(h:ThreadHandle):Void {
-		this = h;
+private class HaxeThread {
+	static final threads = new Array<{thread:HaxeThread, handle:ThreadHandle}>();
+	static final threadsMutex = new Mutex();
+	static var mainThreadHandle:ThreadHandle = currentHandle();
+	static var mainThread:HaxeThread = new HaxeThread(currentHandle());
+
+	public var events(default,null):Null<EventLoop>;
+	public var handle:ThreadHandle;
+	final messages = new Deque<Dynamic>();
+
+	static public function current():HaxeThread {
+		var handle = currentHandle();
+		if(handle == mainThreadHandle) {
+			return mainThread;
+		}
+		threadsMutex.acquire();
+		var thread = null;
+		for(item in threads) {
+			if(item.handle == handle) {
+				thread = item.thread;
+				break;
+			}
+		}
+		if(thread == null) {
+			thread = new HaxeThread(handle);
+			threads.push({thread:thread, handle:handle});
+		}
+		threadsMutex.release();
+		return thread;
 	}
 
-	public inline function sendMessage(msg:Dynamic):Void {
-		untyped __global__.__hxcpp_thread_send(this, msg);
+	public static function create(job:()->Void, withEventLoop:Bool):Thread {
+		var item = {handle:null, thread:new HaxeThread(null)};
+		threadsMutex.acquire();
+		var index = threads.push(item);
+		threadsMutex.release();
+		if(withEventLoop)
+			item.thread.events = new EventLoop();
+		item.handle = createHandle(() -> {
+			if(item.thread.handle == null) {
+				item.handle = currentHandle();
+				item.thread.handle = item.handle;
+			}
+			try {
+				job();
+				if(withEventLoop)
+					item.thread.events.loop();
+			} catch(e) {
+				dropThread(item, index);
+				throw e;
+			}
+			dropThread(item, index);
+		});
+		item.thread.handle = item.handle;
+		return item.thread;
 	}
 
-	public static inline function current():Thread {
-		return new Thread(untyped __global__.__hxcpp_thread_current());
+	public static function runWithEventLoop(job:()->Void):Void {
+		var thread = current();
+		if(thread.events == null) {
+			thread.events = new EventLoop();
+			try {
+				job();
+				thread.events.loop();
+				thread.events = null;
+			} catch(e) {
+				thread.events = null;
+				throw e;
+			}
+		} else {
+			job();
+		}
 	}
 
-	public static inline function create(callb:Void->Void):Thread {
-		return new Thread(untyped __global__.__hxcpp_thread_create(callb));
+	static function dropThread(item, probableIndex:Int) {
+		threadsMutex.acquire();
+		if(threads[probableIndex] == item) {
+			threads.splice(probableIndex, 1);
+		} else {
+			for(i => item2 in threads) {
+				if(item2 == item) {
+					threads.splice(i, 1);
+					break;
+				}
+			}
+		}
+		threadsMutex.release();
 	}
 
-	public static function readMessage(block:Bool):Dynamic {
-		return untyped __global__.__hxcpp_thread_read_message(block);
+	function new(h:ThreadHandle):Void {
+		handle = h;
+	}
+
+	public inline function sendMessage(msg:Dynamic):Void {
+		messages.add(msg);
+	}
+
+	static inline function currentHandle():ThreadHandle {
+		return untyped __global__.__hxcpp_thread_current();
 	}
 
-	@:op(A == B)
-	public inline function equals(other:Thread):Bool {
-		return getHandle() == other.getHandle();
+	static inline function createHandle(callb:Void->Void):ThreadHandle {
+		return untyped __global__.__hxcpp_thread_create(callb);
 	}
 
-	private inline function getHandle():ThreadHandle {
-		return this;
+	public static inline function readMessage(block:Bool):Dynamic {
+		return current().messages.pop(block);
 	}
 }

+ 80 - 15
std/cs/_std/sys/thread/Thread.hx

@@ -27,14 +27,38 @@ import cs.system.WeakReference;
 import cs.Lib;
 
 abstract Thread(HaxeThread) {
+	public var events(get,never):EventLoop;
+
 	inline function new(thread:HaxeThread) {
 		this = thread;
 	}
 
-	public static function create(cb:Void->Void):Thread {
-		var native = new NativeThread(cb);
+	public static function create(job:Void->Void):Thread {
+		var hx:Null<HaxeThread> = null;
+		var native = new NativeThread(job);
+		native.IsBackground = true;
+		hx = HaxeThread.allocate(native, false);
+		native.Start();
+
+		return new Thread(hx);
+	}
+
+	public static inline function runWithEventLoop(job:()->Void):Void {
+		HaxeThread.runWithEventLoop(job);
+	}
+
+	public static inline function createWithEventLoop(job:()->Void):Thread {
+		var hx:Null<HaxeThread> = null;
+		var native = new NativeThread(() -> {
+			job();
+			if(hx == null) {
+				HaxeThread.get(NativeThread.CurrentThread).events.loop();
+			} else {
+				hx.events.loop();
+			}
+		});
 		native.IsBackground = true;
-		var hx = HaxeThread.allocate(native);
+		hx = HaxeThread.allocate(native, true);
 		native.Start();
 
 		return new Thread(hx);
@@ -55,30 +79,53 @@ abstract Thread(HaxeThread) {
 	inline function readMessageImpl(block:Bool):Dynamic {
 		return this.readMessage(block);
 	}
+
+	function get_events():EventLoop {
+		if(this.events == null)
+			throw new NoEventLoopException();
+		return this.events;
+	}
+
+	@:keep
+	static function initEventLoop():Void {
+		@:privateAccess HaxeThread.get(NativeThread.CurrentThread).events = new EventLoop();
+	}
+
+	@:keep
+	static function processEvents():Void {
+		HaxeThread.get(NativeThread.CurrentThread).events.loop();
+	}
 }
 
 private class HaxeThread {
+	static final mainNativeThread = NativeThread.CurrentThread;
+	static final mainHaxeThread = new HaxeThread(NativeThread.CurrentThread);
 	static final threads = new Map<Int, WeakReference>();
+	static final threadsMutex = new cs.system.threading.Mutex();
 	static var allocateCount = 0;
 
 	public final native:NativeThread;
+	public var events(default,null):Null<EventLoop>;
 
 	final messages = new Deque<Dynamic>();
 
 	public static function get(native:NativeThread):HaxeThread {
+		if(native == mainNativeThread) {
+			return mainHaxeThread;
+		}
 		var native = NativeThread.CurrentThread;
-		var ref:Null<WeakReference> = null;
-		Lib.lock(threads, {
-			var key = native.ManagedThreadId;
-			ref = threads.get(key);
-		});
+		var key = native.ManagedThreadId;
+		threadsMutex.WaitOne();
+		var ref = threads.get(key);
+		threadsMutex.ReleaseMutex();
 		if (ref == null || !ref.IsAlive) {
-			return allocate(native);
+			return allocate(native, false);
 		}
 		return ref.Target;
 	}
 
-	public static function allocate(native:NativeThread):HaxeThread {
+	public static function allocate(native:NativeThread, withEventLoop:Bool):HaxeThread {
+		threadsMutex.WaitOne();
 		allocateCount++;
 		inline function cleanup() {
 			if (allocateCount % 100 == 0) {
@@ -90,15 +137,33 @@ private class HaxeThread {
 			}
 		}
 		var hx = new HaxeThread(native);
+		if(withEventLoop)
+			hx.events = new EventLoop();
 		var ref = new WeakReference(hx);
-		Lib.lock(threads, {
-			cleanup();
-			threads.set(native.ManagedThreadId, ref);
-		});
+		cleanup();
+		threads.set(native.ManagedThreadId, ref);
+		threadsMutex.ReleaseMutex();
 		return hx;
 	}
 
-	public function new(native:NativeThread) {
+	public static function runWithEventLoop(job:()->Void):Void {
+		var thread = get(NativeThread.CurrentThread);
+		if(thread.events == null) {
+			thread.events = new EventLoop();
+			try {
+				job();
+				thread.events.loop();
+				thread.events = null;
+			} catch(e) {
+				thread.events = null;
+				throw e;
+			}
+		} else {
+			job();
+		}
+	}
+
+	function new(native:NativeThread) {
 		this.native = native;
 	}
 

+ 48 - 3
std/eval/_std/sys/thread/Thread.hx

@@ -25,6 +25,8 @@ package sys.thread;
 import eval.vm.NativeThread;
 
 abstract Thread(NativeThread) {
+	public var events(get,never):EventLoop;
+
 	inline function new(h:NativeThread):Void {
 		this = h;
 	}
@@ -37,8 +39,34 @@ abstract Thread(NativeThread) {
 		return new Thread(NativeThread.self());
 	}
 
-	public static inline function create(callb:Void->Void):Thread {
-		return new Thread(new NativeThread(callb));
+	public static inline function create(job:()->Void):Thread {
+		return new Thread(new NativeThread(job));
+	}
+
+	public static function runWithEventLoop(job:()->Void):Void {
+		var thread = NativeThread.self();
+		if(thread.events == null) {
+			thread.events = new EventLoop();
+			try {
+				job();
+				thread.events.loop();
+				thread.events = null;
+			} catch(e) {
+				thread.events = null;
+				throw e;
+			}
+		} else {
+			job();
+		}
+	}
+
+	public static inline function createWithEventLoop(job:()->Void):Thread {
+		return new Thread(new NativeThread(() -> {
+			var thread = NativeThread.self();
+			thread.events = new EventLoop();
+			job();
+			thread.events.loop();
+		}));
 	}
 
 	public static inline function readMessage(block:Bool):Dynamic {
@@ -54,7 +82,24 @@ abstract Thread(NativeThread) {
 		return getHandle().id() == other.getHandle().id();
 	}
 
-	private inline function getHandle():NativeThread {
+	inline function getHandle():NativeThread {
 		return this;
 	}
+
+	function get_events():EventLoop {
+		if(this.events == null)
+			throw new NoEventLoopException();
+		return this.events;
+	}
+
+	@:keep
+	static function initEventLoop() {
+		NativeThread.self().events = new EventLoop();
+	}
+
+
+	@:keep
+	static function processEvents():Void {
+		NativeThread.self().events.loop();
+	}
 }

+ 3 - 0
std/eval/vm/NativeThread.hx

@@ -76,4 +76,7 @@ extern class NativeThread {
 	static function readMessage<T>(block:Bool):T;
 
 	function sendMessage<T>(msg:T):Void;
+
+	@:allow(sys.thread.Thread)
+	private var events(get,set):Null<sys.thread.EventLoop>;
 }

+ 14 - 1
std/haxe/EntryPoint.hx

@@ -1,6 +1,6 @@
 package haxe;
 
-#if target.threaded
+#if (target.threaded && !cppia)
 import sys.thread.Lock;
 import sys.thread.Mutex;
 import sys.thread.Thread;
@@ -130,6 +130,19 @@ class EntryPoint {
 		#end
 		#elseif flash
 		flash.Lib.current.stage.addEventListener(flash.events.Event.ENTER_FRAME, function(_) processEvents());
+		#elseif (target.threaded && !cppia)
+		var mainThread = Thread.current();
+		var handler = null;
+		function progress() {
+			if(handler != null) {
+				mainThread.events.cancel(handler);
+			}
+			var nextTick = processEvents();
+			if (nextTick > 0) {
+				handler = mainThread.events.repeat(progress, Std.int(nextTick * 1000.0));
+			}
+		}
+		progress();
 		#elseif sys
 		while (true) {
 			var nextTick = processEvents();

+ 14 - 29
std/haxe/Timer.hx

@@ -22,6 +22,11 @@
 
 package haxe;
 
+#if (target.threaded && !cppia)
+import sys.thread.Thread;
+import sys.thread.EventLoop;
+#end
+
 /**
 	The `Timer` class allows you to create asynchronous timers on platforms that
 	support events.
@@ -39,9 +44,9 @@ package haxe;
 class Timer {
 	#if (flash || js)
 	private var id:Null<Int>;
-	#elseif (java && !jvm)
-	private var timer:java.util.Timer;
-	private var task:java.util.TimerTask;
+	#elseif (target.threaded && !cppia)
+	var thread:Thread;
+	var eventHandler:EventHandler;
 	#else
 	private var event:MainLoop.MainEvent;
 	#end
@@ -66,9 +71,9 @@ class Timer {
 		#elseif js
 		var me = this;
 		id = untyped setInterval(function() me.run(), time_ms);
-		#elseif (java && !jvm)
-		timer = new java.util.Timer();
-		timer.scheduleAtFixedRate(task = new TimerTask(this), haxe.Int64.ofInt(time_ms), haxe.Int64.ofInt(time_ms));
+		#elseif (target.threaded && !cppia)
+		thread = Thread.current();
+		eventHandler = thread.events.repeat(() -> this.run(), time_ms);
 		#else
 		var dt = time_ms / 1000;
 		event = MainLoop.add(function() {
@@ -97,12 +102,8 @@ class Timer {
 		untyped clearInterval(id);
 		#end
 		id = null;
-		#elseif (java && !jvm)
-		if (timer != null) {
-			timer.cancel();
-			timer = null;
-		}
-		task = null;
+		#elseif (target.threaded && !cppia)
+		thread.events.cancel(eventHandler);
 		#else
 		if (event != null) {
 			event.stop();
@@ -189,20 +190,4 @@ class Timer {
 		return 0;
 		#end
 	}
-}
-
-#if (java && !jvm)
-@:nativeGen
-private class TimerTask extends java.util.TimerTask {
-	var timer:Timer;
-
-	public function new(timer:Timer):Void {
-		super();
-		this.timer = timer;
-	}
-
-	@:overload override public function run():Void {
-		timer.run();
-	}
-}
-#end
+}

+ 122 - 38
std/hl/_std/sys/thread/Thread.hx

@@ -22,70 +22,154 @@
 
 package sys.thread;
 
-private typedef ThreadHandle = hl.Abstract<"hl_thread">;
+abstract Thread(HaxeThread) from HaxeThread to HaxeThread {
+	public var events(get,never):EventLoop;
 
-abstract Thread(ThreadHandle) from ThreadHandle to ThreadHandle {
-	public function sendMessage(msg:Dynamic) {
-		getQueue(this).add(msg);
+	public inline function sendMessage(msg:Dynamic) {
+		this.sendMessage(msg);
+	}
+
+	public static inline function readMessage(block = true):Dynamic {
+		return HaxeThread.current().readMessage(block);
+	}
+
+	public static inline function create(job:()->Void):Thread {
+		return HaxeThread.create(job, false);
 	}
 
-	public static function readMessage(block = true):Dynamic {
-		return getQueue(cast current()).pop(block);
+	public static inline function runWithEventLoop(job:()->Void):Void {
+		HaxeThread.runWithEventLoop(job);
+	}
+
+	public static inline function createWithEventLoop(job:()->Void):Thread {
+		return HaxeThread.create(job, true);
+	}
+
+	public static function current():Thread {
+		return HaxeThread.current();
+	}
+
+	function get_events():EventLoop {
+		if(this.events == null)
+			throw new NoEventLoopException();
+		return this.events;
+	}
+
+	@:keep
+	static function initEventLoop() {
+		@:privateAccess HaxeThread.current().events = new EventLoop();
+	}
+
+	@:keep
+	static public function processEvents() {
+		HaxeThread.current().events.loop();
+	}
+}
+
+private typedef ThreadHandle = hl.Abstract<"hl_thread">;
+
+private class HaxeThread {
+	static final mainThreadHandle:ThreadHandle = currentHandle();
+	static final mainThread:HaxeThread = new HaxeThread();
+	static final threads = new Array<{thread:HaxeThread, handle:ThreadHandle}>();
+	static final threadsMutex = new Mutex();
+
+	public var events(default,null):Null<EventLoop>;
+	final messages = new Deque();
+
+	static var ids = 0;
+	var id = ids++;
+
+	@:hlNative("std", "thread_create")
+	static function createHandle(callb:Void->Void):ThreadHandle {
+		return null;
 	}
 
-	static var queue_mutex:Mutex = null;
-	static var threads_queues:Array<{t:ThreadHandle, q:Deque<Dynamic>}> = null;
+	@:hlNative("std", "thread_current")
+	static function currentHandle():ThreadHandle {
+		return null;
+	}
 
-	static function getQueue(t:ThreadHandle) {
-		if (queue_mutex == null) {
-			queue_mutex = new Mutex();
-			threads_queues = [];
+	static public function current():HaxeThread {
+		var handle = currentHandle();
+		if(handle == mainThreadHandle) {
+			return mainThread;
 		}
-		queue_mutex.acquire();
-		var q = null;
-		for (tq in threads_queues)
-			if (tq.t == t) {
-				q = tq.q;
+		threadsMutex.acquire();
+		var thread = null;
+		for(item in threads) {
+			if(item.handle == handle) {
+				thread = item.thread;
 				break;
 			}
-		if (q == null) {
-			q = new Deque<Dynamic>();
-			threads_queues.push({t: t, q: q});
 		}
-		queue_mutex.release();
-		return q;
+		if(thread == null) {
+			thread = new HaxeThread();
+			threads.push({thread:thread, handle:handle});
+		}
+		threadsMutex.release();
+		return thread;
 	}
 
-	static public function create(callb:()->Void):Thread {
-		return createHandle(() -> {
+	public static function create(callb:()->Void, withEventLoop:Bool):Thread {
+		var item = {handle:null, thread:new HaxeThread()};
+		threadsMutex.acquire();
+		threads.push(item);
+		threadsMutex.release();
+		if(withEventLoop)
+			item.thread.events = new EventLoop();
+		item.handle = createHandle(() -> {
+			if(item.handle == null) {
+				item.handle = currentHandle();
+			}
 			try {
 				callb();
+				if(withEventLoop)
+					item.thread.events.loop();
 			} catch(e) {
-				dropThread(current());
+				dropThread(item);
 				throw e;
 			}
-			dropThread(current());
+			dropThread(item);
 		});
+		return item.thread;
 	}
 
-	static inline function dropThread(handle:ThreadHandle) {
-		queue_mutex.acquire();
-		for (i => tq in threads_queues) {
-			if (tq.t == handle) {
-				threads_queues.splice(i, 1);
+	public static function runWithEventLoop(job:()->Void):Void {
+		var thread = current();
+		if(thread.events == null) {
+			thread.events = new EventLoop();
+			try {
+				job();
+				thread.events.loop();
+				thread.events = null;
+			} catch(e) {
+				thread.events = null;
+				throw e;
+			}
+		} else {
+			job();
+		}
+	}
+
+	static function dropThread(deleteItem) {
+		threadsMutex.acquire();
+		for(i => item in threads) {
+			if(item == deleteItem) {
+				threads.splice(i, 1);
 				break;
 			}
 		}
-		queue_mutex.release();
+		threadsMutex.release();
 	}
 
-	@:hlNative("std", "thread_create")
-	static function createHandle(callb:()->Void):ThreadHandle {
-		return null;
+	public function readMessage(block:Bool):Dynamic {
+		return messages.pop(block);
 	}
 
-	@:hlNative("std", "thread_current")
-	public static function current():Thread {
-		return null;
+	public function new() {}
+
+	public function sendMessage(msg:Dynamic) {
+		messages.add(msg);
 	}
 }

+ 126 - 51
std/java/_std/sys/thread/Thread.hx

@@ -23,89 +23,164 @@
 package sys.thread;
 
 import java.Lib;
-
-abstract Thread(NativeThread) {
-	inline function new(t:NativeThread) {
+import java.lang.Runnable;
+import java.util.WeakHashMap;
+import java.util.Collections;
+import java.lang.Thread as JavaThread;
+import java.lang.System;
+import java.StdTypes.Int64 as Long;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.LinkedBlockingDeque;
+
+abstract Thread(HaxeThread) from HaxeThread {
+	public var events(get,never):EventLoop;
+
+	inline function new(t:HaxeThread) {
 		this = t;
 	}
 
-	public static function create(callb:Void->Void):Thread {
-		var ret = new NativeThread();
-		var t = new HaxeThread(ret, callb);
-		t.start();
-		return new Thread(ret);
+	public static inline function create(job:()->Void):Thread {
+		return HaxeThread.create(job, false);
+	}
+
+	public static inline function current():Thread {
+		return HaxeThread.get(JavaThread.currentThread());
+	}
+
+	public static inline function runWithEventLoop(job:()->Void):Void {
+		HaxeThread.runWithEventLoop(job);
 	}
 
-	public static function current():Thread {
-		return new Thread(NativeThread.getThread(java.lang.Thread.currentThread()));
+	public static inline function createWithEventLoop(job:()->Void):Thread {
+		return HaxeThread.create(job, true);
 	}
 
-	public static function readMessage(block:Bool):Dynamic {
-		return current().getHandle().messages.pop(block);
+	public static inline function readMessage(block:Bool):Dynamic {
+		return current().getHandle().readMessage(block);
 	}
 
 	public inline function sendMessage(msg:Dynamic):Void {
 		this.sendMessage(msg);
 	}
 
-	private inline function getHandle():NativeThread {
+	inline function getHandle():HaxeThread {
 		return this;
 	}
+
+	function get_events():EventLoop {
+		if(this.events == null)
+			throw new NoEventLoopException();
+		return this.events;
+	}
+
+	@:keep
+	static function initEventLoop() {
+		@:privateAccess HaxeThread.get(JavaThread.currentThread()).events = new EventLoop();
+	}
+
+	@:keep //TODO: keep only if events are actually used
+	static function processEvents():Void {
+		current().getHandle().events.loop();
+	}
 }
 
-@:native('haxe.java.vm.Thread') private class NativeThread {
-	@:private static var javaThreadToHaxe = new haxe.ds.WeakMap<java.lang.Thread, NativeThread>();
-	@:private static var mainJavaThread = java.lang.Thread.currentThread();
-	@:private static var mainHaxeThread = {
-		var ret = new NativeThread();
-		javaThreadToHaxe.set(mainJavaThread, ret);
-		ret;
-	};
-
-	public static function getThread(jt:java.lang.Thread):NativeThread {
-		if (Std.isOfType(jt, HaxeThread)) {
-			var t:HaxeThread = cast jt;
-			return t.threadObject;
-		} else if (jt == mainJavaThread) {
+private class HaxeThread {
+	static final nativeThreads = Collections.synchronizedMap(new WeakHashMap<JavaThread,HaxeThread>());
+	static final mainJavaThread = JavaThread.currentThread();
+	static final mainHaxeThread = new HaxeThread();
+
+	public final messages = new LinkedBlockingDeque<Dynamic>();
+
+	public var events(default,null):Null<EventLoop>;
+
+	public static function create(job:()->Void, withEventLoop:Bool):HaxeThread {
+		var hx = new HaxeThread();
+		if(withEventLoop)
+			hx.events = new EventLoop();
+		var thread = new NativeHaxeThread(hx, job, withEventLoop);
+		thread.setDaemon(true);
+		thread.start();
+		return hx;
+	}
+
+	public static function get(javaThread:JavaThread):HaxeThread {
+		if(javaThread == mainJavaThread) {
 			return mainHaxeThread;
+		} else if(javaThread is NativeHaxeThread) {
+			return (cast javaThread:NativeHaxeThread).haxeThread;
 		} else {
-			var ret = null;
-			untyped __lock__(javaThreadToHaxe, {
-				ret = javaThreadToHaxe.get(jt);
-				if (ret == null) {
-					ret = new NativeThread();
-					javaThreadToHaxe.set(jt, ret);
-				}
-			});
-			return ret;
+			switch nativeThreads.get(javaThread) {
+				case null:
+					var hx = new HaxeThread();
+					nativeThreads.put(javaThread, hx);
+					return hx;
+				case hx:
+					return hx;
+			}
 		}
 	}
 
-	public var messages:Deque<Dynamic>;
-
-	public function new() {
-		this.messages = new Deque();
+	public static function runWithEventLoop(job:()->Void):Void {
+		var thread = get(JavaThread.currentThread());
+		if(thread.events == null) {
+			thread.events = new EventLoop();
+			try {
+				job();
+				thread.events.loop();
+				thread.events = null;
+			} catch(e) {
+				thread.events = null;
+				throw e;
+			}
+		} else {
+			job();
+		}
 	}
 
+	function new() {}
+
 	public function sendMessage(msg:Dynamic):Void {
 		messages.add(msg);
 	}
+
+	public function readMessage(block:Bool):Dynamic {
+		return block ? messages.take() : messages.poll();
+	}
 }
 
-@:native('haxe.java.vm.HaxeThread')
-private class HaxeThread extends java.lang.Thread {
-	public var threadObject(default, null):NativeThread;
+private class NativeHaxeThread extends java.lang.Thread {
+	public final haxeThread:HaxeThread;
+	final withEventLoop:Bool;
 
-	private var runFunction:Void->Void;
+	public function new(haxeThread:HaxeThread, job:()->Void, withEventLoop:Bool) {
+		super(new Job(job));
+		this.haxeThread = haxeThread;
+		this.withEventLoop = withEventLoop;
+	}
+
+	override overload public function run() {
+		super.run();
+		if(withEventLoop)
+			haxeThread.events.loop();
+	}
+}
+
+#if jvm
+private abstract Job(Runnable) from Runnable to Runnable {
+	public inline function new(job:()->Void) {
+		this = cast job;
+	}
+}
+#else
+private class Job implements Runnable {
+	final job:()->Void;
 
-	@:overload override public function run():Void {
-		runFunction();
+	public function new(job:()->Void) {
+		this.job = job;
 	}
 
-	public function new(hxThread:NativeThread, run:Void->Void) {
-		super();
-		threadObject = hxThread;
-		runFunction = run;
-		setDaemon(true);
+	public function run() {
+		job();
 	}
 }
+#end

+ 0 - 107
std/jvm/_std/sys/thread/Thread.hx

@@ -1,107 +0,0 @@
-/*
- * Copyright (C)2005-2019 Haxe Foundation
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- */
-
-package sys.thread;
-
-import java.Lib;
-import java.lang.Runnable;
-import java.util.WeakHashMap;
-import java.util.Collections;
-import java.lang.Thread as JavaThread;
-
-abstract Thread(HaxeThread) from HaxeThread {
-	inline function new(t:HaxeThread) {
-		this = t;
-	}
-
-	public static inline function create(callb:()->Void):Thread {
-		return HaxeThread.create(callb);
-	}
-
-	public static inline function current():Thread {
-		return HaxeThread.get(JavaThread.currentThread());
-	}
-
-	public static inline function readMessage(block:Bool):Dynamic {
-		return current().getHandle().readMessage(block);
-	}
-
-	public inline function sendMessage(msg:Dynamic):Void {
-		this.sendMessage(msg);
-	}
-
-	inline function getHandle():HaxeThread {
-		return this;
-	}
-}
-
-private class HaxeThread {
-	static final nativeThreads = Collections.synchronizedMap(new WeakHashMap<JavaThread,HaxeThread>());
-	static final mainJavaThread = JavaThread.currentThread();
-	static final mainHaxeThread = new HaxeThread();
-
-	public final messages = new Deque<Dynamic>();
-
-	public static function create(callb:()->Void):HaxeThread {
-		var hx = new HaxeThread();
-		var thread = new NativeHaxeThread(hx, callb);
-		thread.setDaemon(true);
-		thread.start();
-		return hx;
-	}
-
-	public static function get(javaThread:JavaThread):HaxeThread {
-		if(javaThread == mainJavaThread) {
-			return mainHaxeThread;
-		} else if(javaThread is NativeHaxeThread) {
-			return (cast javaThread:NativeHaxeThread).haxeThread;
-		} else {
-			switch nativeThreads.get(javaThread) {
-				case null:
-					var hx = new HaxeThread();
-					nativeThreads.put(javaThread, hx);
-					return hx;
-				case hx:
-					return hx;
-			}
-		}
-	}
-
-	function new() {}
-
-	public inline function sendMessage(msg:Dynamic):Void {
-		messages.add(msg);
-	}
-
-	public inline function readMessage(block:Bool):Dynamic {
-		return messages.pop(block);
-	}
-}
-
-private class NativeHaxeThread extends java.lang.Thread {
-	public final haxeThread:HaxeThread;
-
-	public function new(haxeThread:HaxeThread, callb:()->Void) {
-		super((cast callb:Runnable));
-		this.haxeThread = haxeThread;
-	}
-}

+ 125 - 74
std/neko/_std/sys/thread/Thread.hx

@@ -22,103 +22,154 @@
 
 package sys.thread;
 
-@:callable
-@:coreType
-private abstract ThreadHandle {}
+abstract Thread(HaxeThread) from HaxeThread to HaxeThread {
 
-abstract Thread(ThreadHandle) {
-	inline function new(h:ThreadHandle):Void {
-		this = h;
-	}
+	public var events(get,never):EventLoop;
 
 	public inline function sendMessage(msg:Dynamic):Void {
-		thread_send(this, msg);
+		this.sendMessage(msg);
 	}
 
 	public static inline function current():Thread {
-		return new Thread(thread_current());
+		return HaxeThread.current();
 	}
 
-	public static inline function create(callb:Void->Void):Thread {
-		return new Thread(thread_create(function(_) {
-			return callb();
-		}, null));
+	public static inline function create(job:()->Void):Thread {
+		return HaxeThread.create(job, false);
+	}
+
+	public static inline function runWithEventLoop(job:()->Void):Void {
+		HaxeThread.runWithEventLoop(job);
+	}
+
+	public static inline function createWithEventLoop(job:()->Void):Thread {
+		return HaxeThread.create(job, true);
 	}
 
 	public static inline function readMessage(block:Bool):Dynamic {
-		return thread_read_message(block);
+		return HaxeThread.readMessage(block);
 	}
 
-	@:op(A == B)
-	inline function equals(other:Thread):Bool {
-		return getHandle() == other.getHandle();
+	function get_events():EventLoop {
+		if(this.events == null)
+			throw new NoEventLoopException();
+		return this.events;
 	}
 
-	private inline function getHandle():ThreadHandle {
-		return this;
+	@:keep
+	static function initEventLoop() {
+		@:privateAccess HaxeThread.current().events = new EventLoop();
 	}
 
-	/**
-			Starts an OS message loop after [osInitialize] has been done.
-			In that state, the UI handled by this thread will be updated and
-			[sync] calls can be performed. The loop returns when [exitLoop] is
-			called for this thread.
-		**
-		public static function osLoop() {
-			if( os_loop == null ) throw "Please call osInitialize() first";
-			os_loop();
-		}
+	@:keep
+	static function processEvents() {
+		HaxeThread.current().events.loop();
+	}
+}
 
-		/**
-			The function [f] will be called by this thread if it's in [osLoop].
-			[sync] returns immediately. See [osInitialize] remarks.
-		**
-		public function sync( f : Void -> Void ) {
-			os_sync(handle,f);
-		}
+@:callable
+@:coreType
+private abstract ThreadHandle {}
+
+private class HaxeThread {
+	static var thread_create:(callb:(_:Dynamic)->Void, _:Dynamic)->ThreadHandle = neko.Lib.load("std", "thread_create", 2);
+	static var thread_current:()->ThreadHandle = neko.Lib.load("std", "thread_current", 0);
+	static var thread_send:(handle:ThreadHandle, msg:Dynamic)->Void = neko.Lib.load("std", "thread_send", 2);
+	static var thread_read_message:(block:Bool)->Dynamic = neko.Lib.load("std", "thread_read_message", 1);
 
-		/**
-			The function [f] will be called by this thread and the calling thread
-			will wait until the result is available then return its value.
-		**
-		public function syncResult<T>( f : Void -> T ) : T {
-			if( this == current() )
-				return f();
-			var v = new neko.vm.Lock();
-			var r = null;
-			sync(function() {
-				r = f();
-				v.release();
-			});
-			v.wait();
-			return r;
+	static var mainThreadHandle = thread_current();
+	static var mainThread = new HaxeThread(mainThreadHandle);
+
+	static final threads = new Array<{thread:HaxeThread, handle:ThreadHandle}>();
+	static final threadsMutex = new Mutex();
+
+	public var events(default,null):Null<EventLoop>;
+	public var handle:ThreadHandle;
+
+	static public function current():HaxeThread {
+		var handle = thread_current();
+		if(handle == mainThreadHandle) {
+			return mainThread;
+		}
+		threadsMutex.acquire();
+		var thread = null;
+		for(item in threads) {
+			if(item.handle == handle) {
+				thread = item.thread;
+				break;
+			}
+		}
+		if(thread == null) {
+			thread = new HaxeThread(handle);
+			threads.push({thread:thread, handle:handle});
 		}
+		threadsMutex.release();
+		return thread;
+	}
+
+	public static function create(callb:()->Void, withEventLoop:Bool):Thread {
+		var item = {handle:null, thread:new HaxeThread(null)};
+		threadsMutex.acquire();
+		threads.push(item);
+		threadsMutex.release();
+		if(withEventLoop)
+			item.thread.events = new EventLoop();
+		item.handle = thread_create(_ -> {
+			if(item.thread.handle == null) {
+				item.handle = thread_current();
+				item.thread.handle = item.handle;
+			}
+			try {
+				callb();
+				if(withEventLoop)
+					item.thread.events.loop();
+			} catch(e) {
+				dropThread(item);
+				throw e;
+			}
+			dropThread(item);
+		}, null);
+		item.thread.handle = item.handle;
+		return item.thread;
+	}
 
-		/**
-			Exit from [osLoop].
-		**
-		public function exitLoop() {
-			os_loop_stop(handle);
+	public static function runWithEventLoop(job:()->Void):Void {
+		var thread = current();
+		if(thread.events == null) {
+			thread.events = new EventLoop();
+			try {
+				job();
+				thread.events.loop();
+				thread.events = null;
+			} catch(e) {
+				thread.events = null;
+				throw e;
+			}
+		} else {
+			job();
 		}
+	}
 
-		/**
-			If you want to use the [osLoop], [sync] and [syncResult] methods, you
-			need to call [osInitialize] before creating any thread or calling [current].
-			This will load [os.ndll] library and initialize UI methods for each thread.
-		**
-		public static function osInitialize() {
-			os_loop = neko.Lib.load("os","os_loop",0);
-			os_loop_stop = neko.Lib.load("os","os_loop_stop",1);
-			os_sync = neko.Lib.load("os","os_sync",2);
+	static function dropThread(deleteItem) {
+		threadsMutex.acquire();
+		for(i => item in threads) {
+			if(item == deleteItem) {
+				threads.splice(i, 1);
+				break;
+			}
 		}
+		threadsMutex.release();
+	}
+
+	public static inline function readMessage(block:Bool):Dynamic {
+		return thread_read_message(block);
+	}
 
-		static var os_loop = null;
-		static var os_loop_stop = null;
-		static var os_sync = null;
-	 */
-	static var thread_create = neko.Lib.load("std", "thread_create", 2);
+	public function new(handle:ThreadHandle) {
+		this.handle = handle;
+	}
 
-	static var thread_current = neko.Lib.load("std", "thread_current", 0);
-	static var thread_send = neko.Lib.load("std", "thread_send", 2);
-	static var thread_read_message = neko.Lib.load("std", "thread_read_message", 1);
-}
+	public function sendMessage(msg:Dynamic) {
+		thread_send(handle, msg);
+	}
+}

+ 92 - 16
std/python/_std/sys/thread/Thread.hx

@@ -22,51 +22,104 @@
 
 package sys.thread;
 
-class Thread {
-	var nativeThread: NativeThread;
-	var messages: Deque<Dynamic>;
+abstract Thread(HxThread) from HxThread {
+	public var events(get,never):EventLoop;
 
-	static var threads = new haxe.ds.ObjectMap<NativeThread, Thread>();
+	public static inline function current():Thread {
+		return HxThread.current();
+	}
+
+	public static inline function create(callb:Void->Void):Thread {
+		return HxThread.create(callb, false);
+	}
+
+	public static inline function runWithEventLoop(job:()->Void):Void {
+		HxThread.runWithEventLoop(job);
+	}
+
+	public static inline function createWithEventLoop(job:()->Void):Thread {
+		return HxThread.create(job, true);
+	}
+
+	public static inline function readMessage(block:Bool):Dynamic {
+		return HxThread.readMessage(block);
+	}
+
+	public inline function sendMessage(msg:Dynamic):Void {
+		this.sendMessage(msg);
+	}
+
+	function get_events():EventLoop {
+		if(this.events == null)
+			throw new NoEventLoopException();
+		return this.events;
+	}
+
+	@:keep
+	static function initEventLoop() {
+		@:privateAccess HxThread.current().events = new EventLoop();
+	}
+
+	@:keep
+	static public function processEvents() {
+		HxThread.current().events.loop();
+	}
+}
+
+private class HxThread {
+	public var events(default,null):Null<EventLoop>;
+
+	final nativeThread:NativeThread;
+	final messages = new Deque<Dynamic>();
+
+	static var threads = new haxe.ds.ObjectMap<NativeThread, HxThread>();
 	static var threadsMutex: Mutex = new Mutex();
-	static var mainThread: Thread;
+	static var mainThread: HxThread;
 
 	private function new(t:NativeThread) {
 		nativeThread = t;
-		messages = new Deque<Dynamic>();
 	}
 
 	public function sendMessage(msg:Dynamic):Void {
 		messages.add(msg);
 	}
 
-	public static function current():Thread {
+	public static function current():HxThread {
 		threadsMutex.acquire();
 		var ct = PyThreadingAPI.current_thread();
 		if (ct == PyThreadingAPI.main_thread()) {
-			if (mainThread == null) mainThread = new Thread(ct);
+			if (mainThread == null) mainThread = new HxThread(ct);
 			threadsMutex.release();
 			return mainThread;
 		}
 		// If the current thread was not created via the haxe API, it can still be wrapped
 		if (!threads.exists(ct)) {
-			threads.set(ct, new Thread(ct));
+			threads.set(ct, new HxThread(ct));
 		}
 		var t = threads.get(ct);
 		threadsMutex.release();
 		return t;
 	}
 
-	public static function create(callb:Void->Void):Thread {
+	public static function create(callb:Void->Void, withEventLoop:Bool):HxThread {
 		var nt:NativeThread = null;
+		var t:HxThread = null;
 		// Wrap the callback so it will clear the thread reference once the thread is finished
-		var wrappedCallB = () -> { 
-			callb();
-			threadsMutex.acquire();
-			threads.remove(nt);
-			threadsMutex.release();
+		var wrappedCallB = () -> {
+			try {
+				callb();
+				if(withEventLoop)
+					t.events.loop();
+			} catch(e) {
+				dropThread(nt);
+				throw e;
+			}
+			dropThread(nt);
 		}
 		nt = new NativeThread(null, wrappedCallB);
-		var t = new Thread(nt);
+		t = new HxThread(nt);
+		if(withEventLoop)
+			t.events = new EventLoop();
 		threadsMutex.acquire();
 		threads.set(nt, t);
 		threadsMutex.release();
@@ -74,6 +127,29 @@ class Thread {
 		return t;
 	}
 
+	public static function runWithEventLoop(job:()->Void):Void {
+		var thread = current();
+		if(thread.events == null) {
+			thread.events = new EventLoop();
+			try {
+				job();
+				thread.events.loop();
+				thread.events = null;
+			} catch(e) {
+				thread.events = null;
+				throw e;
+			}
+		} else {
+			job();
+		}
+	}
+
+	static inline function dropThread(nt:NativeThread) {
+		threadsMutex.acquire();
+		threads.remove(nt);
+		threadsMutex.release();
+	}
+
 	public static function readMessage(block:Bool):Dynamic {
 		return current().messages.pop(block);
 	}

+ 230 - 0
std/sys/thread/EventLoop.hx

@@ -0,0 +1,230 @@
+package sys.thread;
+
+/**
+	When an event loop has an available event to execute.
+**/
+enum NextEventTime {
+	/** There's already an event waiting to be executed */
+	Now;
+	/** No new events are expected. */
+	Never;
+	/**
+		An event is expected to arrive at any time.
+		If `time` is specified, then the event will be ready at that time for sure.
+	*/
+	AnyTime(time:Null<Float>);
+	/** An event is expected to be ready for execution at `time`. */
+	At(time:Float);
+}
+
+/**
+	An event loop implementation used for `sys.thread.Thread`
+**/
+class EventLoop {
+	final mutex = new Mutex();
+	final oneTimeEvents = new Array<Null<()->Void>>();
+	var oneTimeEventsIdx = 0;
+	final waitLock = new Lock();
+	var promisedEventsCount = 0;
+	var regularEvents:Null<RegularEvent>;
+
+	public function new():Void {}
+
+	/**
+		Schedule event for execution every `intervalMs` milliseconds in current loop.
+	**/
+	public function repeat(event:()->Void, intervalMs:Int):EventHandler {
+		mutex.acquire();
+		var interval = 0.001 * intervalMs;
+		var event = new RegularEvent(event, Sys.time() + interval, interval);
+		switch regularEvents {
+			case null:
+			case current:
+				event.next = current;
+				current.previous = event;
+		}
+		regularEvents = event;
+		waitLock.release();
+		mutex.release();
+		return event;
+	}
+
+	/**
+		Prevent execution of a previousely scheduled event in current loop.
+	**/
+	public function cancel(eventHandler:EventHandler):Void {
+		mutex.acquire();
+		var event:RegularEvent = eventHandler;
+		if(regularEvents == event) {
+			regularEvents = event.next;
+		}
+		switch event.next {
+			case null:
+			case e: e.previous = event.previous;
+		}
+		switch event.previous {
+			case null:
+			case e: e.next = event.next;
+		}
+		mutex.release();
+	}
+
+	/**
+		Notify this loop about an upcoming event.
+		This makes the thread to stay alive and wait for as many events as many times
+		`.promise()` was called. These events should be added via `.runPromised()`
+	**/
+	public function promise():Void {
+		mutex.acquire();
+		++promisedEventsCount;
+		mutex.release();
+	}
+
+	/**
+		Execute `event` as soon as possible.
+	**/
+	public function run(event:()->Void):Void {
+		mutex.acquire();
+		oneTimeEvents[oneTimeEventsIdx++] = event;
+		waitLock.release();
+		mutex.release();
+	}
+
+	/**
+		Add previously promised `event` for execution.
+	**/
+	public function runPromised(event:()->Void):Void {
+		mutex.acquire();
+		oneTimeEvents[oneTimeEventsIdx++] = event;
+		--promisedEventsCount;
+		waitLock.release();
+		mutex.release();
+	}
+
+	/**
+		Executes all pending events.
+
+		The returned time stamps can be used with `Sys.time()` for calculations.
+	**/
+	public function progress():NextEventTime {
+		return switch __progress(Sys.time(), []) {
+			case {nextEventAt:-2}: Now;
+			case {nextEventAt:-1, anyTime:false}: Never;
+			case {nextEventAt:-1, anyTime:true}: AnyTime(null);
+			case {nextEventAt:time, anyTime:true}: AnyTime(time);
+			case {nextEventAt:time, anyTime:false}: At(time);
+		}
+	}
+
+	/**
+		Waits for a new event to be added, or `timeout` (in seconds) to expire.
+		Returns `true` if an event was added and `false` if a timeout occurs.
+	**/
+	public function wait(?timeout:Float) {
+		return waitLock.wait(timeout);
+	}
+
+	/**
+		Execute all pending events.
+		Wait and execute as many events as many times `promiseEvent()` was called.
+		Runs until all repeating events are cancelled and no more events is expected.
+	**/
+	public function loop():Void {
+		var events = [];
+		while(true) {
+			var r = __progress(Sys.time(), events);
+			switch r {
+				case {nextEventAt:-2}:
+				case {nextEventAt:-1, anyTime:false}:
+					break;
+				case {nextEventAt:-1, anyTime:true}:
+					waitLock.wait();
+				case {nextEventAt:time}:
+					var timeout = time - Sys.time();
+					waitLock.wait(Math.max(0, timeout));
+			}
+		}
+	}
+
+	/**
+		`.pogress` implementation with a resuable array for internal usage.
+		The `nextEventAt` field of the return value denotes when the next event
+		is expected to run:
+		* -1 - never
+		* -2 - now
+		* other values - at specified time
+	**/
+	inline function __progress(now:Float, recycle:Array<()->Void>):{nextEventAt:Float, anyTime:Bool} {
+		var eventsToRun = recycle;
+		var eventsToRunIdx = 0;
+		// When the next event is expected to run
+		var nextEventAt:Float = -1;
+
+		mutex.acquire();
+		//reset waitLock
+		while(waitLock.wait(0.0)) {}
+		// Collect regular events to run
+		var current = regularEvents;
+		while(current != null) {
+			if(current.nextRunTime <= now) {
+				eventsToRun[eventsToRunIdx++] = current.run;
+				current.nextRunTime += current.interval;
+				nextEventAt = -2;
+			} else if(nextEventAt == -1 || current.nextRunTime < nextEventAt) {
+				nextEventAt = current.nextRunTime;
+			}
+			current = current.next;
+		}
+		mutex.release();
+
+		// Run regular events
+		for(i in 0...eventsToRunIdx) {
+			eventsToRun[i]();
+			eventsToRun[i] = null;
+		}
+		eventsToRunIdx = 0;
+
+		// Collect pending one-time events
+		mutex.acquire();
+		for(i => event in oneTimeEvents) {
+			switch event {
+				case null:
+					break;
+				case _:
+					eventsToRun[eventsToRunIdx++] = event;
+					oneTimeEvents[i] = null;
+			}
+		}
+		oneTimeEventsIdx = 0;
+		var hasPromisedEvents = promisedEventsCount > 0;
+		mutex.release();
+
+		//run events
+		for(i in 0...eventsToRunIdx) {
+			eventsToRun[i]();
+			eventsToRun[i] = null;
+		}
+
+		// Some events were executed. They could add new events to run.
+		if(eventsToRunIdx > 0) {
+			nextEventAt = -2;
+		}
+		return {nextEventAt:nextEventAt, anyTime:hasPromisedEvents}
+	}
+}
+
+abstract EventHandler(RegularEvent) from RegularEvent to RegularEvent {}
+
+private class RegularEvent {
+	public var nextRunTime:Float;
+	public final interval:Float;
+	public final run:()->Void;
+	public var next:Null<RegularEvent>;
+	public var previous:Null<RegularEvent>;
+
+	public function new(run:()->Void, nextRunTime:Float, interval:Float) {
+		this.run = run;
+		this.nextRunTime = nextRunTime;
+		this.interval = interval;
+	}
+}

+ 9 - 0
std/sys/thread/NoEventLoopException.hx

@@ -0,0 +1,9 @@
+package sys.thread;
+
+import haxe.Exception;
+
+class NoEventLoopException extends Exception {
+	public function new(msg:String = 'Event loop is not available. Refer to sys.thread.Thread.runWithEventLoop.', ?previous:Exception) {
+		super(msg, previous);
+	}
+}

+ 39 - 3
std/sys/thread/Thread.hx

@@ -25,7 +25,17 @@ package sys.thread;
 #if (!target.threaded)
 #error "This class is not available on this target"
 #end
+
 extern abstract Thread({}) {
+	/**
+		Event loop of this thread (if available).
+
+		Note that by default event loop is only available in the main thread.
+		To setup an event loop in other threads use `sys.thread.Thread.runWithEventLoop`
+		or create new threads with built-in event loops using `sys.thread.Thread.createWithEventLoop`
+	**/
+	public var events(get,never):EventLoop;
+
 	/**
 		Send a message to the thread queue. This message can be read by using `readMessage`.
 	**/
@@ -37,9 +47,25 @@ extern abstract Thread({}) {
 	public static function current():Thread;
 
 	/**
-		Creates a new thread that will execute the `f` function, then exit.
+		Creates a new thread that will execute the `job` function, then exit.
+
+		This function does not setup an event loop for a new thread.
+	**/
+	public static function create(job:()->Void):Thread;
+
+	/**
+		Simply execute `job` if current thread already has an event loop.
+
+		But if current thread does not have an event loop: setup event loop,
+		run `job` and then destroy event loop. And in this case this function
+		does not return until no more events left to run.
 	**/
-	public static function create(callb:Void->Void):Thread;
+	public static function runWithEventLoop(job:()->Void):Void;
+
+	/**
+		This is logically equal to `Thread.create(() -> Thread.runWithEventLoop(job));`
+	**/
+	public static function createWithEventLoop(job:()->Void):Thread;
 
 	/**
 		Reads a message from the thread queue. If `block` is true, the function
@@ -47,4 +73,14 @@ extern abstract Thread({}) {
 		returns `null` if no message is available.
 	**/
 	public static function readMessage(block:Bool):Dynamic;
-}
+
+	/**
+		Initialize event loop in this thread
+	**/
+	private static function initEventLoop():Void;
+
+	/**
+		Run event loop of the current thread
+	**/
+	private static function processEvents():Void;
+}

+ 2 - 0
tests/eventLoop/.gitignore

@@ -0,0 +1,2 @@
+bin/*
+dump/*

+ 5 - 0
tests/eventLoop/build.hxml

@@ -0,0 +1,5 @@
+--class-path src
+--main Main
+--library utest
+--dce full
+-D analyzer-optimize

+ 11 - 0
tests/eventLoop/src/Main.hx

@@ -0,0 +1,11 @@
+import utest.ui.Report;
+import utest.Runner;
+
+function main() {
+	var runner = new Runner();
+	var report = Report.create(runner);
+	report.displayHeader = AlwaysShowHeader;
+	report.displaySuccessResults = NeverShowSuccessResults;
+	runner.addCases('cases');
+	runner.run();
+}

+ 103 - 0
tests/eventLoop/src/cases/TestEvents.hx

@@ -0,0 +1,103 @@
+package cases;
+
+@:timeout(1000)
+class TestEvents extends utest.Test {
+
+	function testThreadRunWithEventLoop() {
+		var eventExecuted = false;
+		var lock = new sys.thread.Lock();
+		Thread.create(() -> {
+			var thread = Thread.current();
+			raises(
+				() -> thread.events.run(() -> {}),
+				sys.thread.NoEventLoopException
+			);
+			Thread.runWithEventLoop(() -> {
+				thread.events.run(lock.release);
+			});
+		});
+		isTrue(lock.wait(1.0));
+	}
+
+	function testRun(async:Async) {
+		var mainThread = Thread.current();
+		Thread.createWithEventLoop(() -> {
+			var childThread = Thread.current();
+			isTrue(mainThread != childThread);
+			mainThread.events.run(() -> {
+				isTrue(mainThread == Thread.current());
+				childThread.events.run(() -> {
+					isTrue(childThread == Thread.current());
+					mainThread.events.run(() -> {
+						isTrue(mainThread == Thread.current());
+						async.done();
+					});
+				});
+			});
+			//keep child thread alive while main thread is adding an event to run in it
+			Sys.sleep(0.5);
+		});
+	}
+
+	@:depends(testRun)
+	function testRepeat(async:Async) {
+		function test(thread:Thread, done:()->Void) {
+			var timesExecuted = 0;
+			var eventHandler = null;
+			eventHandler = thread.events.repeat(() -> {
+				++timesExecuted;
+				isTrue(thread == Thread.current());
+				if(timesExecuted >= 3) {
+					thread.events.cancel(eventHandler);
+					done();
+				}
+			}, 50);
+		}
+
+		var mainThread = Thread.current();
+		//test in main thread
+		test(mainThread, () -> {
+			//now test in a child thread
+			Thread.createWithEventLoop(() -> {
+				var childThread = Thread.current();
+				isTrue(childThread != mainThread);
+				test(childThread, mainThread.events.run.bind(() -> async.done()));
+			});
+		});
+	}
+
+	@:depends(testRun)
+	function testPromisedEvents(async:Async) {
+		var mainThread = Thread.current();
+		// this thread is expected to wait for promised events
+		Thread.createWithEventLoop(() -> {
+			var eventsExecuted = 0;
+			var testThread = Thread.current();
+			testThread.events.promise(); // 1 promised event
+			// this thread will deliver promised events to the testThread
+			Thread.createWithEventLoop(() -> {
+				Sys.sleep(0.2);
+				testThread.events.promise(); // 2 promised events
+				testThread.events.runPromised(() -> {
+					++eventsExecuted;
+					isTrue(testThread == Thread.current());
+				});
+				testThread.events.promise(); // 3 promised events
+				Sys.sleep(0.2);
+				testThread.events.runPromised(() -> {
+					++eventsExecuted;
+					isTrue(testThread == Thread.current());
+				});
+				Sys.sleep(0.2);
+				testThread.events.runPromised(() -> {
+					++eventsExecuted;
+					isTrue(testThread == Thread.current());
+					mainThread.events.run(() -> {
+						equals(3, eventsExecuted);
+						async.done();
+					});
+				});
+			});
+		});
+	}
+}

+ 92 - 0
tests/eventLoop/src/cases/TestTimer.hx

@@ -0,0 +1,92 @@
+package cases;
+
+import haxe.Timer;
+
+@:timeout(10000)
+@:depends(cases.TestEvents)
+class TestTimer extends utest.Test {
+	function testCorrectInterval(async:Async) {
+		async.branch(async -> {
+			var i = 0;
+			var interval = 0.1;
+			var t = new Timer(Std.int(interval * 1000));
+			var start = Timer.stamp();
+			t.run = () -> {
+				var dt = Timer.stamp() - start;
+				//check the interval is ~100ms
+				isTrue(dt >= interval * 0.98, 'Passed time ($dt seconds) is too small. At least $interval seconds expected.');
+				if(i++ > 5) {
+					t.stop();
+					async.done();
+					return;
+				}
+				start += interval;
+			}
+		});
+		async.branch(async -> {
+			var delay = 0.05;
+			var start = Timer.stamp();
+			Timer.delay(() -> {
+				var dt = Timer.stamp() - start;
+				//check the interval is ~50ms
+				isTrue(dt >= delay * 0.98, 'Passed time ($dt seconds) is too small. At least $delay seconds expected.');
+				async.done();
+			}, Std.int(delay * 1000));
+		});
+	}
+
+	function testCallbackInSameThread(async:Async) {
+		var mainThread = Thread.current();
+
+		function work(n:Int) {
+			var thread = Thread.current();
+			var t = new Timer(100);
+			var i = 0;
+			var sameThread = true;
+			t.run = () -> {
+				sameThread = sameThread && thread == Thread.current();
+				if(i++ > 5) {
+					t.stop();
+					mainThread.sendMessage({n:n, type:'interval', sameThread:sameThread});
+				}
+			}
+			Timer.delay(() -> {
+				mainThread.sendMessage({n:n, type:'delay', sameThread:thread == Thread.current()});
+			}, 50);
+		}
+
+		for(n in 0...10) {
+			Thread.createWithEventLoop(work.bind(n));
+		}
+
+		//expect two messages with different types per thread
+		var counters = [for(i in 0...10) ['delay', 'interval']];
+		for(i in 0...20) {
+			var msg:{n:Int, type:String, sameThread:Bool} = Thread.readMessage(true);
+			isTrue(msg.sameThread);
+			isTrue(counters[msg.n].remove(msg.type));
+		}
+		for(types in counters) {
+			equals(0, types.length);
+		}
+
+		//test in main thread
+		async.branch(async -> {
+			var t = new Timer(100);
+			var i = 0;
+			t.run = () -> {
+				isTrue(mainThread == Thread.current());
+				if(i++ > 5) {
+					t.stop();
+					async.done();
+				}
+			}
+		});
+		async.branch(async -> {
+			Timer.delay(() -> {
+				isTrue(mainThread == Thread.current());
+				async.done();
+			}, 50);
+		});
+	}
+}

+ 3 - 0
tests/eventLoop/src/import.hx

@@ -0,0 +1,3 @@
+import utest.Assert.*;
+import utest.Async;
+import sys.thread.Thread;

+ 1 - 0
tests/runci/Config.hx

@@ -20,6 +20,7 @@ class Config {
 	static public final sourcemapsDir = cwd + "sourcemaps/";
 	static public final nullSafetyDir = cwd + "nullsafety/";
 	static public final threadsDir = cwd + "threads/";
+	static public final eventLoopDir = cwd + "eventLoop/";
 
 	static public final ci:Null<Ci> =
 		if (Sys.getEnv("TF_BUILD") == "True")

+ 6 - 0
tests/runci/targets/Cpp.hx

@@ -68,6 +68,12 @@ class Cpp {
 		runCommand("haxe", ["compile-cpp.hxml"].concat(args));
 		runCpp("bin/cpp/Main-debug", []);
 
+		if(testCompiled) {
+			changeDirectory(eventLoopDir);
+			runCommand("haxe", ["build.hxml", "--cpp", "bin/cpp", "--debug"].concat(args));
+			runCpp("bin/cpp/Main-debug", []);
+		}
+
 		if (systemName != "Windows") { // TODO: find out why we keep getting "missed async calls" error
 			changeDirectory(threadsDir);
 			runCommand("haxe", ["build.hxml", "-cpp", "export/cpp"]);

+ 4 - 0
tests/runci/targets/Cs.hx

@@ -66,6 +66,10 @@ class Cs {
 		// runCommand("haxe", ["build.hxml", "-cs", "export/cs"]);
 		// runCs("export/cs/bin/Main.exe");
 
+		changeDirectory(eventLoopDir);
+		runCommand("haxe", ["build.hxml", "--cs", "bin/cs"]);
+		runCs("bin/cs/bin/Main.exe");
+
 		changeDirectory(miscCsDir);
 		runCommand("haxe", ["run.hxml"]);
 

+ 4 - 0
tests/runci/targets/Java.hx

@@ -31,6 +31,10 @@ class Java {
 		runCommand("haxe", ["compile-java.hxml"].concat(args));
 		runCommand("java", ["-jar", "bin/java/Main-Debug.jar"]);
 
+		changeDirectory(eventLoopDir);
+		runCommand("haxe", ["build.hxml", "-java", "bin/java"].concat(args));
+		runCommand("java", ["-jar", "bin/java/Main.jar"]);
+
 		changeDirectory(threadsDir);
 		runCommand("haxe", ["build.hxml", "-java", "export/java"].concat(args));
 		if (systemName != "Windows") { // #8154

+ 4 - 0
tests/runci/targets/Jvm.hx

@@ -18,6 +18,10 @@ class Jvm {
 		runCommand("haxe", ["compile-jvm.hxml"].concat(args));
 		runCommand("java", ["-jar", "bin/jvm/sys.jar"]);
 
+		changeDirectory(eventLoopDir);
+		runCommand("haxe", ["build.hxml", "--jvm", "bin/test.jar"].concat(args));
+		runCommand("java", ["-jar", "bin/test.jar"]);
+
 		changeDirectory(threadsDir);
 		runCommand("haxe", ["build.hxml", "--jvm", "export/threads.jar"].concat(args));
 		if (systemName != "Windows") { // #8154

+ 3 - 0
tests/runci/targets/Macro.hx

@@ -37,6 +37,9 @@ class Macro {
 			case _: // TODO
 		}
 
+		changeDirectory(eventLoopDir);
+		runCommand("haxe", ["build.hxml"].concat(args).concat(["--interp"]));
+
 		// changeDirectory(threadsDir);
 		// runCommand("haxe", ["build.hxml", "--interp"]);
 	}

+ 4 - 0
tests/runci/targets/Neko.hx

@@ -13,6 +13,10 @@ class Neko {
 		runCommand("haxe", ["compile-neko.hxml"].concat(args));
 		runCommand("neko", ["bin/neko/sys.n"]);
 
+		changeDirectory(eventLoopDir);
+		runCommand("haxe", ["build.hxml", "--neko", "bin/test.n"]);
+		runCommand("neko", ["bin/test.n"]);
+
 		// changeDirectory(threadsDir);
 		// runCommand("haxe", ["build.hxml", "-neko", "export/threads.n"]);
 		// runCommand("neko", ["export/threads.n"]);

+ 6 - 0
tests/runci/targets/Python.hx

@@ -79,5 +79,11 @@ class Python {
 		for (py in pys) {
 			runCommand(py, ["test.py"]);
 		}
+
+		changeDirectory(eventLoopDir);
+		runCommand("haxe", ["build.hxml", "--python", "bin/test.py"]);
+		for (py in pys) {
+			runCommand(py, ["bin/test.py"]);
+		}
 	}
 }

+ 4 - 3
tests/threads/src/cases/Issue3767.hx

@@ -8,8 +8,8 @@ class Issue3767 implements ITest {
 
 	#if (java || python)
 
-	@:timeout(5000)
-	function testBasicLock(async:utest.Async) {
+	function testBasicLock() {
+		var mainLock = new Lock();
 		Thread.create(() -> {
 			var lock = new Lock();
 			//it starts locked
@@ -43,8 +43,9 @@ class Issue3767 implements ITest {
 				lock.release();
 			});
 			Assert.isTrue(lock.wait());
-			async.done();
+			mainLock.release();
 		});
+		Assert.isTrue(mainLock.wait(2.0));
 	}
 
 	#end

+ 4 - 4
tests/threads/src/cases/Issue4878.hx

@@ -1,6 +1,5 @@
 package cases;
 
-import utest.Async;
 import utest.Assert;
 import utest.ITest;
 
@@ -9,8 +8,8 @@ class Issue4878 implements ITest {
 
 	#if (java || python)
 
-	@:timeout(5000)
-	function test(async:Async) {
+	function test() {
+		var lock = new Lock();
 		Thread.create(() -> {
 			var mutex = new Mutex();
 			Thread.create(function() {
@@ -24,8 +23,9 @@ class Issue4878 implements ITest {
 			Assert.isFalse(mutex.tryAcquire());
 			Sys.sleep(.3);
 			Assert.isTrue(mutex.tryAcquire());
-			async.done();
+			lock.release();
 		});
+		Assert.isTrue(lock.wait(2.0));
 	}
 
 	#end

+ 4 - 4
tests/threads/src/cases/Issue8063.hx

@@ -1,18 +1,18 @@
 package cases;
 
-import utest.Async;
 import utest.Assert;
 import utest.ITest;
 
 class Issue8063 implements ITest {
 	public function new() { }
 
-	@:timeout(5000)
-	function test(async:Async) {
+	function test() {
+		var lock = new Lock();
 		Assert.isTrue(Thread.current() == Thread.current());
 		Thread.create(() -> {
 			Assert.isTrue(Thread.current() == Thread.current());
-			async.done();
+			lock.release();
 		});
+		Assert.isTrue(lock.wait(2.0));
 	}
 }

+ 4 - 4
tests/threads/src/cases/TestThreads.hx

@@ -1,18 +1,18 @@
 package cases;
 
-import utest.Async;
 import utest.Assert;
 
 class TestThreads implements utest.ITest
 {
 	public function new() { }
 
-	@:timeout(40000)
-	function testSort(async:Async) {
+	function testSort() {
+		var lock = new Lock();
 		Thread.create(() -> {
 			doTestSort();
-			async.done();
+			lock.release();
 		});
+		Assert.isTrue(lock.wait(40.0));
 	}
 
 	private function doTestSort()

+ 5 - 3
tests/threads/src/cases/WeirdTreeSum.hx

@@ -2,6 +2,7 @@ package cases;
 
 import sys.io.File;
 import utest.Assert;
+import sys.thread.Lock;
 
 using StringTools;
 class Ref<T> {
@@ -57,8 +58,8 @@ typedef TreeNode<T> = {
 class WeirdTreeSum implements utest.ITest {
 	public function new() {}
 
-	@:timeout(2000)
-	public function test(async:utest.Async) {
+	public function test() {
+		var lock = new Lock();
 		Thread.create(() -> {
 			var fileContent = File.getContent("res/tree1.txt");
 			var buf = new StringBuf();
@@ -68,8 +69,9 @@ class WeirdTreeSum implements utest.ITest {
 			}
 			var tree = parseTree(buf.toString().trim())[0];
 			compare(tree);
-			async.done();
+			lock.release();
 		});
+		Assert.isTrue(lock.wait(2.0));
 	}
 
 	static function compare(tree:Tree<Int>) {