API CoWaitForMultipleHandles не работает как описано

Это было вызвано другим вопросом, на который я смотрел. Это может быть слишком долго читать, поэтому, пожалуйста, потерпите меня.

По-видимому, CoWaitForMultipleHandles не ведет себя так, как описано в MSDN.

Приведенный ниже код (на основе исходного вопроса) представляет собой консольное приложение, которое запускает поток STA с тестовым окном Win32 и пытается опубликовать и перекачать некоторые сообщения. Это делает три разных теста на CoWaitForMultipleHandles все без COWAIT_WAITALL флаг.

Тест № 1 предназначен для проверки этого:

COWAIT_INPUTAVAILABLE Если установлено, вызов CoWaitForMultipleHandles вернет S_OK, если для очереди существует вход, даже если вход был просмотрен (но не удален) с использованием вызова другой функции, такой как PeekMessage.

Этого не происходит, CoWaitForMultipleHandles блокирует и не возвращает до тех пор, пока дескриптор ожидания не будет сигнализирован. Я предполагаю, что любое ожидающее сообщение должно рассматриваться как ввод (так же, как и с MWMO_INPUTAVAILABLE из MsgWaitForMultipleObjectsEx, который работает как положено).

Тест № 2 предназначен для проверки этого:

COWAIT_DISPATCH_WINDOW_MESSAGES Включает отправку оконных сообщений от CoWaitForMultipleHandles в ASTA или STA. По умолчанию в ASTA не отправляются оконные сообщения, по умолчанию в STA отправляется только небольшой набор отправленных сообщений. Значение не имеет значения в MTA и игнорируется.

Это тоже не работает. когда CoWaitForMultipleHandles называется с COWAIT_DISPATCH_WINDOW_MESSAGES только флаг, мгновенно возвращает ошибку CO_E_NOT_SUPPORTED (0x80004021). Если это комбинация COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS, вызов блокирует, но не качает никаких сообщений.

Тест № 3 демонстрирует единственный способ, которым я мог сделать CoWaitForMultipleHandles прокачайте очередь сообщений Windows вызывающего потока. Это комбинация COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS | COWAIT_INPUTAVAILABLE, Это делает прокачку и отправку сообщений, хотя, по-видимому, это недокументированное поведение.

Тестовый код (готовое к запуску консольное приложение):

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

namespace ConsoleTestApp
{
    static class Program
    {
        // Main 
        static void Main(string[] args)
        {
            Console.WriteLine("Starting an STA thread...");
            RunStaThread();

            Console.WriteLine("\nSTA thread finished.");
            Console.WriteLine("Press Enter to exit.");
            Console.ReadLine();
        }

        // start and run an STA thread
        static void RunStaThread()
        {
            var thread = new Thread(() =>
            {
                // create a simple Win32 window
                IntPtr hwnd = CreateTestWindow();

                // Post some WM_TEST messages
                Console.WriteLine("Post some WM_TEST messages...");
                NativeMethods.PostMessage(hwnd, NativeMethods.WM_TEST, new IntPtr(1), IntPtr.Zero);
                NativeMethods.PostMessage(hwnd, NativeMethods.WM_TEST, new IntPtr(2), IntPtr.Zero);
                NativeMethods.PostMessage(hwnd, NativeMethods.WM_TEST, new IntPtr(3), IntPtr.Zero);

                // Test #1
                Console.WriteLine("\nTest #1. CoWaitForMultipleHandles with COWAIT_INPUTAVAILABLE only, press Enter to stop...");
                var task = ReadLineAsync();

                uint index;
                var result = NativeMethods.CoWaitForMultipleHandles(
                    NativeMethods.COWAIT_INPUTAVAILABLE,
                    NativeMethods.INFINITE,
                    1, new[] { task.AsUnmanagedHandle() },
                    out index);
                Console.WriteLine("Result: " + result + ", pending messages in the queue: " + (NativeMethods.GetQueueStatus(0x1FF) >> 16 != 0));

                // Test #2
                Console.WriteLine("\nTest #2. CoWaitForMultipleHandles with COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS, press Enter to stop...");
                task = ReadLineAsync();

                result = NativeMethods.CoWaitForMultipleHandles(
                    NativeMethods.COWAIT_DISPATCH_WINDOW_MESSAGES | 
                        NativeMethods.COWAIT_DISPATCH_CALLS,
                    NativeMethods.INFINITE, 
                    1, new[] { task.AsUnmanagedHandle() },
                    out index);
                Console.WriteLine("Result: " + result + ", pending messages in the queue: " + (NativeMethods.GetQueueStatus(0x1FF) >> 16 != 0));

                // Test #3
                Console.WriteLine("\nTest #3. CoWaitForMultipleHandles with COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS | COWAIT_INPUTAVAILABLE, press Enter to stop...");
                task = ReadLineAsync();

                result = NativeMethods.CoWaitForMultipleHandles(
                    NativeMethods.COWAIT_DISPATCH_WINDOW_MESSAGES | 
                        NativeMethods.COWAIT_DISPATCH_CALLS | 
                        NativeMethods.COWAIT_INPUTAVAILABLE,
                    NativeMethods.INFINITE,
                    1, new[] { task.AsUnmanagedHandle() },
                    out index);
                Console.WriteLine("Result: " + result + ", pending messages in the queue: " + (NativeMethods.GetQueueStatus(0x1FF) >> 16 != 0));
            });

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

            thread.Join();
        }

        //
        // Helpers
        //

        // create a window to handle messages
        static IntPtr CreateTestWindow()
        {
            // Create a simple Win32 window 
            var hwndStatic = NativeMethods.CreateWindowEx(0, "Static", String.Empty, NativeMethods.WS_POPUP,
                0, 0, 0, 0, NativeMethods.HWND_MESSAGE, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);

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

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

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

            return hwndStatic;
        }

        // call Console.ReadLine on a pool thread
        static Task<string> ReadLineAsync()
        {
            return Task.Run(() => Console.ReadLine());
        }

        // get Win32 waitable handle of Task object
        static IntPtr AsUnmanagedHandle(this Task task)
        {
            return ((IAsyncResult)task).AsyncWaitHandle.SafeWaitHandle.DangerousGetHandle();
        }
    }

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

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

        [DllImport("user32.dll")]
        public static extern IntPtr CreateWindowEx(
            uint dwExStyle, string lpClassName, string lpWindowName, uint 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);

        [DllImport("ole32.dll", SetLastError = true)]
        public static extern uint CoWaitForMultipleHandles(uint dwFlags, uint dwTimeout,
           int cHandles, IntPtr[] pHandles, out uint lpdwindex);

        [DllImport("user32.dll")]
        public static extern uint GetQueueStatus(uint flags);

        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        public delegate IntPtr WndProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);

        public static IntPtr HWND_MESSAGE = new IntPtr(-3);

        public const int GWL_WNDPROC = -4;
        public const uint WS_POPUP = 0x80000000;

        public const uint WM_USER = 0x0400;
        public const uint WM_TEST = WM_USER + 1;

        public const uint COWAIT_WAITALL = 1;
        public const uint COWAIT_ALERTABLE = 2;
        public const uint COWAIT_INPUTAVAILABLE = 4;
        public const uint COWAIT_DISPATCH_CALLS = 8;
        public const uint COWAIT_DISPATCH_WINDOW_MESSAGES = 0x10;

        public const uint RPC_S_CALLPENDING = 0x80010115;

        public const uint WAIT_TIMEOUT = 0x00000102;
        public const uint WAIT_FAILED = 0xFFFFFFFF;
        public const uint WAIT_OBJECT_0 = 0;
        public const uint WAIT_ABANDONED_0 = 0x00000080;
        public const uint WAIT_IO_COMPLETION = 0x000000C0;

        public const uint INFINITE = 0xFFFFFFFF;
    }
}

Выход:

Запуск темы STA... Опубликовать несколько сообщений WM_TEST... Тест №1. CoWaitForMultipleHandles только с COWAIT_INPUTAVAILABLE, нажмите Enter, чтобы остановить... Результат: 0, ожидающие сообщения в очереди: True Test # 2. CoWaitForMultipleHandles с COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS, нажмите Enter, чтобы остановить... Результат: 0, ожидающие сообщения в очереди: True Test # 3. CoWaitForMultipleHandles с COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS | COWAIT_INPUTAVAILABLE, нажмите Enter, чтобы остановить... WM_TEST обработано: 1 WM_TEST обработано: 2 WM_TEST обработано: 3 Результат: 0, ожидающие сообщения в очереди: Ложный поток STA завершен. Нажмите Enter, чтобы выйти. 

Все тесты проводятся под Windows 8.1 Pro 64bit + NET v4.5.1.

  • Я неправильно читаю документы или упускаю что-то еще?

  • Должен ли я сообщить об этом как об ошибке (по крайней мере, об ошибке в документации)?

  • Должен CoWaitForMultipleHandles следует избегать и заменить решение на основе MsgWaitForMultipleObjectsEx (который ведет себя в соответствии с документами)?

[ОБНОВЛЕНИЕ] Под Windows 7, ни COWAIT_DISPATCH_WINDOW_MESSAGES ни COWAIT_DISPATCH_CALLS поддерживаются, CoWaitForMultipleHandles не удается с E_INVALIDARG (0x80070057). При вызове с нулями в качестве флагов он блокируется без накачки.

1 ответ

Решение

CoWaitForMultipleHandles предназначен для обработки сообщений окна COM (например, сортировка между квартирами) и нескольких других (не спрашивайте меня, какие) в STA или просто блокировать в MTA. В этом посте "Управляемая блокировка" Криса Брамме говорится, что CWFMH обрабатывает "только правильное количество" оконных сообщений. Однако, поскольку он оставляет в очереди любое сообщение, отправленное не COM-сообщением, очередь все еще может заполняться, но не сообщениями COM-окна.

Согласно этому документу, "Перенос приложения Windows 8 Consumer Preview в Windows 8 Release Preview" гласит:

Функция CoWaitForMultipleHandles больше не поддерживается в приложениях Магазина Windows. Кроме того, были удалены следующие CoWait_Flags:

COWAIT_DISPATCH_CALLS

COWAIT_DISPATCH_WINDOW_MESSAGES

Если вы действительно хотите обрабатывать все сообщения, вы должны использовать MsgWaitForMultipleObjectsEx в цикле сообщений с GetMessage или же PeekMessage с PM_REMOVE, Это означает потенциальное безумие повторного входа. Вы по-прежнему не управляете дальнейшими вызовами в STA из других компонентов в стеке. То есть модальное диалоговое окно (например, Common Dialog Box for Open) может качать каждое сообщение в цикле сообщений обычного старого окна, но некоторые фреймворки могут вызывать CoWaitForMultipleHandles,

Суть в том, что если вы выполняете интенсивную обработку или блокируете операции, делегируйте его другому потоку (возможно, используя очередь) и, если необходимо, сообщите вызывающему потоку пользовательского интерфейса для обновления после выполнения операции.

Это отличается, например, от длительных вызовов пользовательского интерфейса, таких как встраивание OLE или модальное диалоговое окно, где обычно есть цикл сообщений окна где-то вдоль стека. Или из длительных, но группируемых / возобновляемых операций (например, конечного автомата), где вы можете сотрудничать, обрабатывая сообщения время от времени, или используя функции ожидания, которые возвращаются, когда есть сообщения, чтобы вы могли обработать их, прежде чем снова ждать.

Будьте осторожны, это хорошо работает только для одной ручки; для нескольких дескрипторов, например мьютексов, вам нужно либо все, либо ни одного, а следующий лучший подход - это активный цикл с тайм-аутом вызова WaitForMultipleObjects с последующим PeekMessage с PM_REMOVE оконный цикл сообщений. Это пограничный случай, приемлемый для приложения с пользовательским интерфейсом, которое является центром внимания пользователя (например, это его основная работа), но неприемлемым, если такой код может выполняться без присмотра и по требованию. Если вы точно не знаете, что это должно происходить в потоке STA или пользовательского интерфейса, мой совет: не делайте этого.

Наконец, вам, вероятно, следует открыть ошибку в Microsoft Connect, по крайней мере, для обновления документации. Или фактически заставить это работать как "ожидаемый".

Другие вопросы по тегам