Bug 36655 - Multiple Task.Run not run simultaneously
Summary: Multiple Task.Run not run simultaneously
Status: RESOLVED FIXED
Alias: None
Product: iOS
Classification: Xamarin
Component: BCL Class Libraries ()
Version: unspecified
Hardware: Macintosh Mac OS
: --- blocker
Target Milestone: Untriaged
Assignee: Ludovic Henry
URL:
Depends on:
Blocks:
 
Reported: 2015-12-06 14:48 UTC by Alexandre Pepin
Modified: 2015-12-15 14:19 UTC (History)
5 users (show)

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


Attachments
Sample (3.11 MB, application/zip)
2015-12-07 19:27 UTC, Alexandre Pepin
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 FIXED

Description Alexandre Pepin 2015-12-06 14:48:46 UTC
# Steps to reproduce
Add this code to your root view controller
public override void ViewDidAppear(bool animated)
{
	UIButton button = new UIButton(UIButtonType.RoundedRect);
	button.SetTitle("Tap me", UIControlState.Normal);
	button.BackgroundColor = UIColor.Red;
	button.TouchDown += this.Button_TouchDown;
	button.Frame = new CGRect(100, 100, 100, 100);
	this.View.Add(button);

	base.ViewDidAppear(animated);
}

private int Count = 0;

private string DoSomethingHeavy()
{
	this.Count++;
	int currentCount = this.Count;
	Console.WriteLine("Doing something heavy : " + currentCount);
	string worker = "";
	for (int i = 0; i < 10000; i++)
	{
		worker += i.ToString();
	}
	Console.WriteLine("DONE Doing something heavy : " + currentCount);
	return worker;
}

private void Button_TouchDown(object sender, EventArgs e)
{
	Task.Run(() => this.DoSomethingHeavy());
	Task.Run(() => this.DoSomethingHeavy());
	Task.Run(() => this.DoSomethingHeavy());
	Task.Run(() => this.DoSomethingHeavy());
	Task.Run(() => this.DoSomethingHeavy());
	Task.Run(() => this.DoSomethingHeavy());
	Task.Run(() => this.DoSomethingHeavy());
	Task.Run(() => this.DoSomethingHeavy());
	Task.Run(() => this.DoSomethingHeavy());
	Task.Run(() => this.DoSomethingHeavy());
}

# Expected behavior
That all 10 tasks run simultaneously

# Actual behavior
On my iPad mini, only 2 tasks are run simultaneously. The other tasks are only started when the previous two are completed

# Supplemental info (logs, images, videos)
This wasn't a problem before we upgraded to Xamarin 4

# Test environment (full version information)
Microsoft Visual Studio Professional 2015
Version 14.0.24720.00 Update 1
Microsoft .NET Framework
Version 4.6.01055
Installed Version: Professional
Xamarin   4.0.0.1697 (deffc90)
Visual Studio extension to enable development for Xamarin.iOS and Xamarin.Android.
Xamarin.Android   6.0.0.34 (3efa14c)
Visual Studio plugin to enable development for Xamarin.Android.
Xamarin.iOS   9.2.1.51 (3c0ec35)
Visual Studio extension to enable development for Xamarin.iOS.
Comment 1 Sebastien Pouliot 2015-12-06 15:44:52 UTC
I suspect that might be a change either from adopting more MS RS code (e.g. Task) or in the Mono runtime (e.g. # task == # of core * # of CPU).
Comment 2 Marek Safar 2015-12-07 10:38:18 UTC
We changed ThreadPool implementation which drives Task queuing/dequeuing.
Comment 3 Ludovic Henry 2015-12-07 14:49:08 UTC
There is no guarantee in the simultaneous execution of Task. So the expected behaviour is not correct IMO.

To ensure that they are run simultaneously, you should do:

Task.Factory.StartNew (() => this.DoSomethingHeavy (), TaskCreationOptions.LongRunning)

And to answer Sebastien and Marek, the threadpool implementation has indeed changed, and the heuristic is now a bit smarter in the number of threads it uses to execute work items. And I wouldn't be surprised this iPad mini has 2 cores, thus the 2 tasks running simultaneously.
Comment 4 Alexandre Pepin 2015-12-07 16:17:54 UTC
I do not accept this resolution. If I run the same code in a Windows Console application, all 10 threads are running simultaneously (on my machine with 2 cores). I even increased the number of thread to 20 and they all ran at the same time.

You are doing something different in Mono than in the .Net implementation.
Comment 5 Rodrigo Kumpera 2015-12-07 16:45:32 UTC
Hi Alexandre,

Yes, the behavior is different between the two runtimes and this is expected, threadpool scheduling is very specific to the target platform.

Mono tries to optimize for throughput and resource conservation. Spawning 10 threads to perform CPU bound work on a 2 core device will result is less work being done as there will be a lot of context switches compared to have 10 threads doing it.

The runtime tries to avoid that by adaptively controlling the number of threads and monitoring CPU usage.

The threadpool scheduling behavior is an implementation detail and, just like dotnet, is subject to changes and differences across versions.

Ludovic's suggestion of using TaskCreationOptions to hint the scheduler on how to handle your tasks is the best way to get consistent scheduling across runtimes and versions.

It looks that two of your tasks at a time would saturate the 2 cores of your device. That is in line with our scheduler policy of avoiding trashing and that's why we believe it to be correct.
Comment 6 Alexandre Pepin 2015-12-07 19:03:54 UTC
The big problem with your solution is that we are using a 3rd party dll that creates the task. We are using Microsoft.Tpl.Dataflow. We do not have access to the task creation to set the option

With your new scheduling, you are completely messing up with the data flow. The tasks are completed in batch instead of switching between each tasks.

I can contact Microsoft to ask them to add this option, but until then we are to stick with Xamarin 3?
Comment 7 Alexandre Pepin 2015-12-07 19:27:12 UTC
I created a sample project that represents better our problem and that uses the Microsoft.Tpl.Dataflow.

In Xamarin 3, when I run the application on my iPad mini, I get : 
2015-12-07 14:14:58.204 iPadTests[3083:775820] Number 1 done after : 22.703243
2015-12-07 14:15:07.283 iPadTests[3083:775820] Number 2 done after : 31.822445
2015-12-07 14:15:14.939 iPadTests[3083:775820] Number 3 done after : 39.477945
2015-12-07 14:15:22.521 iPadTests[3083:775820] Number 4 done after : 47.059572
2015-12-07 14:15:30.160 iPadTests[3083:775820] Number 5 done after : 54.698459
2015-12-07 14:15:37.121 iPadTests[3083:775820] Number 6 done after : 61.660186
2015-12-07 14:15:44.717 iPadTests[3083:775820] Number 7 done after : 69.256053
2015-12-07 14:15:52.185 iPadTests[3083:775820] Number 8 done after : 76.724342
2015-12-07 14:15:59.803 iPadTests[3083:775820] Number 9 done after : 84.341952
2015-12-07 14:16:07.433 iPadTests[3083:775820] Number 10 done after : 91.972253
2015-12-07 14:16:14.945 iPadTests[3083:775820] Number 11 done after : 99.48412
2015-12-07 14:16:22.471 iPadTests[3083:775820] Number 12 done after : 107.009669
2015-12-07 14:16:30.044 iPadTests[3083:775820] Number 13 done after : 114.582572
2015-12-07 14:16:37.475 iPadTests[3083:775820] Number 14 done after : 122.014412
2015-12-07 14:16:44.953 iPadTests[3083:775820] Number 15 done after : 129.491643
2015-12-07 14:16:52.397 iPadTests[3083:775820] Number 16 done after : 136.936022
2015-12-07 14:16:59.759 iPadTests[3083:775820] Number 17 done after : 144.298267
2015-12-07 14:17:07.158 iPadTests[3083:775820] Number 18 done after : 151.69715
2015-12-07 14:17:14.482 iPadTests[3083:775820] Number 19 done after : 159.020737
2015-12-07 14:17:21.868 iPadTests[3083:775820] Number 20 done after : 166.406629
2015-12-07 14:17:29.285 iPadTests[3083:775820] Number 21 done after : 173.823504
2015-12-07 14:17:36.084 iPadTests[3083:775820] Number 22 done after : 180.622509
2015-12-07 14:17:41.299 iPadTests[3083:775820] Number 23 done after : 185.837682
2015-12-07 14:17:44.627 iPadTests[3083:775820] Number 24 done after : 189.166184
2015-12-07 14:17:46.336 iPadTests[3083:775811] Number 25 done after : 190.874655

In Xamarin 4, when I run the application on my iPad mini, I get : 
2015-12-07 14:10:24.188 iPadTests[3073:774841] Number 1 done after : 14.329358
2015-12-07 14:10:41.177 iPadTests[3073:774881] Number 2 done after : 31.343447
2015-12-07 14:10:53.811 iPadTests[3073:774881] Number 3 done after : 43.976766
2015-12-07 14:11:18.646 iPadTests[3073:774881] Number 4 done after : 68.812624
2015-12-07 14:11:18.647 iPadTests[3073:774881] Number 5 done after : 68.813604
2015-12-07 14:12:02.919 iPadTests[3073:774841] Number 6 done after : 113.085226
2015-12-07 14:12:02.920 iPadTests[3073:774841] Number 7 done after : 113.086487
2015-12-07 14:12:02.921 iPadTests[3073:774841] Number 8 done after : 113.087261
2015-12-07 14:12:02.922 iPadTests[3073:774841] Number 9 done after : 113.087852
2015-12-07 14:12:33.793 iPadTests[3073:774881] Number 10 done after : 143.959274
2015-12-07 14:12:33.796 iPadTests[3073:774881] Number 13 done after : 143.961741
2015-12-07 14:12:33.794 iPadTests[3073:774881] Number 11 done after : 143.960385
2015-12-07 14:12:33.795 iPadTests[3073:774881] Number 12 done after : 143.961062
2015-12-07 14:12:43.155 iPadTests[3073:774881] Number 14 done after : 153.320938
2015-12-07 14:12:49.968 iPadTests[3073:774881] Number 15 done after : 160.133797
2015-12-07 14:12:56.647 iPadTests[3073:774881] Number 16 done after : 166.812849
2015-12-07 14:13:09.497 iPadTests[3073:774881] Number 17 done after : 179.662803
2015-12-07 14:13:09.498 iPadTests[3073:774881] Number 18 done after : 179.663799
2015-12-07 14:13:16.339 iPadTests[3073:774881] Number 19 done after : 186.50549
2015-12-07 14:13:20.605 iPadTests[3073:774881] Number 20 done after : 190.771543
2015-12-07 14:13:24.748 iPadTests[3073:774881] Number 21 done after : 194.914323
2015-12-07 14:13:28.992 iPadTests[3073:774881] Number 22 done after : 199.158079
2015-12-07 14:13:33.222 iPadTests[3073:774881] Number 23 done after : 203.387732
2015-12-07 14:13:36.400 iPadTests[3073:774881] Number 24 done after : 206.566002
2015-12-07 14:13:38.279 iPadTests[3073:774841] Number 25 done after : 208.44497

In Xamarin 3, we get a completed task every 6-9 seconds. That is what I desire. This the main reason why we programmed a flow with Dataflow. And it is even completed faster than Xamarin 4

With your new scheduling in Xamarin 4, the tasks are completed in batch after a long wait time (mainly the number 4 to number 12) and slower than Xamarin 3

I hope it is my clear why I think your new scheduling is problematic
Comment 8 Alexandre Pepin 2015-12-07 19:27:33 UTC
Created attachment 14156 [details]
Sample
Comment 9 Rodrigo Kumpera 2015-12-08 17:03:23 UTC
Yes, this looks bad.

Ludovic, can you take a look on why the new TP takes more time?
Comment 10 Alexandre Pepin 2015-12-08 20:47:32 UTC
Just to clarify, I am more concerned about the Task not running at the same time than the fact that it takes more time to run
Comment 11 Ludovic Henry 2015-12-15 14:19:30 UTC
As the behaviour you are expecting is not the default TaskScheduler documented behaviour, I would advise you to provide a custom TaskScheduler. To do that you can use the following:

public class LongRunningThreadScheduler : TaskScheduler
{
	protected override void QueueTask(Task task)
	{
		Thread thread = new Thread(obj => base.TryExecuteTask ((Task) obj));
		thread.IsBackground = true;
		thread.Start(task);
	}

	protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
	{
		return false;
	}

	protected override IEnumerable<Task> GetScheduledTasks()
	{
		return new Task [0];
	}
}

And you can simply pass it in the ExecutionDataflowBlockOptions with:

new ExecutionDataflowBlockOptions { TaskScheduler = new LongRunningThreadScheduler () };

The previous code will ensure that your task will run simultaneously, or at least on dedicated threads which are going to be scheduled by the OS directly. This solution will bypass entirely the ThreadPool, so you won't be disturbed by it's heuristic.