Bug 2278 - InvokeOnMainThread() does not execute on main thread
Summary: InvokeOnMainThread() does not execute on main thread
Status: RESOLVED NOT_REPRODUCIBLE
Alias: None
Product: iOS
Classification: Xamarin
Component: XI runtime ()
Version: 5.0
Hardware: Macintosh Mac OS
: --- normal
Target Milestone: Untriaged
Assignee: Bugzilla
URL:
Depends on:
Blocks:
 
Reported: 2011-11-30 15:36 UTC by René Ruppert
Modified: 2011-12-05 15:48 UTC (History)
3 users (show)

Tags:
Is this bug a regression?: ---
Last known good build:


Attachments
Project to demonstrate the issue (7.26 KB, application/zip)
2011-11-30 15:36 UTC, René Ruppert
Details


Notice (2018-05-24): bugzilla.xamarin.com is now in read-only mode.

Please join us on Visual Studio Developer Community and in the Xamarin and Mono organizations on GitHub to continue tracking issues. Bugzilla will remain available for reference in read-only mode. We will continue to work on open Bugzilla bugs, copy them to the new locations as needed for follow-up, and add the new items under Related Links.

Our sincere thanks to everyone who has contributed on this bug tracker over the years. Thanks also for your understanding as we make these adjustments and improvements for the future.


Please create a new report on Developer Community or GitHub with your current version information, steps to reproduce, and relevant error messages or log files if you are hitting an issue that looks similar to this resolved bug and you do not yet see a matching new report.

Related Links:
Status:
RESOLVED NOT_REPRODUCIBLE

Description René Ruppert 2011-11-30 15:36:01 UTC
Created attachment 977 [details]
Project to demonstrate the issue

The issue: if InvokeOnMainThread() is used, the code is not necessarily executed on the main thread if the calling code is already running in a separate thread.


Attached find some code to demonstrate the problem. It's an MD 2.8 project.

The code gives the main thread a meaningful name "Main thread".

There are four test cases which are commented and explained in the code.

Most interesting is test case number 4: it starts a timer, and if the timer ticks it starts a thread. That thread does InvokeOnMainThread() and executes a method that spits out the current thread's name. The output clearly shows "Subthread" and not the expected "Main thread".

To overcome the problem, test case 3 can be run: it does a double main thread invocation and the output is the expected "Main thread".

The other cases demonstrate situations that work as expected and case  fails randomly but it is unclear to me why.

I found this while implementing a PDF viewer the previews pages. After a while of inactivity (Timer) a thread is spawned that renders a page preview. Without the double invocation I got UI lockups.
Comment 1 Sebastien Pouliot 2011-11-30 15:41:33 UTC
ref: http://stackoverflow.com/a/8327363/220643
Comment 2 Sebastien Pouliot 2011-12-05 13:46:49 UTC
I fail to see why Test #4 is wrong. The delegate, assigned to `Elapsed`, executes on a separate thread (the timer thread), you then ask for another delegate to be execute on the *main* thread (which it does, see my example below) but then you're starting a new thread (why?) which is calling the C.WL (and showing the subthread id/name).

I think what you're looking for is:

		// this is executed on the main thread
		Console.WriteLine ("main thread id {0}", Thread.CurrentThread.ManagedThreadId);
		timer.Elapsed += delegate(object sender, System.Timers.ElapsedEventArgs e) {
				// this is executed on the timer's thread
				Console.WriteLine ("timer.Elapsed thread id {0}", Thread.CurrentThread.ManagedThreadId);
				this.InvokeOnMainThread(delegate
				{
					// this will be executed on the main thread
					Console.WriteLine ("InvokedByThread2OnMainThread thread id {0}", Thread.CurrentThread.ManagedThreadId);
					this.InvokedByThread2OnMainThread();	
				});
			};
			timer.Start();
	
which will log:

main thread id 1
timer.Elapsed thread id 7
InvokedByThread2OnMainThread thread id 1
Comment 3 René Ruppert 2011-12-05 14:57:41 UTC
I get your example and it is clear to me but I tried to build more complex cases where things are not so clear.

The thinking was: If I use

InvokeOnMainThread( delegate
{
// CODE
})

Will everything between the curly braces e executed on the main thread, no matter what this code is doing? If "// CODE" decides to spin off another thread it also gets started inside the curlies and hence I would assume it also executes on the main thread. But obviously that's not true.

Let me try to explain the real world example behind this code.
I have a method that renders a thumbnail of a page of a PDF doc. This method starts a thread to perform this job and returns immediately.

Now I'm using this method to start rendering a bunch of thumbnails and all is working as expected, they show up when they're done.

But now I want to use the thumbnail generation in a slightly different scenario. If the user uses a UISlider to change the page and lets the slider come to a rest, I will start a timer. If that timer reaches 200ms, I start rendering the thumbail using the same code as I did before. The delay is used to prevent constant thumb rendering is the user just stops shortly. As the thumbnail thread already uses InvokeOnMainThread() to draw its preview image I did not see sense in wrapping it into another InvokeOnMainThread() on the timer delegate and found out that rendering fails. The reason: the InvokeOnMainThread() of the thumbnail rendering does not invoke on the main thread but on the timer thread.

And yes, maybe there are ways to implement it differently but the main things I'm wondering about:

* If I nest Threads (for whatever stupid reason) and the innermost thread uses InvokeOnMainThread(), is it supposed to execute on the main thread or on the innermost - 1 thread?
* If I put a InvokeOnMainThread() in the outermost thread and create my nested threads inside, are the inner threads supposed to be executed on the main thread or not? (I know this is stupid but it needs a definition I think)
* My real test case is number three. You may ignore the rest. The question is: is the double InvokeOnMainThread() supposed to be necessary here? The one on the inner thread should IMHO be enough.

In short: take case 3 alone - do you think there is an issue?

Puh, lots of info. :-)
Comment 4 Sebastien Pouliot 2011-12-05 15:39:43 UTC
As it's name implies:

InvokeOnMainThread( delegate
{
// CODE
})

ensure that your delegate is *invoke*d on the main thread. What you do from there, e.g. async calls, using a thread pool, creating new threads... is up to you. IOW nothing will enforce the code execution to remain on main thread.

> * If I nest Threads (for whatever stupid reason) and the innermost thread uses
> InvokeOnMainThread(), is it supposed to execute on the main thread or on the
> innermost - 1 thread?
>
> * If I put a InvokeOnMainThread() in the outermost thread and create my nested
> threads inside, are the inner threads supposed to be executed on the main
> thread or not? (I know this is stupid but it needs a definition I think)

The code will execute on the current thread. You can change the current thread, e.g. by creating a new one or by calling InvokeOnMainThread to get back to the main thread. Rules won't change for nesting them - at least not until you deadlock them where execution will stop ;-)

> * My real test case is number three. You may ignore the rest. The question is:
> is the double InvokeOnMainThread() supposed to be necessary here? The one on
> the inner thread should IMHO be enough.

In doubt just add a few CWL and try it :-) E.g.

			// Double-main-invocation. If the timer fires, another thread is started. Output "Main thread"; but only if double-invocation is used
			// RESULT: FISHY. DOUBLE INVOCATION IS REQUIRED TO HAVE THE CODE EXECUTED ON THE MAIN THREAD.
			Console.WriteLine ("main thread id {0}", Thread.CurrentThread.ManagedThreadId);
			timer.Elapsed += delegate(object sender, System.Timers.ElapsedEventArgs e) {
				Console.WriteLine ("timer.Elapsed thread id {0}", Thread.CurrentThread.ManagedThreadId);
				this.InvokeOnMainThread(delegate
				{
					Console.WriteLine ("InvokeOnMainThread #1 thread id {0}", Thread.CurrentThread.ManagedThreadId);
					Thread subThread = new Thread(delegate()
					{
						Console.WriteLine ("subThread thread id {0}", Thread.CurrentThread.ManagedThreadId);
						this.InvokeOnMainThread(delegate
						{
							Console.WriteLine ("InvokeOnMainThread #2 thread id {0}", Thread.CurrentThread.ManagedThreadId);
							this.InvokedByThread2OnMainThread();	
						});
					});
					subThread.Name = "Subthread";
					subThread.Start();
				});
			};
			timer.Start();			


main thread id 1
Thread started: 
Thread started: 
timer.Elapsed thread id 7
InvokeOnMainThread #1 thread id 1
Thread started: Subthread
subThread thread id 8
InvokeOnMainThread #2 thread id 1
InvokedByThread2OnMainThread thread id 1
Thread finished: Subthread
timer.Elapsed thread id 7
InvokeOnMainThread #1 thread id 1
Thread started: Subthread
subThread thread id 9
InvokeOnMainThread #2 thread id 1
InvokedByThread2OnMainThread thread id 1
Thread finished: Subthread
timer.Elapsed thread id 7
InvokeOnMainThread #1 thread id 1
Thread started: Subthread
subThread thread id 10
InvokeOnMainThread #2 thread id 1
InvokedByThread2OnMainThread thread id 1
Thread finished: Subthread

That shows where everything gets executed, the main thread (1), the timer thread (7) and the thread you manually create (8, 9, 10...).

> The question is:
> is the double InvokeOnMainThread() supposed to be necessary here?

No (at least not to be executed on the main thread*). From the previous log you can see that the delegate called with InvokeOnMainThread are *always* executed on the main thread (1), that the Timer thread is always the same (7, invoked at the determined interval) and that each thread you created are unique. 

* maybe you're doing something else in your own thread that affects your application (and the 2nd InvokeOnMainThread hides/fixes this) ? but that does not change where the execution is being done.

I'm closing the bug report as InvokeOnMainThread is always executed on the main thread. If the above does not help, wrt your application, then I think you better try to explain in more details what you want to achieve on the mailing-list.
Comment 5 René Ruppert 2011-12-05 15:48:50 UTC
I think I got the point. The "*invoke*d" made me rethink it :-)