I'm writing a C# Blazor MAUI app and am trying to perform some automated background tasks. Every 30 seconds, my applications scans the configuration to perform a connection test on a set of user-defined servers.
public static bool CanConnect(string host, int port, TimeSpan timeout) {
try {
using (var client = new TcpClient()) {
var result = client.BeginConnect(host, port, null, null);
var success = result.AsyncWaitHandle.WaitOne(timeout);
client.EndConnect(result);
System.Diagnostics.Debug.Print($"Success! {host}:{port}");
return success;
}
} catch {
System.Diagnostics.Debug.Print($"Failed connecting to {host}:{port}");
return false;
}
}
When my scheduler begins queuing up these tasks, I notice a very slight (but noticeable) hiccup on the GUI.
clusters.AsParallel()
.ForAll(item => item.Status = ClusterIsAccessible(item) ? Cluster.ConnectionStatus.Online : Cluster.ConnectionStatus.Offline);
I believe the hiccup is a result of threads being created. I am noticing that when the jobs complete, the threads that are used to scan the connections will exit / timeout after 20-25s.
The thread 0x225c has exited with code 0 (0x0).
The thread 0x4d2c has exited with code 0 (0x0).
The thread 0x7c28 has exited with code 0 (0x0).
The thread 0x6724 has exited with code 0 (0x0).
The thread 0x822c has exited with code 0 (0x0).
The thread 0x849c has exited with code 0 (0x0).
The thread 0x5a24 has exited with code 0 (0x0).
The thread 0x86ac has exited with code 0 (0x0).
The thread 0x8840 has exited with code 0 (0x0).
The thread 0x22f8 has exited with code 0 (0x0).
The thread 0x74e0 has exited with code 0 (0x0).
The thread 0x7550 has exited with code 0 (0x0).
The thread 0x8b80 has exited with code 0 (0x0).
The thread 0x4d48 has exited with code 0 (0x0).
The thread 0x14a8 has exited with code 0 (0x0).
The thread 0x5ed0 has exited with code 0 (0x0).
My thought was that LINQ isn't using a thread pool but rather initializing new threads for each task.
To try and force the jobs onto the existing C# thread pool, I rewrote the iteration logic to use ThreadPool::QueueUserWorkItem(...)
but this also resulted in threads exiting and a slight hiccup when threads were being created.
clusters.ForEach((item) => ThreadPool.QueueUserWorkItem(state => {
item.Status = ClusterIsAccessible(item) ? Cluster.ConnectionStatus.Online : Cluster.ConnectionStatus.Offline;
}));
What am I doing wrong? I don't want to create unnecessary threads when there are already threads waiting for work.
CodePudding user response:
The problem is that the WaitOne
is blocking a threadpool thread, which means a new one will be created after a short wait. The threadpool will continue creating more and more threads just to satisfy your requests, because each one keeps getting blocked.
Instead, you should use async
await
, which allows the threadpool thread to go off and do other things until the response comes back
public static async Task<bool> CanConnect(string host, int port, TimeSpan timeout)
{
try
{
using (var cancel = new CancellationTokenSource(timeout))
using (var client = new TcpClient())
{
await client.ConnectAsync(host, port, cancel.Token);
System.Diagnostics.Debug.Print($"Success! {host}:{port}");
return true;
}
}
catch
{
System.Diagnostics.Debug.Print($"Failed connecting to {host}:{port}");
return false;
}
}
Then you call it not using Parallel.ForEach
but using Task.WaitAll
await Task.WhenAll(
clusters.Select(async item =>
item.Status = (await ClusterIsAccessible(item)) ? Cluster.ConnectionStatus.Online : Cluster.ConnectionStatus.Offline)
);
If you want to limit the number of tasks running at once, you can do this
var running = new List<Task>();
var completed = new List<Task>();
foreach (var cluster in clusters)
{
if (running.Count == YourMaxCount)
{
var completedTask = await Task.WhenAny(running);
running.Remove(completedTask);
completed.Add(completedTask);
}
running.Add(Task.Run(async item =>
item.Status = (await ClusterIsAccessible(item)) ? Cluster.ConnectionStatus.Online : Cluster.ConnectionStatus.Offline));
}
await Task.WhenAll(running);
completed.AddRange(running);