I have a C# application where I spawn multiple threads. I'm on .NET framework 4.7.1. Inside these threads, work is performed and this work may execute user-defined scripted functions. I am using ClearScript as the scripting engine and for purposes of this question I am using the VBScriptEngine. Here is an sample application demonstrating my problem:
static VBScriptEngine vbsengine = new VBScriptEngine();
static void Main(string[] args)
{
for (int i=0;i<4000;i++)
{
Thread t = new Thread(Program.ThreadedFunc);
t.Start(i);
}
Console.ReadKey();
}
static void ThreadedFunc(object i)
{
Console.WriteLine(i + ": " + vbsengine.Evaluate("1+1"));
}
On the _Evaluate()_ function I get the following error: System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'
I understand ClearScript has implemented thread affinity and a spawned thread cannot access the globally defined engine. So what is my alternative? Create a new instance of ClearScript for each new thread? This seems incredibly wasteful and would create a lot of overhead - my application will need to process thousands of threads. I went ahead and tried this approach anyways - and while it does work (for a while) - end up getting an error. Here's a revised version of my sample app:
static void Main(string[] args)
{
for (int i=0;i<4000;i++)
{
Thread t = new Thread(Program.ThreadedFunc);
t.Start(i);
}
Console.ReadKey();
}
static void ThreadedFunc(object i)
{
using (VBScriptEngine vbsengine = new VBScriptEngine())
{
Console.WriteLine(i + ": " + vbsengine.Evaluate("1+1"));
}
}
On the _new VBScriptEngine()_ call, I now start getting: System.ComponentModel.Win32Exception: 'Not enough storage is available to process this command'.
I'm not really sure what's causing that message as the application isn't taking up a lot of RAM.
I realize this sample application is starting all the threads at once but my full application ensures only 4 threads are running and I still end up getting this message after a while. I don't know why, but I can't get rid of this message either - even after restarting the app and Visual Studio. A little clarity on what's causing this message would be useful.
So my question is - if I only need, say 4 threads, running at once - is there a way I can just create 4 instances of the VBScriptEngine and reuse it for each new thread call? Or even just 1 instance of the VBScriptEngine on the main thread and each new thread just shares it?
Hello @ahoekstra,
It's difficult to diagnose your issue without more information, so we'll make a couple of general comments and provide a sample that demonstrates concurrent VBScriptEngine usage.
First, creating thousands of threads is almost always counterproductive. You reserve gigabytes of address space just for stacks, and since most machines don't have thousands of cores, you're likely to get thrashing and resource exhaustion. Thread pools optimize efficiency by carefully controlling thread usage.
Unfortunately Windows script engines (JScript/VBScript) have thread affinity, and that makes them fundamentally incompatible with thread pools, as each instance can only run script code on the thread that constructed it.
The Dispatcher class provides a convenient way to deal with thread-bound objects in a thread-pooled environment. Here's a quick sample.
First, create an array of script engines with dedicated threads:
``` C#
var engines = new VBScriptEngine[Environment.ProcessorCount];
var checkPoint = new ManualResetEventSlim();
for (var index = 0; index < engines.Length; ++index) {
var thread = new Thread(indexArg => {
using (var engine = new VBScriptEngine()) {
engines[(int)indexArg] = engine;
checkPoint.Set();
Dispatcher.Run();
}
});
thread.Start(index);
checkPoint.Wait();
checkPoint.Reset();
}
Now you can use `Dispatcher` to invoke the script engines from the thread pool:
``` C#
var items = Enumerable.Range(0, 10000);
Parallel.ForEach(items, item => {
var engine = engines[item % engines.Length];
engine.Dispatcher.Invoke(() => {
var expr = string.Format("{0} * 2", item);
Console.WriteLine(engine.Evaluate(expr));
});
});
This sample uses simple round-robin engine selection and synchronous dispatching, but more sophisticated approaches are certainly possible.
Finally, to terminate the threads and dispose the script engines:
C#
Array.ForEach(engines, engine => engine.Dispatcher.InvokeShutdown());
Good luck!
Thank you! This is a great response and appears to have addressed my question perfectly. Now I get to try and implement this solution into my application! Was time to update to _Parallel_ threading anyways..
Most helpful comment
Hello @ahoekstra,
It's difficult to diagnose your issue without more information, so we'll make a couple of general comments and provide a sample that demonstrates concurrent
VBScriptEngineusage.First, creating thousands of threads is almost always counterproductive. You reserve gigabytes of address space just for stacks, and since most machines don't have thousands of cores, you're likely to get thrashing and resource exhaustion. Thread pools optimize efficiency by carefully controlling thread usage.
Unfortunately Windows script engines (JScript/VBScript) have thread affinity, and that makes them fundamentally incompatible with thread pools, as each instance can only run script code on the thread that constructed it.
The
Dispatcherclass provides a convenient way to deal with thread-bound objects in a thread-pooled environment. Here's a quick sample.First, create an array of script engines with dedicated threads:
``` C#
var engines = new VBScriptEngine[Environment.ProcessorCount];
var checkPoint = new ManualResetEventSlim();
for (var index = 0; index < engines.Length; ++index) {
var thread = new Thread(indexArg => {
using (var engine = new VBScriptEngine()) {
engines[(int)indexArg] = engine;
checkPoint.Set();
Dispatcher.Run();
}
});
thread.Start(index);
checkPoint.Wait();
checkPoint.Reset();
}
This sample uses simple round-robin engine selection and synchronous dispatching, but more sophisticated approaches are certainly possible.
Finally, to terminate the threads and dispose the script engines:
C# Array.ForEach(engines, engine => engine.Dispatcher.InvokeShutdown());Good luck!