COM-Ausnahmen durch falsche Thread-Apartments vermeiden
Das Automation Interface-COM-Objekt muss im STA ausgeführt werden. STA steht für Single-Threaded-Apartment. Der Grund liegt im COM-Threading-Modell. STA-registrierte Objekte erwarten, dass alle Aufrufe über denselben Thread serialisiert werden, der das Objekt erzeugt hat. Code, der in MTA-Threads läuft, zum Beispiel BackgroundWorker, ThreadPool oder Task.Run, bricht diese Zusage und verursacht RPC_E_SERVERCALL_RETRYLATER-Ausnahmen, weil der COM-Server den Aufruf aus dem fremden Apartment nicht direkt bedienen kann. Dies ist kein Defekt des Automation Interface, sondern eine grundlegende Eigenschaft von STA-COM-Objekten. Die Lösung besteht darin, konsequent einen eigenen STA-Thread bereitzustellen.
Das Problem
// WRONG — BackgroundWorker uses thread-pool threads (MTA)
var worker = new BackgroundWorker();
worker.DoWork += (sender, e) =>
{
// RPC_E_SERVERCALL_RETRYLATER: COM object is STA, thread is MTA
sysManager.LookupTreeItem("TIPC^PLC1"); // Error!
};
worker.RunWorkerAsync();
Gleiches gilt für Task.Run(() => ...) und ThreadPool.QueueUserWorkItem(...), da alle MTA-Pool-Threads nutzen.
Lösung: Explizit STA-Thread erzeugen
public static class AsyncFactory
{
private static StaTaskScheduler _sta = new StaTaskScheduler(numberOfThreads: 1);
public static async Task StartNew(Action action, bool configureAwait = false)
{
try
{
await Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, _sta).ConfigureAwait(configureAwait);
}
catch (Exception ex)
{
throw ex;
}
}
public static async Task<TResult> StartNew<TResult>(Func<TResult> function, bool configureAwait = false)
{
try
{
return await Task.Factory.StartNew(function, CancellationToken.None, TaskCreationOptions.None, _sta).ConfigureAwait(configureAwait);
}
catch (Exception ex)
{
throw ex;
}
}
}
Diese AsyncFactory kann nun genutzt werden, um Tasks zu starten, die nebenläufig laufen und den Main-Thread nicht blockieren. Entscheidend ist, dass alle COM-Objekte innerhalb der über die Factory eingereihten Aktionen erzeugt und verwendet werden. So ist garantiert, dass sie auf dem STA-Thread des Schedulers leben und auch von dort aus angesprochen werden. Das folgende Beispiel zeigt einen klassischen Anwendungsablauf, bei dem nacheinander Aufgaben an die AsyncFactory übergeben werden: DTE-Instanz erzeugen, Solution mit TwinCAT-Projekt anlegen, ITcSysManager abfragen, eine länger laufende COM-Operation ausführen und am Schluss aufräumen.
await RunTwinCATAsync();
async Task RunTwinCATAsync()
{
Console.WriteLine($"Main thread: {Environment.CurrentManagedThreadId}");
// Step 1 - Create a new DTE instance on the STA thread.
// The MessageFilter is registered to handle RPC_E_CALL_REJECTED errors.
DTE dte = await AsyncFactory.StartNew(() =>
{
MessageFilter.Register();
Console.WriteLine($"STA thread: {Environment.CurrentManagedThreadId}");
Type t = Type.GetTypeFromProgID("TcXaeShell.DTE.17.0");
DTE instance = (DTE)Activator.CreateInstance(t);
instance.SuppressUI = false;
instance.MainWindow.Visible = true;
return instance;
});
Console.WriteLine("DTE instance created. Creating solution...");
// Step 2 - Create a new solution and add a TwinCAT project from a template.
await AsyncFactory.StartNew(() =>
{
string solutionDir = @"C:\tmp\Test\AsyncSolution";
string solutionName = "AsyncSolution";
Solution sol = dte.Solution;
sol.Create(solutionDir, solutionName);
string tcTemplate = @"C:\Program Files (x86)\Beckhoff\TwinCAT\3.1\Components\Base\PrjTemplate\TwinCAT Project.tsproj";
sol.AddFromTemplate(tcTemplate,
Path.Combine(solutionDir, "TwinCATProject1"),
"TwinCATProject1");
sol.SaveAs(Path.Combine(solutionDir, $"{solutionName}.sln"));
Console.WriteLine("Solution saved.");
});
// Step 3 - Access the TwinCAT System Manager and query the PLC tree item.
string plcPath = await AsyncFactory.StartNew(() =>
{
ITcSysManager sysManager = dte.Solution.Projects.Item(1).Object as ITcSysManager;
ITcSmTreeItem plc = sysManager.LookupTreeItem("TIPC");
return plc.PathName;
});
Console.WriteLine($"PLC path: {plcPath}");
// Step 4 - Run a long-running COM operation while doing other work concurrently.
var comTask = AsyncFactory.StartNew(() =>
{
// Simulate a time-consuming operation (e.g. code generation)
System.Threading.Thread.Sleep(2000);
return dte.Solution.FullName;
});
Console.WriteLine("Performing other work while COM operation is in progress...");
await Task.Delay(500);
Console.WriteLine("Other work completed.");
string fullName = await comTask;
Console.WriteLine($"Solution: {fullName}");
// Step 5 - Clean up: close the DTE instance and revoke the message filter.
await AsyncFactory.StartNew(() =>
{
dte.Quit();
MessageFilter.Revoke();
});
Console.WriteLine("Done.");
}
Hintergrund
TwinCAT-COM-Objekte wie ITcSysManager, ITcSmTreeItem usw. sind STA-registriert. Alle Aufrufe müssen vom selben STA-Thread kommen, in dem das Objekt erzeugt wurde. BackgroundWorker und Task.Run verwenden Thread-Pool-Threads, die standardmäßig MTA sind.
![]() | Der Message Filter ( |
