程序可以作为服务或表单运行 - 主线程应该是MTA还是STA?

时间:2014-11-21 14:12:38

标签: c# winforms

我们有一个可以作为服务或winforms应用程序运行的程序。我们根据传入的命令行参数执行不同的行为。

如果我们作为表单运行,我想我们希望我们的入口点是STAThread。

CA2232:使用STAThread标记Windows窗体入口点

http://msdn.microsoft.com/query/dev12.query?appId=Dev12IDEF1&l=EN-US&k=k%28CA2232%29;k%28TargetFrameworkMoniker-.NETFramework

但是如果我们作为服务运行,我们希望我们的入口点是MTAThread吗?人们通常如何处理这个问题?

我们发现了一些崩溃转储(作为服务运行时),我们似乎遇到了卡住的终结器。

如果主入口点未标记为STAThread,我们不会遇到此问题。

  

线程2:
             IP
  00:U 00000000779312fa ntdll!NtWaitForSingleObject + 0xa
  01:U 000007fefd6d10dc KERNELBASE!WaitForSingleObjectEx + 0x79
  02:U 000007fefdd1e68e ole32! GetToSTA + 0x8a
  03:U 000007fefde53700 ole32!CRpcChannelBuffer :: SwitchAptAndDispatchCall + 0x13b
  04:U 000007fefde5265b ole32!CRpcChannelBuffer :: SendReceive2 + 0x11b
  05:U 000007fefdd0daaa ole32!CAptRpcChnl :: SendReceive + 0x52
  06:U 000007fefdd1cbe6 ole32!CCtxComChnl :: SendReceive + 0x15c
  07:U 000007fefde5205d ole32!NdrExtpProxySendReceive + 0x45
  08:U 000007fefdb3b949 rpcrt4!NdrpClientCall3 + 0x2e2
  09:U 000007fefde521d0 ole32!ObjectStublessClient + 0x11d
  0a:U 000007fefdd0d8a2 ole32!ObjectStubless + 0x42
  0b:U 000007fefdd2ea07 ole32!CObjectContext :: InternalContextCallback + 0x31537
  0c:U 000007fefdd349d1 ole32!CObjectContext :: ContextCallback + 0x81
  0d:U 000007fef4e439b6 clr!CtxEntry :: EnterContext + 0x232
  0e:U 000007fef4e4383c clr!RCW :: EnterContext + 0x3d
  0f:U 000007fef4e437e6 clr! ?? :: FNODOBFM :: string'+0x8c449
10:U 000007fef4e437a9 clr! ?? ::FNODOBFM::
字符串'+ 0x8b99d
  11:U 000007fef4ed326e clr!SyncBlockCache :: CleanupSyncBlocks + 0xc2
  12:U 000007fef4ed319f clr!Thread :: DoExtraWorkForFinalizer + 0xdc
  13:U 000007fef4dfab47 clr!WKS :: GCHeap :: FinalizerThreadWorker + 0x109
  14:U 000007fef4d4458c clr!Frame :: Pop + 0x50
  15:U 000007fef4d4451a clr!COMCustomAttribute :: PopSecurityContextFrame + 0x192
  16:U 000007fef4d44491 clr!COMCustomAttribute :: PopSecurityContextFrame + 0xbd
  17:U 000007fef4e21bfe clr!ManagedThreadBase_NoADTransition + 0x3f
  18:U 000007fef4e21d90 clr!WKS :: GCHeap :: FinalizerThreadStart + 0xb4
  19:U 000007fef4da33de clr!Thread :: intermediateThreadProc + 0x7d
  1a:U 00000000777d59ed kernel32!BaseThreadInitThunk + 0xd
  1b:U 000000007790c541 ntdll!RtlUserThreadStart + 0x1d

4 个答案:

答案 0 :(得分:5)

这是标准的终结器线程死锁。总是代码中的错误,很容易在服务或控制台模式应用程序中犯这样的错误。之所以发生这种情况,是因为您在代码中使用了单线程COM对象。很常见,绝大多数COM类都像绝大多数.NET类,而且根本不是线程安全的。由于COM的不同,它会自动处理线程安全要求。

是的,您调用COM服务器方法的线程的单元类型是一个非常重要的细节。当您选择MTA然后将它留给COM以保持对象线程安全。当你选择STA然后你做一个承诺,你的线程表现良好,并且需要这样一个线程不安全的对象。这样的承诺很容易在GUI应用程序中实现。

履行承诺的两个基本要求。您必须永远不会阻塞该线程,等待某种同步对象发出信号。并且您必须在.NET程序中引入消息循环Application.Run()。消息循环需要从工作线程调用以在拥有COM对象的线程上运行,从而保持线程安全。永不阻塞的要求是确保这样的调用可以进行而不是死锁,因为STA线程被阻塞而不是泵送。

打破这些承诺会让你遇到麻烦,代码会陷入僵局。就像终结器一样,它试图释放对象,以线程安全的方式调用IUnknown.Release()。但是拥有COM对象的线程是紧张性的。要么是因为它从未调用过Application.Run(),要么是因为它被阻止了,我们无法告诉它。

你可以让线程加入MTA,但这并不总是可行的,并且它具有非常严重的性能后果。当你这样做时,COM运行时被强制给对象一个安全的家,并为它创建一个线程。这很容易,但有两个基本问题。对COM对象的每次调用都将被封送,这可能非常昂贵。在简单的属性getter调用上,x10000的速度要慢得多。并且COM组件的作者必须提供帮助,他需要提供代理和存根实现。将方法调用的参数从调用者线程复制到所有者线程并将结果复制回来所需的额外代码。在.NET中很容易做到,多亏了反射,在COM中并不那么容易。作者通常不会意识到需求并且会跳过该要求。您现在强制使用STA。

从问题中忽略了您忽略的要求是不清楚的。调试器可以向您显示拥有COM服务器的其他线程,这样就足以识别死锁。或者只是忘记调用Application.Run(),当然,您可以从代码中看到它。您可以在this post中找到为此类COM服务器提供安全住所的代码。

答案 1 :(得分:0)

为什么不两个都做。您不需要将入口点作为STA线程,您只需要将消息泵作为STA线程。当你作为一个表单运行时,你需要做的就是启动一个非背景 STA线程,让#34; main thead"超出范围。

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [MTAThread]
    static void Main()
    {
        var args = Environment.GetCommandLineArgs();
        if (args.Contains("--service", StringComparer.OrdinalIgnoreCase))
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[]
            {
                new Service1()
            };
            ServiceBase.Run(ServicesToRun);
        }
        else
        {
            Thread formsThread = new Thread(FormsMain);
            formsThread.IsBackground = false;
            formsThread.SetApartmentState(ApartmentState.STA);
            formsThread.Name = "New Main";
            formsThread.Start();

            //The program will continue on here and exit Main but the program 
            // will not shutdown because formsThread is not a background thread.
        }
    }


    static void FormsMain()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}

答案 2 :(得分:0)

我只是将一个新线程作为非后台线程启动,让mainthread终止

using System;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Boolean isService = false;                       // Figure it out by args, ...

            Thread t = new Thread(new ParameterizedThreadStart(HelperMain));

            t.IsBackground = false;
            t.Priority = ThreadPriority.Normal;

            if (isService)
            {
                t.SetApartmentState(ApartmentState.MTA);
            }
            else
            {
                t.SetApartmentState(ApartmentState.STA);
            }

            t.Start(args);
        }

        static void HelperMain(Object o)
        {
            Console.WriteLine(Thread.CurrentThread.GetApartmentState());
            Console.ReadLine();
        }
    }
}

答案 3 :(得分:0)

STAThread属性在运行线程之前有效。但是在线程启动后调用方法Main()。

要解决此问题 - 运行其他线程:

static class Program
{
    static void Main()
    {
        if (RunningAsWindowsServiceCondition)
        {
            // run aaplication in windows service mode
        }
        else
        {
            // run as WinForms application
            var thread = new Thread(RunAsWinForms);
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
        }
    }

    private static void RunAsWinForms()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}