【问题标题】:Running a STA (Single-Threaded Apartment) thread from a Web API (2.1) controller从 Web API (2.1) 控制器运行 STA(单线程单元)线程
【发布时间】:2018-08-11 04:23:55
【问题描述】:

我正在尝试从 Web API (2.1) 控制器运行 STA(单线程单元)线程。

为此,我使用StaTaskScheduler

/// <summary>Provides a scheduler that uses STA threads.</summary>
public sealed class StaTaskScheduler : TaskScheduler, IDisposable
{
    /// <summary>Stores the queued tasks to be executed by our pool of STA threads.</summary>
    private BlockingCollection<Task> _tasks;
    /// <summary>The STA threads used by the scheduler.</summary>
    private readonly List<Thread> _threads;

    /// <summary>Initializes a new instance of the StaTaskScheduler class with the specified concurrency level.</summary>
    /// <param name="numberOfThreads">The number of threads that should be created and used by this scheduler.</param>
    public StaTaskScheduler(int numberOfThreads)
    {
        // Validate arguments
        if (numberOfThreads < 1) throw new ArgumentOutOfRangeException("concurrencyLevel");

        // Initialize the tasks collection
        _tasks = new BlockingCollection<Task>();

        // Create the threads to be used by this scheduler
        _threads = Enumerable.Range(0, numberOfThreads).Select(i =>
        {
            var thread = new Thread(() =>
            {
                // Continually get the next task and try to execute it.
                // This will continue until the scheduler is disposed and no more tasks remain.
                foreach (var t in _tasks.GetConsumingEnumerable())
                {
                    TryExecuteTask(t);
                }
            });
            thread.IsBackground = true;
            thread.SetApartmentState(ApartmentState.STA);
            return thread;
        }).ToList();

        // Start all of the threads
        _threads.ForEach(t => t.Start());
    }
}

一种选择是从CustomHttpControllerDispatcher 运行 STA 线程:

public static void Register(HttpConfiguration config)
{
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }      
        //constraints: null,        
        // - Trying to avoid this
        //handler: new Controllers.CustomHttpControllerDispatcher(config)
    );
}

public class CustomHttpControllerDispatcher : System.Web.Http.Dispatcher.HttpControllerDispatcher
{
    public CustomHttpControllerDispatcher(HttpConfiguration configuration) : base(configuration)
    {
    }
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // My stuff here
    }
}

但是,这需要手动构建 http 响应,这涉及到 json 序列化,这是我宁愿避免的。

这让我有了当前的选择,即从控制器调用的库中运行 STA 线程(以生成自定义表单)。

如果使用带有Main 装饰有[STAThread] 的“Windows 应用程序”类型的测试应用程序调用该库,则一切正常。但是,当从 Web 服务控制器调用时,当以下任务返回时会出现“吞咽”异常:

注意:我没有渲染任何对话框,所以不应该有任何 “当应用程序未在 UserInteractive 模式下运行时显示模式对话框或表单不是一个有效的操作。” 异常,在从 CustomHttpControllerDispatcher 中启动的 STA 线程生成自定义表单时根本不会发生。

private Task<AClass> SendAsync1()
{
    var staTaskScheduler = new StaTaskScheduler(1);

    return Task.Factory.StartNew<CustomForm>(() =>
        {
            // - execution causes "swallowed exception"
            return new AClass();
        }, 
        CancellationToken.None,
        TaskCreationOptions.None,
        staTaskScheduler
    );
}

即单步调试时,单步调试后堆栈跟踪消失:

return new AClass(); 

我通常通过缩小一些断点来解决这个问题,但对于这种情况,我认为这是不可能的,因为 Task.cs(以及大量相关文件)没有调试符号或源代码。

注意:我现在可以逐步完成 System.Threading.Tasks 的反汇编,但是调试过程可能会很长,因为这些库不是微不足道的,因此非常感谢任何见解。

我怀疑可能是 b/c 我正在尝试从 MTA 线程(控制器)调度 STA 线程,但不是肯定的?


用法

GetCustomForm().Wait();


private FixedDocumentSequence _sequence;

private async Task GetCustomForm()
{
    // - will be slapped with a "calling thread cannot access...", next issue
    _sequence = await SendAsync1b();
}


private readonly StaTaskScheduler _staTaskScheduler = new StaTaskScheduler(1);

private Task<FixedDocumentSequence> SendAsync1b()
{
    //var staTaskScheduler = new StaTaskScheduler(100);
    //var staTaskScheduler = new StaTaskScheduler(1);

    return Task.Factory.StartNew<FixedDocumentSequence>(() =>
    {
        FixedDocumentSequence sequence;
        CustomForm view = new CustomForm();

        view.ViewModel = new ComplaintCustomFormViewModel(BuildingEntity.form_id, (int)Record);     
        sequence = view.ViewModel.XpsDocument.GetFixedDocumentSequence();

        return sequence;
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    _staTaskScheduler);
}

参考文献

【问题讨论】:

  • 只是好奇:Web API 如何与 STA 线程相关联。我记得公寓问题与 COM 相关(实际上从未真正理解它),这个主题是如何在“现代”网络 API 中出现的?
  • @GianPaolo 我添加了一些对帖子的引用...我正在尝试实例化一个窗口 CustomForm : Window,以便我可以创建 XPS(和 PDF)从中。但是,我不渲染它与view.ShowDialog() 一起显示,只是膨胀它。
  • 所以,如果我没看错的话,你想通过一个 asp.net 应用程序硬塞一个 winforms 应用程序来做一些与 winforms 无关的事情吗?这似乎是一个非常错误的行动方案来满足要求。
  • @ErikPhilips 更新了使用示例的帖子...我正在尝试尽可能多地利用现有功能,它使用实体和 Windows 窗体来生成可以转换为 xps 的视图,然后最终一个pdf。这将在现场和办公室提供相同的打印输出。从头开始编写报告生成工具将是一项艰巨的任务。你知道有什么工具可以获取 windows 表单 xaml、绑定数据并将它们转换为 pdf(我当然不想重新发明这个轮子,但如果我必须这样做,尽管时间很重要)?
  • 老实说,我认为最好花时间编写 PDF 并将其重新集成到 winforms/wpf 中,而不是相反。而且我在 C# 中从头开始创建报告具有丰富的经验(同时保持它们与 Full .Net 和 CE .Net 很久以前的交叉兼容......)

标签: c# multithreading asp.net-web-api


【解决方案1】:

直接在 STA 线程中运行 WPF/Entity 代码似乎可以正常工作:

Thread thread = GetCustomFormPreviewView4();
thread.Start();
thread.Join();


private Thread GetCustomFormPreviewView4()
{
    var thread = new Thread(() =>
        {
            FixedDocumentSequence sequence;
            CustomForm view = new CustomForm();

            view.ViewModel = new ComplaintCustomFormViewModel(BuildingEntity.form_id, (int)Record);     
            sequence = view.ViewModel.XpsDocument.GetFixedDocumentSequence();

            //view.ShowDialog();
            //...
        }
    );

    thread.IsBackground = true;
    thread.SetApartmentState(ApartmentState.STA);

    return thread;
}

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多