C
C#2mo ago
maxmahem

Call from Invalid Thread possibly related to Avalonia

Getting a "Call from Invalid Thread" exception when running this chunk of code from an Avalonia context.
public async Task StartAsync(CancellationToken? token = null) {
this.tokenSource = CancellationTokenSource.CreateLinkedTokenSource(token ?? new CancellationToken());
var internalToken = tokenSource.Token;
this.listener.Start();
OnMessage(this.listener.LocalEndpoint, $"{Name} Service Started...");

try {
while (!internalToken.IsCancellationRequested) {
await Task.Run(async () => {
var tcpClient = await this.listener.AcceptTcpClientAsync(internalToken);
OnMessage(tcpClient.Client.RemoteEndPoint, "Client connected.");
await HandleClientAsync(tcpClient, internalToken);
}, internalToken);
}
}
catch (Exception exception) {
OnMessage(this.listener.LocalEndpoint, $"Exception: {exception.Message}");
}
finally {
this.listener.Stop();
}
}
public async Task StartAsync(CancellationToken? token = null) {
this.tokenSource = CancellationTokenSource.CreateLinkedTokenSource(token ?? new CancellationToken());
var internalToken = tokenSource.Token;
this.listener.Start();
OnMessage(this.listener.LocalEndpoint, $"{Name} Service Started...");

try {
while (!internalToken.IsCancellationRequested) {
await Task.Run(async () => {
var tcpClient = await this.listener.AcceptTcpClientAsync(internalToken);
OnMessage(tcpClient.Client.RemoteEndPoint, "Client connected.");
await HandleClientAsync(tcpClient, internalToken);
}, internalToken);
}
}
catch (Exception exception) {
OnMessage(this.listener.LocalEndpoint, $"Exception: {exception.Message}");
}
finally {
this.listener.Stop();
}
}
(excecption happens at var tcpClient = await this.listener.AcceptTcpClientAsync(internalToken);). Doesn't happen when called from a CLI context.
3 Replies
maxmahem
maxmahem2mo ago
Okay wrapping my event callback in a Dispatcher fixed it. Hmm...
Becquerel
Becquerel2mo ago
in general with Avalonia and similar UI frameworks (such as WPF), you aren't allowed to touch the UI thread in any way from any other thread without first marshalling it back to that thread via a specific API -- which in avalonia's case is the Dispatcher, as you found i don't know where in your callstack here you're touching the UI thread's stuff, so I might be wrong. but it's the first explanation that comes to my mind this is something to keep in mind whenever you use Task.Run etc. in a UI project
SleepWellPupper
SleepWellPupper2mo ago
Couple of things: Concerning your use of Task.Run:
Queues the specified work to run on the thread pool and returns a Task object that represents that work. A cancellation token allows the work to be cancelled if it has not yet started.
Your internalToken will only be checked before the passed delegate starts execution. Also, if you were to move
var tcpClient = await this.listener.AcceptTcpClientAsync(internalToken);
OnMessage(tcpClient.Client.RemoteEndPoint, "Client connected.");
await HandleClientAsync(tcpClient, internalToken);
var tcpClient = await this.listener.AcceptTcpClientAsync(internalToken);
OnMessage(tcpClient.Client.RemoteEndPoint, "Client connected.");
await HandleClientAsync(tcpClient, internalToken);
out of the Task.Run, you could call .ConfigureAwait(continueOnCapturedContext: true) on the tasks returned by AcceptTcpClientAsync(internalToken) and HandleClientAsync(tcpClient, internalToken). This causes execution to continue on the ui thread after asynchronously returning from either operation. All of this of course only makes sense if StartAsync is being called from the UI-thread in the first place. As @Becquerel (ping on reply please) points out, you may only perform certain actions on the UI-thread, and Dispatcher marshals the passed action to the UI-thread. The reason you are not on the UI-thread at await this.listener.AcceptTcpClientAsync(internalToken) is because Task.Run schedules the delegate passed on the thread pool.