[c#] StaTaskScheduler و STA thread message pumping



0 Answers

موضوع ضخ STA هو واحد كبير مع عدد قليل جدا من المبرمجين الذين لديهم وقت ممتع حل الجمود. كتبت الورقة المنوية حول هذا الموضوع من قبل كريس بروم ، الرجل الذكي الرئيسي الذي عمل على .NET. ستجد في this . لسوء الحظ ، هو باختصار في التفاصيل ، فهو لا يذهب إلى ما هو أبعد من ملاحظة أن CLR يقوم ببعض الضخ ولكن دون أي تفاصيل حول القواعد الدقيقة.

التعليمات البرمجية التي يتحدث عنها ، تمت إضافتها في .NET 2.0 ، موجودة في دالة CLR داخلية مسماة MsgWaitHelper (). يتوفر رمز المصدر لـ .NET 2.0 من خلال توزيع SSCLI20. كاملة للغاية ، ولكن لم يتم تضمين مصدر MsgWaitHelper (). جد، غير عادي. إلغاء ترجمته هو قضية خاسرة ، إنه كبير جدا.

إن الشيء الوحيد الذي يجب أن يبعده عن مدونته هو خطر إعادة الدخول . الضخ في موضوع STA يشكل خطراً على قدرته على إرسال رسائل Windows والحصول على تعليمات برمجية عشوائية لتنفيذها عندما لا يكون البرنامج الخاص بك في الحالة الصحيحة للسماح بتنفيذ مثل هذه التعليمات البرمجية. شيء معظم يعرفه أي مبرمج VB6 عند استخدام DoEvents () للحصول على حلقة مشروطة في التعليمات البرمجية الخاصة به لإيقاف تجميد واجهة المستخدم. كتبت رسالة عن أخطارها الأكثر شيوعًا. MsgWaitHelper () يفعل هذا النوع الدقيق من ضخ نفسها ، إلا أنه انتقائي للغاية حول بالضبط أي نوع من التعليمات البرمجية التي يسمح لها بتشغيل.

يمكنك الحصول على بعض الأفكار في ما تقوم به داخل برنامج الاختبار الخاص بك عن طريق تشغيل البرنامج بدون مصحح أخطاء ثم إرفاق مصحح أخطاء غير مُدار. سترى أنه حظر على NtWaitForMultipleObjects (). أخذت خطوة واحدة إضافية وقمت بتعيين نقطة توقف على PeekMessageW () ، للحصول على هذا تتبع المكدس:

user32.dll!PeekMessageW()   Unknown
combase.dll!CCliModalLoop::MyPeekMessage(tagMSG * pMsg, HWND__ * hwnd, unsigned int min, unsigned int max, unsigned short wFlag) Line 2305  C++
combase.dll!CCliModalLoop::PeekRPCAndDDEMessage() Line 2008 C++
combase.dll!CCliModalLoop::FindMessage(unsigned long dwStatus) Line 2087    C++
combase.dll!CCliModalLoop::HandleWakeForMsg() Line 1707 C++
combase.dll!CCliModalLoop::BlockFn(void * * ahEvent, unsigned long cEvents, unsigned long * lpdwSignaled) Line 1645 C++
combase.dll!ClassicSTAThreadWaitForHandles(unsigned long dwFlags, unsigned long dwTimeout, unsigned long cHandles, void * * pHandles, unsigned long * pdwIndex) Line 46 C++
combase.dll!CoWaitForMultipleHandles(unsigned long dwFlags, unsigned long dwTimeout, unsigned long cHandles, void * * pHandles, unsigned long * lpdwindex) Line 120 C++
clr.dll!MsgWaitHelper(int,void * *,int,unsigned long,int)   Unknown
clr.dll!Thread::DoAppropriateWaitWorker(int,void * *,int,unsigned long,enum WaitMode)   Unknown
clr.dll!Thread::DoAppropriateWait(int,void * *,int,unsigned long,enum WaitMode,struct PendingSync *)    Unknown
clr.dll!CLREventBase::WaitEx(unsigned long,enum WaitMode,struct PendingSync *)  Unknown
clr.dll!CLREventBase::Wait(unsigned long,int,struct PendingSync *)  Unknown
clr.dll!Thread::Block(int,struct PendingSync *) Unknown
clr.dll!SyncBlock::Wait(int,int)    Unknown
clr.dll!ObjectNative::WaitTimeout(bool,int,class Object *)  Unknown

كن حذرًا من أنني سجلت هذا التتبع على نظام Windows 8.1 ، سيبدو الأمر مختلفًا تمامًا في إصدارات Windows القديمة. تم ترقيع حلقة مشروطة COM بشكل كبير في Windows 8 ، وهي أيضًا صفقة كبيرة جدًا لبرامج WinRT. لا أعرف الكثير عن ذلك ، ولكن يبدو أن هناك نموذج خيوط STA آخر يدعى ASTA يقوم بنوع أكثر تقييدًا من الضخ ، متضمنًا في CoWaitForMultipleObjects ()

ObjectNative :: WaitTimeout () حيث يبدأ SemaphoreSlim.Wait () داخل أسلوب BlockingCollection.Take () تنفيذ التعليمات البرمجية CLR. ترى أنها الحرث من خلال مستويات رمز CLR الداخلي للوصول إلى وظيفة MsgWaitHelper () الأسطورية ، ثم التحول إلى حلقة مرسل مشروط COM سيئة السمعة.

إشارة الخفاش إشارة القيام به نوع "خاطئة" من الضخ في البرنامج الخاص بك هو استدعاء أسلوب CliModalLoop :: PeekRPCAndDDEMessage (). بمعنى آخر ، يتم فقط النظر في نوع الرسائل المتداخلة التي يتم نشرها إلى إطار داخلي محدد يقوم بإرسال استدعاءات COM التي تعبر حدود الشقة. لن يضخ الرسائل الموجودة في قائمة انتظار الرسائل لإطارك الخاص.

هذا سلوك يمكن تفهمه ، يمكن لـ Windows التأكد تمامًا من أن re-entrancy لن يقتل البرنامج الخاص بك عندما يرى أن مؤشر ترابط واجهة المستخدم الخاص بك خامل . يكون الخمول عندما يضخ حلقة الرسالة نفسها ، يشير استدعاء PeekMessage () أو GetMessage () إلى هذه الحالة. المشكلة هي أنك لا تضغط على نفسك. لقد انتهكت العقد الأساسي لسلسلة STA ، يجب أن تضخ حلقة الرسالة. على أمل أن حلقة مشروطة COM ستفعل الضخ بالنسبة لك هو بالتالي أمل خامل.

يمكنك بالفعل إصلاح هذا ، على الرغم من أنني لا أنصحك بذلك. سيترك CLR إلى التطبيق نفسه لتنفيذ الانتظار بواسطة كائن SynchronizationContext.Current التي تم إنشاؤها بشكل صحيح. يمكنك إنشاء واحدة عن طريق اشتقاق الدورة التدريبية الخاصة بك وتجاوز أسلوب Wait (). استدعاء الأسلوب SetWaitNotificationRequired () لإقناع CLR أنه يجب ترك الأمر لك. نسخة غير كاملة توضح النهج:

class MySynchronizationProvider : System.Threading.SynchronizationContext {
    public MySynchronizationProvider() {
        base.SetWaitNotificationRequired();
    }
    public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) {
        for (; ; ) {
            int result = MsgWaitForMultipleObjects(waitHandles.Length, waitHandles, waitAll, millisecondsTimeout, 8);
            if (result == waitHandles.Length) System.Windows.Forms.Application.DoEvents();
            else return result;
        }
    }
    [DllImport("user32.dll")]
    private static extern int MsgWaitForMultipleObjects(int cnt, IntPtr[] waitHandles, bool waitAll,
        int millisecondTimeout, int mask);        
}

وتثبيته في بداية الموضوع الخاص بك:

    System.ComponentModel.AsyncOperationManager.SynchronizationContext =
        new MySynchronizationProvider();

سترى الآن رسالة WM_TEST الخاصة بك يتم إرسالها. انها دعوة ل Application.DoEvents () التي أرسلت ذلك. يمكن أن يكون تغطيتها باستخدام PeekMessage + DispatchMessage ولكن ذلك من شأنه التعتيم على خطر هذا الرمز ، أفضل عدم الالتزام DoEvents () تحت الجدول. كنت حقا تلعب لعبة إعادة entrancy خطير جدا هنا. لا تستخدم هذا الرمز.

قصة قصيرة طويلة ، هو الأمل الوحيد في استخدام StaThreadScheduler بشكل صحيح عندما يتم استخدامه في التعليمات البرمجية التي نفذت بالفعل العقد STA والمضخات مثل مؤشر ترابط STA يجب القيام به. كان من المفترض حقا كمساعد عصابة للرمز القديم حيث لم يكن لديك إلى الترف للتحكم في حالة الخيط. مثل أي رمز بدأ الحياة في برنامج VB6 أو Office Add-in. جرب قليلا معها ، لا أعتقد أنه في الواقع يمكن أن تعمل. والجدير بالملاحظة أيضًا هو أن الحاجة إليها يجب أن يتم القضاء عليها تمامًا مع توافر asych / await.

Question

TL ؛ DR: حالة توقف تام داخل مهمة تشغيل بواسطة StaTaskScheduler . نسخة طويلة:

أستخدم StaTaskScheduler من ParallelExtensionsExtras بواسطة Parallel Team ، لاستضافة بعض كائنات COM STA القديمة التي تم توفيرها بواسطة جهة خارجية. وصف تفاصيل تطبيق StaTaskScheduler يقول ما يلي:

والخبر السار هو أن تطبيق TPL قادر على العمل على خيوط MTA أو STA ، ويأخذ في الاعتبار الاختلافات ذات الصلة حول واجهات برمجة التطبيقات الأساسية مثل WaitHandle.WaitAll (الذي لا يدعم سوى سلاسل عمليات MTA عندما يتم توفير الأسلوب مقابض انتظار متعددة).

ظننت أن ذلك يعني أن الأجزاء المحجوبة من TPL ستستخدم واجهة برمجة تطبيقات للانتظار ، والتي تضخ الرسائل ، مثل CoWaitForMultipleHandles ، لتجنب حالات توقف تام عند استدعاء مؤشر ترابط STA.

في وضعي ، أعتقد أن ما يحدث هو التالي: كائن STA COM في proc إجراء مكالمة إلى كائن خارج proc من B ، ثم تتوقع استدعاء من B عبر كجزء من الاستدعاء الصادر.

في شكل مبسط:

var result = await Task.Factory.StartNew(() =>
{
    // in-proc object A
    var a = new A(); 
    // out-of-proc object B
    var b = new B(); 
    // A calls B and B calls back A during the Method call
    return a.Method(b);     
}, CancellationToken.None, TaskCreationOptions.None, staTaskScheduler);

المشكلة هي ، a.Method(b) لا يعود. بقدر ما أستطيع أن أقول ، يحدث هذا لأن حظر الانتظار في مكان ما داخل BlockingCollection<Task> لا يضخ الرسائل ، لذا فإن افتراضاتي حول العبارة المقتبسة ربما يكون خطأ.

EDITED نفس التعليمة البرمجية يعمل عند تنفيذ على مؤشر ترابط واجهة المستخدم من تطبيق اختبار WinForms (أي توفير TaskScheduler.FromCurrentSynchronizationContext() بدلاً من staTaskScheduler إلى Task.Factory.StartNew ).

ما هي الطريقة الصحيحة لحل هذا؟ هل يجب أن أقوم بتطبيق سياق التزامن المخصص ، والذي سيقوم بشكل صريح CoWaitForMultipleHandles الرسائل باستخدام CoWaitForMultipleHandles ، وتثبيته على كل مؤشر ترابط STA يتم تشغيله بواسطة StaTaskScheduler ؟

إذا كان الأمر كذلك ، فهل سيكون التطبيق الأساسي لـ BlockingCollection هو الأسلوب SynchronizationContext.Wait بي؟ هل يمكنني استخدام SynchronizationContext.WaitHelper لتنفيذ SynchronizationContext.Wait ؟

EDITED مع بعض التعليمات البرمجية التي تبين أن مؤشر ترابط STA الذي تتم إدارته لا يتم ضخه عند إجراء انتظار حظر. الرمز هو تطبيق وحدة تحكم كامل جاهز للنسخ / اللصق / التشغيل:

using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleTestApp
{
    class Program
    {
        // start and run an STA thread
        static void RunStaThread(bool pump)
        {
            // test a blocking wait with BlockingCollection.Take
            var tasks = new BlockingCollection<Task>();

            var thread = new Thread(() => 
            {
                // Create a simple Win32 window 
                var hwndStatic = NativeMethods.CreateWindowEx(0, "Static", String.Empty, NativeMethods.WS_POPUP,
                    0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);

                // subclass it with a custom WndProc
                IntPtr prevWndProc = IntPtr.Zero;

                var newWndProc = new NativeMethods.WndProc((hwnd, msg, wParam, lParam) =>
                {
                    if (msg == NativeMethods.WM_TEST)
                        Console.WriteLine("WM_TEST processed");
                    return NativeMethods.CallWindowProc(prevWndProc, hwnd, msg, wParam, lParam);
                });

                prevWndProc = NativeMethods.SetWindowLong(hwndStatic, NativeMethods.GWL_WNDPROC, newWndProc);
                if (prevWndProc == IntPtr.Zero)
                    throw new ApplicationException();

                // post a test WM_TEST message to it
                NativeMethods.PostMessage(hwndStatic, NativeMethods.WM_TEST, IntPtr.Zero, IntPtr.Zero);

                // BlockingCollection blocks without pumping, NativeMethods.WM_TEST never arrives
                try { var task = tasks.Take(); }
                catch (Exception e) { Console.WriteLine(e.Message); }

                if (pump)
                {
                    // NativeMethods.WM_TEST will arrive, because Win32 MessageBox pumps
                    Console.WriteLine("Now start pumping...");
                    NativeMethods.MessageBox(IntPtr.Zero, "Pumping messages, press OK to stop...", String.Empty, 0);
                }
            });

            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();

            Thread.Sleep(2000);

            // this causes the STA thread to end
            tasks.CompleteAdding(); 

            thread.Join();
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Testing without pumping...");
            RunStaThread(false);

            Console.WriteLine("\nTest with pumping...");
            RunStaThread(true);

            Console.WriteLine("Press Enter to exit");
            Console.ReadLine();
        }
    }

    // Interop
    static class NativeMethods
    {
        [DllImport("user32")]
        public static extern IntPtr SetWindowLong(IntPtr hwnd, int nIndex, WndProc newProc);

        [DllImport("user32")]
        public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hwnd, int msg, int wParam, int lParam);

        [DllImport("user32.dll")]
        public static extern IntPtr CreateWindowEx(int dwExStyle, string lpClassName, string lpWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);

        [DllImport("user32.dll")]
        public static extern bool PostMessage(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        public static extern int MessageBox(IntPtr hwnd, string text, String caption, int options);

        public delegate IntPtr WndProc(IntPtr hwnd, int msg, int wParam, int lParam);

        public const int GWL_WNDPROC = -4;
        public const int WS_POPUP = unchecked((int)0x80000000);
        public const int WM_USER = 0x0400;

        public const int WM_TEST = WM_USER + 1;
    }
}

هذا ينتج الإخراج:

Testing without pumping...
The collection argument is empty and has been marked as complete with regards to additions.

Test with pumping...
The collection argument is empty and has been marked as complete with regards to additions.
Now start pumping...
WM_TEST processed
Press Enter to exit



Related