Threading query

BlitzMax Forums/BlitzMax Programming/Threading query

How would I go about loading an image (or sound, or some other file) in a separate thread, then pass the data to the main thread for use?

There seems to be functions for 'thread storage' - AllocThreadData, SetThreadData, GetThreadData etc, and I'd assume it to be something to do with this, but the explanation for each doesn't reach to more than four words at best - The help for GetThreadData() simply reads "Get thread storage data", which is no help at all.

Just load what you want on the secondary thread, and share the variable on the main thread (it can be a return variable on the called function, you may access using MyThread.GetThreadData once the TThread.Status is done).

Be sure to not be using it unless the loading thread has ended. if using the TThread class, check the status variable to ensure it. Loading images on a secondary thread on OpenGL does not work well sometimes. I've seen no problems on DX.

The data allocation you're talking about is used to pass parameters on the threaded first function call, and it can be ignored most of the times, unless you're making a very speciffic thing.

[EDIT:] I confused GetThreadData with the Result() method. See next post.

So.... what does GetThreadData actually return? The raw data itself? A pointer? An object?

That is a simple example, I hope it'll make it clearer:
Local T:TThread = New TThread

t.Start(LoadImage1, "MyImage.bmp")

While t.Status() <> TThread.STOPPED
	
WEnd

Local MyImage:TImage = TImage(T.Result())

Function LoadImage1:Object(Obj:Object)
	Local MyImage:TImage = LoadImage(String(Obj))
	Return MyImage
End Function


this example is not very functional, as the main thread waits for the secondary thread doing nothing, but it shows how the class works as a starting point.

GetThreadData is something you should not need when using the high level TThread class, wich I would recommend you to use.

[EDIT:] The OpenGL issue seems to happen only when using GL commands from several threads, not when loading images or sound.

How would I go about loading an image (or sound, or some other file) in a separate thread, then pass the data to the main thread for use?


I haven't looked into threads yet myself so I can't really answer your question directly, but depending on what you need the data for you can also approach this the other way around:

for example, if you need a loading screen:

1) Spin off your loading screen to a seperate thread which just shows an animated progress bar etc.
2) have the main thread continue loading images
3) when the loading is completed, let the animation thread know to end itself, and have the main thread take over the screen again.

(Unless you're looking to load files in the background while the main game is already in progress of course, in that case the above obviously won't suffice)

@xlsior: If I'm not wrong, you can not draw anything to a graphics context from a thread that has not created the graphic context, so you can't do it this way. specially when using OpenGL. DX may work, but may have some flickering sometimes. So theorically your approach should work, but it doesn't becouse the the non thread safeness of GL and DX.

Ziggy - that helps a lot. Thanks.

But why this:
Function LoadImage1:Object(Obj:Object)
	Local MyImage:TImage = LoadImage(String(Obj))
	Return MyImage
End Function

Instead of this:
Function LoadImage1:TImage(Obj:String)
	Local MyImage:TImage = LoadImage(String(Obj))
	Return MyImage
End Function

??

Becouse the signature of the function called by any thread starter has to be:
Name:Object(Var:Object)

Otherwise you'll get: Unable to convert from 'brl.max2d.TImage(String)' to 'Object(Object)'

but you can cast everything inside and outside the function, so not really a major issue.

This might possibly be of some help, though it downloads images from the internet while animating, so you'll have to let it go online. (Leave it running a while as it downloads multiple images.)

It's a messy work-in-progress, but I'm only loading Pixmaps outside the main thread, as my understanding is that Images are tied up with graphics contexts (not thread-friendly), while Pixmaps are only blocks of memory. I just convert the resulting Pixmap to an Image once it's returned.

' Needs SVN version of BlitzMax, with Program menu
' -> Build Options -> Threaded Build enabled!

' Sometimes gives ***** UNKNOWN THREAD EXCEPTION ***** on exit...

' EDIT: Handling ESC while DownloadPixmap's Read/WriteStream are
' open ought to solve this. Think a message needs to be sent to this
' part of the thread before End to allow CloseStream, etc.

' Stupid delay code fixed (so that's what Delay is for)...

' Converted returned pixmap to images for more flexibility...

' Spinning and shadows! Might have been a bit early for this sort of thing...

' Single-CPU test function (Windows only)...

' Tidied and commented...

Strict

' Thread functions...

' DownloadPixmapThread: Calls DownloadPixmap in background. Uses pixmaps
' instead of images as images interact with graphics drivers (not thread-
' friendly) while pixmaps are just blocks of memory...

Function DownloadPixmapThread:Object (data:Object)

	' Index for global Pic[] array...
	
	Local img:Int = 0

	' Infinite loop while main program runs...
	
	Repeat

		Local photo:TPixmap = DownloadPixmap (Pic[img])	' Download pixmap...
		
		TThread.Main ().SendMsg photo					' Send copy to main program...
		
		img = img + 1								' Image index increased for
		If img > PicNum - 1 Then img = 0				' next URL...

		Delay Timeout								' Wait before downloading a
												' new image...
	Forever

End Function

' Timer for redrawing. Sends message to main program advising when to redraw...

Function RenderTimerThread:Object (data:Object)

	Repeat
		' Delay 16
		TThread.Main ().SendMsg Null
	Forever
	
End Function

' Enable next line to force everything onto first CPU on multicore systems...
' ForceSingleCPU () ' Windows only!

' Wait this many milliseconds after each download...

Global Timeout = 5000

AppTitle = "New download attempted after " + (Timeout / 1000) + " seconds (IDE Output window shows progress)..."

Const PicNum = 7
Global Pic$ [PicNum]

Pic [0] = ConvURL ("http://www.blitzbasic.com/img/platypus.jpg")
Pic [1] = ConvURL ("http://www.blitzbasic.com/img/auto_cross_racing.jpg")
Pic [2] = ConvURL ("http://www.blitzbasic.com/img/super_gerball.jpg")
Pic [3] = ConvURL ("http://www.blitzbasic.com/img/tank_universal.jpg")
Pic [4] = ConvURL ("http://www.blitzbasic.com/img/tecno.jpg")
Pic [5] = ConvURL ("http://www.blitzbasic.com/img/master_of_defence.jpg")
Pic [6] = ConvURL ("http://www.blitzbasic.com/img/kingdom_elemental_tactics.jpg")

' Try a big pic! Comment out previous line, uncomment next...

Pic [0] = ConvURL ("http::nuerburgring.de/fileadmin/webcam/webcam.jpg")

Graphics 640, 480

SetClsColor 71, 132, 217
AutoMidHandle True
SetBlend ALPHABLEND

' List for copies of downloaded images...

Local ImgList:TList = CreateList ()

' Downloaded image control type...

Type Img
	Field image:TImage
	Field x#
	Field y#
	Field xs# = Rnd (-8, 8)
	Field ys# = Rnd (-8, 8)
	Field ang#
	Field angsp# = Rnd (-4, 4)
End Type

' Start threads...

New TThread.Start DownloadPixmapThread, Null
New TThread.Start RenderTimerThread, Null

Local photo:TPixmap
Local i:Img
Local image:TImage

Repeat

	Local mx:Int = MouseX ()
	Local my:Int = MouseY ()
	
	' Get thread messages...
	
	Local msg:Object = TThread.RecvMsg ()
	TThread.ReplyMsg Null
	
	' Check if msg is a pixmap...
	
	If TPixmap (msg) ' Came from DownloadPixmapThread...
	
		' Create local pixmap and convert to image (for flexibility)...
		
		photo = TPixmap (msg)
		image:TImage = LoadImage (photo)

		' Add copy of image to list...
		
		i:Img = New Img
		i.image = image
		i.x = mx
		i.y = my
		
		ListAddLast ImgList, i

	Else	' Must be from RenderTimerThread...

		Cls

		' Draw twirling copies of downloaded images...
		
		For i:Img = EachIn ImgList
		
			i.ang = i.ang + i.angsp
			
			SetRotation i.ang
			DrawImage i.image, i.x, i.y
			
			i.x = i.x + i.xs
			i.y = i.y + i.ys
			
			If i.x < 0 Or i.x > GraphicsWidth () - 1 Then i.xs = -i.xs; i.x = i.x + i.xs
			If i.y < 0 Or i.y > GraphicsHeight () - 1 Then i.ys = -i.ys; i.y = i.y + i.ys
			
		Next

		' Reset image rotation...
		
		SetRotation 0

		' If we have a downloaded image, draw it...
		
		If image
		
			' Shadow...
			
			SetAlpha 0.25
			SetColor 0, 0, 0
			DrawRect (mx - ImageWidth (image) / 2.0) + 8, (my - ImageHeight (image) / 2.0) + 8, ImageWidth (image) + 2, ImageHeight (image) + 2
			
			' Border...
			
			SetAlpha 1
			SetColor 255, 255, 255
			DrawRect (mx - ImageWidth (image) / 2.0) - 1, (my - ImageHeight (image) / 2.0) - 1, ImageWidth (image) + 2, ImageHeight (image) + 2
			
			' Draw image...
			
			DrawImage image, mx, my
			
		EndIf
		
		Flip
		
	EndIf
	
Until KeyHit (KEY_ESCAPE)

End

' Non-thread functions...

Function DownloadPixmap:TPixmap (img$, bail = 3)

	Local pix:TPixmap		' Downloaded pixmap...
	
	Local url:TStream		' Download stream...
	Local copy:TStream		' Local copy of download...
	
	Local count:Int		' Byte count during download...
	
	Local retry:Int		' If download fails, retry 'bail' times...
	Local quit:Int			' Too many fails, exit...
	
	Repeat

		Print ""
		Print "Attempting new download..."
		Print ""
		
		url = ReadStream (img$)

		If url

			' Create local copy...
			
			copy = WriteStream ("local.jpg")
			
			If copy
			
				' Reset byte count...
				
				count = 0

				Repeat

					' Not every efficient (one byte at a time), but works as a demo...
					
					WriteByte copy, ReadByte (url)

					' Count bytes downloaded...
					
					count = count + 1
					If count Mod 1024 = 0 Then Print count + " bytes downloaded in background"
					
				Until Eof (url)

				CloseStream copy

				' Try to load pixmap...
				
				pix = LoadPixmap ("local.jpg")
				
			EndIf
						
			CloseStream url

		EndIf

		' Download failed? Retry 'bail' times...
		
		If pix = Null

			retry = retry + 1

			If retry = bail
			
				quit = True
				Print "Failed to download after " + bail + " attempts..."
			
			Else
			
				Print "Retrying..."
				
			EndIf

		EndIf
				
	Until pix Or quit

	Return pix ' May still be Null after 'bail' failed attempts...
	
End Function

Function ConvURL$ (url$)
	Return Replace (url$, "://", "::")
End Function

Function ForceSingleCPU () ' Windows only!

?win32

	Extern "win32"
		Const PROCESS_SET_INFORMATION = $200
		Function GetCurrentProcessId ()
		Function OpenProcess (desiredaccess, inherithandle, processid)
		Function SetProcessAffinityMask (processhandle, processaffinitymask)
	End Extern
	
	Local proc:Int = GetCurrentProcessId ()
	Local handle:Int = OpenProcess (PROCESS_SET_INFORMATION, 0, proc)
	
	SetProcessAffinityMask (handle, %00000000000000000000000000000001)

?

End Function


How comes I don't have a 'tThread' datatype?

[edit] Never mind - didn't realise I had to import pub.thread.

OK, another query. Why do DetatchThread() and WaitThread() expect an Integer, not a TThread?

WaitThread expects an integer that represents the number of millisecs to wait. DetatchThread does not expect an integer but a TThread instance (the numeric object handle is passed to the internal C function, so it is seen by any ide as a integer, but internally it uses the object instance.)

I see...

Just noticed a file called 'test.exe.log" is being created every time I run my program. It contains one line; "GC Warning: Finalization cycle involving d19258"

What's that about??

I have no idea, that is always created on threaded apps, it may be something related to how max handles threads or something

It's the new GC framework. It creates that file by default.
I'm sure there's a flag somewhere in there to disable it...

Hmm... am I better off just not using this stuff?

I can't release a game with it doing that. Even though it works great otherwise.

Another thing - if I compile "mycode.bmx", how come it creates another file called ".mycode.bmx"?

The AllocThreadData, SetThreadData and GetThreadData are to be used for `Thread Local Storage`, sometimes referred to as TLS.

Basically, when you need to create some storage space which is totally confined to and local to a thread - ie unique to each thread, you can allocate that storage space using AllocThreadData(), you can store some kind of Object in it using SetThreadData, and retrieve it using GetThreadData. I don't think there is any kind of `deallocate thread storage` command so I presume they just pile up for the lifetime of the thread.

It would be basically the same thing as using local variables within the function that the thread is executing. In fact there isn't a lot of benefit from using TLS than just using local variables, except perhaps if you have quite a number of them to keep track of. Using locals will be faster because it doesn't have to go find the data based on a handle.

You start by allocating some storage space local to the thread, by assigning a handle to the return value of AllocThreadData(). e.g. myData:Int=AllocThreadData(). The handle is an integer. Its value is not predictable.

Once you've created the handle, you've made space in the thread's local memory (stack presumably) for storing one piece of information. From what I could tell, each time you call this function to allocate more room it decreases the handle's value by 8 - which suggests you can store anything up to 8-bytes as your data (to support doubles and longs). Not sure about that.

When you have a handle you can then store data in it by saying SetThreadData(MyData,MyStuff) and can later retrieve it from anywhere in the thread's scope of execution using MyStuff=GetThreadData(MyData). Not sure if you need to cast to Object/back to your Type. You should be able to store integers and all the data types as well as custom types.

TLS cannot be used to transfer information between threads. It's Thread LOCAL storage, not thread shared storage. The only way to get access to the stored data is for the accessing function call to occur within the thread that allocated and stored the data. If you want to use it to somehow pass data to another thread, you can't, unless the data that the local thread retrieves is then stored in a globally accessible place afterwards.

One of my gripes about TLS is that in order for you to store and retrieve a piece of data that is local to the thread, you have to have a handle. The handle lives OUTSIDE of thread local storage. You can't really have a handle stored within TLS itself. So then you have the issue of having to store the handle in a local, a field, or a global, or passing it to functions as a parameter. This largely undermines the whole purpose of TLS. If you try to store data in your main function and you want it to be retrievable in some deeply nested function call, you can't just have that nested function say x=GetThreadData(handle) because you have to have access to the handle first, which means you have to share it globally (which means you can't have local handles), or pass it as a parameter, or pass the handle of the type instance containing a field, which means you might as well be passing the value that you stored anyway, so what's the point?! (unless you have lots of data to store, maybe?)

I eventually settled on just passing the one piece of data that I wanted to store in TLS, as a function parameter. Not only is it faster to access than going via an indirect retrieval function, but it was easier to implement.

This of course has nothing to do with how to load images/sounds in a threaded way. I like James's suggestion of loading into a pixmap rather than an image, because that does mean you are only loading memory bytes outside of the gpu/graphics API. LoadImage loads a pixmap first anyway before uploading as a texture.

So.... anybody know how to get rid of the error/log file?

Its doing my head in now.

Turn Debug off?

Cheers
Charlie

Nope, tried that.

Interesting. I get the GC log file error if I load an OGG format sound in a thread - even though the loaded sound is returned to the main thread and played correctly. I don't get the error if I load an image in a thread (which is pointless, since that's a fast process anyway).

Why would this be?

Some utterly pointless code, strictly for fiddling purposes:
Import pub.threads
Strict

Graphics 800,600
Local t:tthread = New tthread
t.start(loadsnd,"test.ogg")

Local music:TSound
Local channel:TChannel
While Not KeyDown(key_escape)
	Cls
	DrawText "Time: " + CurrentTime(),10,10
	DrawText MilliSecs(),10,30
	If t.status() = t.STOPPED
		If Not channel
			music = TSound(t.result())
			channel = PlaySound(music)
		EndIf
	EndIf
	Flip
Wend

Function loadSnd:Object(data:Object)
	Local s:TSound = LoadSound(String(data))	
	Return s
End Function


Well... I've stopped it doing the logfile thing by commenting out the line that does it (line 535) in c:\BlitzmaxSVN\mod\brl.mod\blitz.mod\bdwgc\finalise.c
WARN("Finalization cycle involving %lx\n", real_ptr);

That's removing the warning though, not the cause. So, not ideal.