[英]Forcing certain code to always run on the same thread
We have an old 3rd party system (let's call it Junksoft® 95) that we interface with via PowerShell (it exposes a COM object) and I'm in the process of wrapping it in a REST API (ASP.NET Framework 4.8 and WebAPI 2)。 I use the System.Management.Automation
nuget package to create a PowerShell
in which I instantiate Junksoft's COM API as a dynamic
object that I then use:
//I'm omitting some exception handling and maintenance code for brevity
powerShell = System.Management.Automation.PowerShell.Create();
powerShell.AddScript("Add-Type -Path C:\Path\To\Junksoft\Scripting.dll");
powerShell.AddScript("New-Object Com.Junksoft.Scripting.ScriptingObject");
dynamic junksoftAPI = powerShell.Invoke()[0];
//Now we issue commands to junksoftAPI like this:
junksoftAPI.Login(user,pass);
int age = junksoftAPI.GetAgeByCustomerId(custId);
List<string> names = junksoftAPI.GetNames();
当我在同一个线程上运行所有这些(例如在控制台应用程序中)时,这工作正常。 但是,由于某种原因,当我将junksoftAPI
放入System.Web.Caching.Cache
并从我的 web 应用程序中的不同控制器使用它时,这通常不起作用。 我说通常是因为当 ASP.NET 碰巧将传入调用传递给创建junksoftAPI
的线程时,这实际上有效。 如果没有,Junksoft 95 会给我一个错误。
我有什么办法可以确保与junksoftAPI
的所有交互都发生在同一个线程上?
请注意,我不想将整个 web 应用程序变成单线程应用程序。 控制器和其他地方的逻辑应该像往常一样在不同的线程上发生,它应该只是在 Junksoft 特定线程上发生的 Junksoft 交互:像这样:
[HttpGet]
public IHttpActionResult GetAge(...)
{
//finding customer ID in database...
...
int custAge = await Task.Run(() => {
//this should happen on the Junksoft-specific thread and not the next available thread
var cache = new System.Web.Caching.Cache();
var junksoftAPI = cache.Get(...); //This has previously been added to cache on the Junksoft-specific thread
return junksoftAPI.GetAgeByCustomerId(custId);
});
//prepare a response using custAge...
}
您可以创建自己的 singleton 工作线程来实现此目的。 这是您可以将其插入 web 应用程序的代码。
public class JunkSoftRunner
{
private static JunkSoftRunner _instance;
//singleton pattern to restrict all the actions to be executed on a single thread only.
public static JunkSoftRunner Instance => _instance ?? (_instance = new JunkSoftRunner());
private readonly SemaphoreSlim _semaphore;
private readonly AutoResetEvent _newTaskRunSignal;
private TaskCompletionSource<object> _taskCompletionSource;
private Func<object> _func;
private JunkSoftRunner()
{
_semaphore = new SemaphoreSlim(1, 1);
_newTaskRunSignal = new AutoResetEvent(false);
var contextThread = new Thread(ThreadLooper)
{
Priority = ThreadPriority.Highest
};
contextThread.Start();
}
private void ThreadLooper()
{
while (true)
{
//wait till the next task signal is received.
_newTaskRunSignal.WaitOne();
//next task execution signal is received.
try
{
//try execute the task and get the result
var result = _func.Invoke();
//task executed successfully, set the result
_taskCompletionSource.SetResult(result);
}
catch (Exception ex)
{
//task execution threw an exception, set the exception and continue with the looper
_taskCompletionSource.SetException(ex);
}
}
}
public async Task<TResult> Run<TResult>(Func<TResult> func, CancellationToken cancellationToken = default(CancellationToken))
{
//allows only one thread to run at a time.
await _semaphore.WaitAsync(cancellationToken);
//thread has acquired the semaphore and entered
try
{
//create new task completion source to wait for func to get executed on the context thread
_taskCompletionSource = new TaskCompletionSource<object>();
//set the function to be executed by the context thread
_func = () => func();
//signal the waiting context thread that it is time to execute the task
_newTaskRunSignal.Set();
//wait and return the result till the task execution is finished on the context/looper thread.
return (TResult)await _taskCompletionSource.Task;
}
finally
{
//release the semaphore to allow other threads to acquire it.
_semaphore.Release();
}
}
}
用于测试的控制台主要方法:
public class Program
{
//testing the junk soft runner
public static void Main()
{
//get the singleton instance
var softRunner = JunkSoftRunner.Instance;
//simulate web request on different threads
for (var i = 0; i < 10; i++)
{
var taskIndex = i;
//launch a web request on a new thread.
Task.Run(async () =>
{
Console.WriteLine($"Task{taskIndex} (ThreadID:'{Thread.CurrentThread.ManagedThreadId})' Launched");
return await softRunner.Run(() =>
{
Console.WriteLine($"->Task{taskIndex} Completed On '{Thread.CurrentThread.ManagedThreadId}' thread.");
return taskIndex;
});
});
}
}
}
Output:
请注意,尽管 function 是从不同的线程启动的,但代码的某些部分始终始终在 ID 为“5”的同一上下文线程上执行。
但请注意,虽然所有 web 请求都在独立线程上执行,但它们最终会等待一些任务在 singleton 工作线程上执行。 这最终会在您的 web 应用程序中造成瓶颈。 无论如何,这是您的设计限制。
以下是如何使用BlockingCollection
class 从专用 STA 线程向 Junksoft API 发出命令的方法:
public class JunksoftSTA : IDisposable
{
private readonly BlockingCollection<Action<Lazy<dynamic>>> _pump;
private readonly Thread _thread;
public JunksoftSTA()
{
_pump = new BlockingCollection<Action<Lazy<dynamic>>>();
_thread = new Thread(() =>
{
var lazyApi = new Lazy<dynamic>(() =>
{
var powerShell = System.Management.Automation.PowerShell.Create();
powerShell.AddScript("Add-Type -Path C:\Path\To\Junksoft.dll");
powerShell.AddScript("New-Object Com.Junksoft.ScriptingObject");
dynamic junksoftAPI = powerShell.Invoke()[0];
return junksoftAPI;
});
foreach (var action in _pump.GetConsumingEnumerable())
{
action(lazyApi);
}
});
_thread.SetApartmentState(ApartmentState.STA);
_thread.IsBackground = true;
_thread.Start();
}
public Task<T> CallAsync<T>(Func<dynamic, T> function)
{
var tcs = new TaskCompletionSource<T>(
TaskCreationOptions.RunContinuationsAsynchronously);
_pump.Add(lazyApi =>
{
try
{
var result = function(lazyApi.Value);
tcs.SetResult(result);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
});
return tcs.Task;
}
public Task CallAsync(Action<dynamic> action)
{
return CallAsync<object>(api => { action(api); return null; });
}
public void Dispose() => _pump.CompleteAdding();
public void Join() => _thread.Join();
}
使用Lazy
class 的目的是通过将其传播给调用者,在动态 object 的构造过程中显示可能的异常。
...缓存异常。 也就是说,如果工厂方法在线程第一次尝试访问
Lazy<T>
object 的Value
属性时抛出异常,则每次后续尝试都会抛出相同的异常。
使用示例:
// A static field stored somewhere
public static readonly JunksoftSTA JunksoftStatic = new JunksoftSTA();
await JunksoftStatic.CallAsync(api => { api.Login("x", "y"); });
int age = await JunksoftStatic.CallAsync(api => api.GetAgeByCustomerId(custId));
如果您发现单个 STA 线程不足以及时处理所有请求,您可以添加更多 STA 线程,它们都运行相同的代码( private readonly Thread[] _threads;
等)。 BlockingCollection
class 是线程安全的,可以由任意数量的线程同时使用。
如果您没有说那是第 3 方工具,我会认为它是 GUI class。 出于实际原因,让多个线程写入它们是一个非常糟糕的主意。 从2.0 开始,.NET 强制执行严格的“只有创建线程才能写入”规则。
一般的 WebServers 和 ASP.Net 特别是使用一个相当大的线程池。 我们说的是每个核心有 10 到 100 个线程。 这意味着很难将任何请求确定到特定线程。 你还不如不试。
同样,查看 GUI 类可能是您最好的选择。 您基本上可以创建一个线程,其唯一目的是模仿 GUI 的事件队列。 您的平均 Windows Forms 应用程序的主/UI 线程负责创建每个 GUI class 实例。 它通过轮询/处理事件队列来保持活动状态。 它仅在通过事件队列收到取消命令时结束。 调度只是将订单放入该队列,因此我们可以避免跨线程问题。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.