强制某些代码始终在同一线程上运行

jbb*_*jbb 3 c# asp.net powershell multithreading asp.net-web-api2

我们有一个旧的第 3 方系统(我们称之为 Junksoft\xc2\xae 95),我们通过 PowerShell 与之交互(它公开了一个 COM 对象),我正在将其包装在 REST API 中( ASP.NET Framework 4.8 和 WebAPI 2)。我使用System.Management.Automationnuget 包创建一个实例,PowerShell在其中将 Junksoft\ 的 COM API 实例化为一个dynamic对象,然后使用:

\n\n
//I\'m omitting some exception handling and maintenance code for brevity\npowerShell = System.Management.Automation.PowerShell.Create();\npowerShell.AddScript("Add-Type -Path C:\\Path\\To\\Junksoft\\Scripting.dll");\npowerShell.AddScript("New-Object Com.Junksoft.Scripting.ScriptingObject");\ndynamic junksoftAPI = powerShell.Invoke()[0];\n\n//Now we issue commands to junksoftAPI like this:\njunksoftAPI.Login(user,pass);\nint age = junksoftAPI.GetAgeByCustomerId(custId);\nList<string> names = junksoftAPI.GetNames();\n
Run Code Online (Sandbox Code Playgroud)\n\n

当我在同一线程上运行所有这些时(例如在控制台应用程序中),这工作得很好。但是,由于某种原因,当我将其放入网络应用程序中的不同控制器并使用它时,这通常不起作用。我说“通常”是因为当 ASP.NET 碰巧向创建的线程发出传入调用时,这实际上会起作用。如果没有,Junksoft 95 会给我一个错误。junksoftAPISystem.Web.Caching.CachejunksoftAPI

\n\n

我有什么方法可以确保所有交互都junksoftAPI发生在同一个线程上吗?

\n\n

请注意,我不想将整个 Web 应用程序变成单线程应用程序!控制器和其他地方的逻辑应该像正常情况一样在不同的线程上发生。它应该只是在 Junksoft 特定线程上发生的 Junksoft 交互,如下所示:

\n\n
[HttpGet]\npublic IHttpActionResult GetAge(...)\n{\n    //finding customer ID in database...\n\n    ...\n\n    int custAge = await Task.Run(() => {\n        //this should happen on the Junksoft-specific thread and not the next available thread\n        var cache = new System.Web.Caching.Cache();\n        var junksoftAPI = cache.Get(...); //This has previously been added to cache on the Junksoft-specific thread\n        return junksoftAPI.GetAgeByCustomerId(custId);\n    });\n\n    //prepare a response using custAge...\n}\n
Run Code Online (Sandbox Code Playgroud)\n

zaf*_*far 7

您可以创建自己的单例工作线程来实现此目的。以下是您可以将其插入到您的 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

控制台主要测试方法:

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;
                });
            });
        }
    }   
}
Run Code Online (Sandbox Code Playgroud)

输出:

在此输入图像描述

请注意,虽然该函数是从不同的线程启动的,但代码的某些部分始终在 ID 为“5”的同一上下文线程上执行。

但请注意,尽管所有 Web 请求都在独立线程上执行,但它们最终将等待某些任务在单例工作线程上执行。这最终会在您的 Web 应用程序中造成瓶颈。无论如何,这都是你的设计限制。