COMMUNITY

Threads and the garbage collector

haxe-cpp

(David) #1

Hi!

I have a crash / error and I’m assuming my mental model of threads and the garbage collector is missing something. I’ve read everything I can find but the docs are a bit brief on the topic and I haven’t got a clear picture. Can someone help me understand what’s happening?

Edit: Actually the program dies with a much shorter piece of code than I originally posted. It’s so simple now I’m wondering is this a bug in hxcpp?

class Main {
	public static function looper_thread() {
		trace ('thread start');
		while (true)
			trace (haxe.Timer.stamp());
	}

	public static function main () {
		cpp.vm.Thread.create (looper_thread);
		while (true)
			cpp.vm.Gc.run(true);
	}
}

I’d expect it to print the time endlessly, but within a couple of minutes the program dies silently.

– The remainder of the original post below –

I shortened the problem down to the code below - I have a buffer that is created and accessed with raw c++. It’s allocated once in the main thread and repeatedly accessed from a separate thread after that.

Here’s some things that I think are true about this code. Which of these are wrong :smile: ? -

  • The garbage collector runs in the main thread.
  • I need to call Gc.safePoint because the child thread doesn’t allocate and so if the collector runs it would deadlock trying to “stop the world”.
  • The collector can’t see the pointer *data because it’s not on the stack and the collector doesn’t follow arbitrary class data.
  • Buffer.finalizer is never called, which seems correct (the reference Main.buffer stays in scope)
  • As a result, buffer->data should be safe to access.
  • Even if the gc does compaction (I dunno if that’s enabled), it should still be safe to access buffer->data as that isn’t part of the collected memory and won’t be moved.

Compile command and example run:

haxe -main Main -cpp bin
bin\Main.exe

Main.hx:46: hi
Main.hx:25: thread start
Main.hx:29: 0
Main.hx:29: 1
Main.hx:29: 2
Main.hx:29: 3
Main.hx:29: 4
Main.hx:29: 5
Critical Error: Bad Allocation while collecting - from finalizer?

Tested on Haxe 4-rc1, compiled to VS2017. Also same behaviour on Haxe 3.4.7. It’s random how far it gets before the error. Sometimes there’s no error message it just dies silently.

// Main.hx

@:headerClassCode('int *data;')
class Buffer extends cpp.Finalizable {
	public function new () {
		super();
		init();
	}

	private function init() {
		untyped __cpp__ ('this->data = new int[1000]');
		untyped __cpp__ ('for (int i = 0; i < 1000; i ++) this->data[i] = i');
	}

	override function finalize() {
		trace ('Uhoh I got deleted that is NO GOOD');
		untyped __cpp__ ('delete[] this->data');
	}
}

class Main {
	var buffer : Buffer;

	public function looper_thread() {
		trace ('thread start');
		while (true) {
			for (i in 0...1000) {
				var x : Int = untyped __cpp__('this->buffer->data[{0}]', i);
				trace (x);
				cpp.vm.Gc.safePoint();
			}
		}
	}

	public function new () {
		buffer = new Buffer();
		cpp.vm.Thread.create (looper_thread);
		while (true) {
			// This induces the crash much faster,
			// but if we did nothing eventually the gc would run and crash anyway
			cpp.vm.Gc.run(true);
		}
	}

	public static function main () {
		trace ('hi');
		new Main ();
	}
}

(Mike Robinson) #2

I’m not sure what is wrong with your example code – haven’t tried to run it yet – but with regard to your original post I wonder: does the main thread keep its reference to the string that it allocated? Or, does the garbage collector eventually reap it out from under the child thread’s nose? Does the child also establish a reference to it, so that the reference-count is now at least “2?”


(David) #3

As far as I can tell there was a reference held to everything in the original post. But see the shorter example now - there aren’t any references to rely on anywhere, it’s incredibly simple. The trace function presumably allocates some string buffers so the interaction with the gc would be in there somewhere, but it seems like something that should work.


(Mike Robinson) #4

I also expect that your simple example should work forever … and that, if it should stop working (especially for any of the reasons suggested), it should not do so “silently.”


(Randy Maxwell) #5

Suggest you try:
Put in code to call stdin.readLine( ) to wait before starting the Thread stuff.
Make sure that Debug build was done.
Debugger like Visual Studio or Visual Studio Code (other Debuggers should do OK here)
Attach to the running Process
THEN hit Enter key to start Thread stuff.

OR:
Use a Profiler to see what Resources are doing.
You likely still need to Attach to Running Process so waiting for Keyboard entry still helps.
Maybe Memory is being allocated and never released.
I think for Release builds when an app hogs resources (at least for some combinations on Windows)
the OS just quietly kills the process with no Notifications. Also on Windows there are App level and System level Event logs that typically contain details about WHY an App or a System process was Terminated. I am guessing that Linux, Mac OS has similar info available, just have to dig around.


(Mike Robinson) #6

Yeah, and what’s bothering me about the original report is that the thing “silently and calmly” quits working. Surely there will be an application crash-report someplace. In my experience, if you rip a piece of memory out from under a running process or a thread: “All hell breaks loose. Very messy.” (Remember “GPFs” and “blue screens of death?” GPF is “General Protection Fault.”)