We are currently evaluating some slowish performance on an application, and found a strange behaviour.
While the code runs perfectly fine on Windows 7 / Server 2008 R2 / Server 2012 R2, starting with Server 2019 / Windows 10/11, we can notice a strange stuck on the Main-Thread.
All Systems using .net Framework 4.8:
The main-thread is iterating over a list of tasks, where foreach task a thread is started, hardly simplified like this:
foreach (CustomTaskObject task in this.Tasks){
task.ThreadObject.start();
}
Each Task-Object wraps basically either a cmd-script or a powershell script, so the tasks objective is to run that script, transscript the output, wait for finish and log.
The ThreadObject is basically created like this:
public class CustomTaskObject
public Thread ThreadObject;
public CustomTaskObject(){
this.ThreadObject = new Thread(new ThreadStart(new Action(() => {
Process p = new Process();
p.StartInfo.FileName = "powershell.exe"; //cmd.exe /C if a batch-script.
p.StartInfo.Arguments = "-ExecutionPolicy Bypass -f " + this.Folder + "\\" + this.File;
p.Start();
p.WaitForExit();
}));
}
All the stuff for redirecting output / reading / logging has been removed, it has no impact, also it doesn't matter if we use ShellExecute True or false.
What happens now is:
For cmd-calls, everything runs nice and smooth. If Tasks are defined to be executable asynchronous, the main thread can create 50 threads in no time, Main-Thread stays active / responsive.
For powershell-calls: As soon as
p.Start()
is called, the THREAD is stucking about 2-3 Seconds, if the file is located on a network share. This is no big surprise, dns resolution, smb paket stuff, building connections - takes some time, all good.
However, what is really strange: The Main-Thread is stuck at calling task.ThreadObject.start();
as well - Until the Process in the thread is started. This makes the main thread stuck while iterating through the job-objects, and you can see that one is started about every 3 seconds.
For testing purpose we added a Thread.Sleep(1000)
before p.Start()
- and guess what? The mainthread was able to kick off 30 these threads, before one of them was causing a stuck...
public class CustomTaskObject
public Thread ThreadObject;
public CustomTaskObject(){
this.ThreadObject = new Thread(new ThreadStart(new Action(() => {
Process p = new Process();
p.StartInfo.FileName = "powershell.exe"; //cmd.exe if a ps-script.
p.StartInfo.Arguments = "-ExecutionPolicy Bypass -f " + this.Folder + "\\" + this.File;
System.Threading.Thread.Sleep(1000);
p.Start();
p.WaitForExit();
}));
}
So, what is happening here? I can only assume, that calling process.Start() is in someway offloaded to the Main-Thread, even if started from within a thread? Hence, If we delay that by 1000ms, the main thread can finish iteration first, then get stuck (invisible) to start processes.
- Why does this only happen for the mentioned operating systems?
- Why does it run flawless for
cmd.exe
targeting network files in the very same way?
I can even reproduce this with the following console-Application:
From the output, you can see, where the first time p.Start()
is reached - leading to stucks of about 1 second here, summing up, as more p.starts()
are reached by their threads.
class Program
{
static Thread[] threads = new Thread[30];
static void Main(string[] args)
{
for (int i = 0; i < 30; i++)
{
threads[i] = new Thread(new ThreadStart(new Action(() =>
{
Process p = new Process();
p.StartInfo.FileName = "powershell.exe";
p.StartInfo.Arguments = @"-ExecutionPolicy Bypass -f \\server\share\ping.ps1";
p.Start();
p.WaitForExit();
})));
}
for (int i = 0; i < 30; i++)
{
Console.WriteLine(DateTime.Now.ToLongTimeString() + " Starting thread " + i);
threads[i].Start();
Console.WriteLine(DateTime.Now.ToLongTimeString() + " Thread started " + i);
}
Console.ReadKey();
}
}
Output:
17:30:20 Starting thread 0
17:30:20 Thread started 0
17:30:20 Starting thread 1
17:30:20 Thread started 1
17:30:20 Starting thread 2
17:30:20 Thread started 2
17:30:20 Starting thread 3
17:30:20 Thread started 3
17:30:20 Starting thread 4
17:30:20 Thread started 4
17:30:20 Starting thread 5
17:30:20 Thread started 5
17:30:20 Starting thread 6
17:30:20 Thread started 6
17:30:20 Starting thread 7
17:30:20 Thread started 7
17:30:20 Starting thread 8
17:30:20 Thread started 8
17:30:20 Starting thread 9
17:30:20 Thread started 9
17:30:20 Starting thread 10
17:30:20 Thread started 10
17:30:20 Starting thread 11
17:30:25 Thread started 11
17:30:25 Starting thread 12
17:30:29 Thread started 12
17:30:29 Starting thread 13
17:30:31 Thread started 13
17:30:31 Starting thread 14
17:30:33 Thread started 14
17:30:33 Starting thread 15
17:30:33 Thread started 15
17:30:33 Starting thread 16
17:30:36 Thread started 16
17:30:36 Starting thread 17
17:30:37 Thread started 17
17:30:37 Starting thread 18
17:30:40 Thread started 18
17:30:40 Starting thread 19
17:30:41 Thread started 19
17:30:41 Starting thread 20
17:30:42 Thread started 20
17:30:42 Starting thread 21
17:30:44 Thread started 21
17:30:44 Starting thread 22
17:30:45 Thread started 22
17:30:45 Starting thread 23
17:30:46 Thread started 23
17:30:46 Starting thread 24
17:30:47 Thread started 24
17:30:47 Starting thread 25
17:30:48 Thread started 25
17:30:48 Starting thread 26
17:30:49 Thread started 26
17:30:49 Starting thread 27
17:30:50 Thread started 27
17:30:50 Starting thread 28
17:30:52 Thread started 28
17:30:52 Starting thread 29
17:30:53 Thread started 29
Sure, 30 threads cannot be executed at the same time, unless you have a 30 core cpu - but how can it "stuck" the Main-Thread on Windows Server 2019 / Windows 10 & 11?
Updated the test-script to access a network share. After 11 threads, the first p.start()
is actually reached, leading to significant stucks on the main-thread.
Running on a 20 core, I can assume the main-thread didnt get suspended. It seems the whole stuck is exactly in place, until powershell has resolved and loaded that file. Then the main thread continues, while the script is running in its process as it is supposed to be.
Funny observation: While the Main-Thread is stucking, even Visual Studios profiler is stucking... (But it also happens when running the app native, without debugger attached)