Вопрос

Я объединяю 15 асинхронных операций через порты и приемники.Это вызвало у меня большое беспокойство по поводу времени обмена сообщениями между потоками, в частности, времени, которое проходит между отправкой данных задачей в порт и началом обработки тех же данных в другом потоке новой задачей.Предполагая, что в лучшем случае каждый поток при запуске простаивает, я создал тест, который использует класс секундомера для измерения времени от двух разных диспетчеров, каждый из которых работает с наивысшим приоритетом с одним потоком.

То, что я обнаружил, меня удивило: мое оборудование для разработки представляет собой четырехъядерный компьютер Q6600 с тактовой частотой 2,4 ГГц под управлением Windows 7 x64, а среднее время переключения контекста в моем тесте составило 5,66 микросекунд со стандартным отклонением 5,738 микросекунд и максимальным значением почти 1,58 миллисекунды ( коэффициент 282!).Частота секундомера составляет 427,7 наносекунд, так что я все еще далеко от шума сенсора.

Что мне хотелось бы сделать, так это максимально сократить время обмена сообщениями между потоками и, что не менее важно, уменьшить стандартное отклонение переключения контекста.Я понимаю, что Windows не является ОС реального времени, и нет никаких гарантий, но планировщик Windows представляет собой график, основанный на справедливом циклическом переборе, и оба потока в этом тесте имеют наивысший приоритет (единственные потоки, которые должны быть такими, чтобы high), поэтому в потоках не должно быть никаких переключений контекста (о чем свидетельствует максимальное время 1,58 мс...Я полагаю, что квант Windows составляет 15,65 мс?) Единственное, о чем я могу думать, это изменение времени вызовов ОС механизмов блокировки, используемых CCR для передачи сообщений между потоками.

Пожалуйста, дайте мне знать, если кто-нибудь еще измерял время обмена сообщениями между потоками и имеет какие-либо предложения по его улучшению.

Вот исходный код из моих тестов:

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Microsoft.Ccr.Core;

using System.Diagnostics;

namespace Test.CCR.TestConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Starting Timer");
            var sw = new Stopwatch();
            sw.Start();

            var dispatcher = new Dispatcher(1, ThreadPriority.Highest, true, "My Thread Pool");
            var dispQueue = new DispatcherQueue("Disp Queue", dispatcher);

            var sDispatcher = new Dispatcher(1, ThreadPriority.Highest, true, "Second Dispatcher");
            var sDispQueue = new DispatcherQueue("Second Queue", sDispatcher);

            var legAPort = new Port<EmptyValue>();
            var legBPort = new Port<TimeSpan>();

            var distances = new List<double>();

            long totalTicks = 0;

            while (sw.Elapsed.TotalMilliseconds < 5000) ;

            int runCnt = 100000;
            int offset = 1000;

            Arbiter.Activate(dispQueue, Arbiter.Receive(true, legAPort, i =>
                                                                            {
                                                                                TimeSpan sTime = sw.Elapsed;
                                                                                legBPort.Post(sTime);
                                                                            }));
            Arbiter.Activate(sDispQueue, Arbiter.Receive(true, legBPort, i =>
                                                                             {
                                                                                 TimeSpan eTime = sw.Elapsed;
                                                                                 TimeSpan dt = eTime.Subtract(i);
                                                                                 //if (distances.Count == 0 || Math.Abs(distances[distances.Count - 1] - dt.TotalMilliseconds) / distances[distances.Count - 1] > 0.1)
                                                                                 distances.Add(dt.TotalMilliseconds);

                                                                                 if(distances.Count > offset)
                                                                                 Interlocked.Add(ref totalTicks,
                                                                                                 dt.Ticks);
                                                                                 if(distances.Count < runCnt)
                                                                                     legAPort.Post(EmptyValue.SharedInstance);
                                                                             }));


            //Thread.Sleep(100);
            legAPort.Post(EmptyValue.SharedInstance);

            Thread.Sleep(500);

            while (distances.Count < runCnt)
                Thread.Sleep(25);

            TimeSpan exTime = TimeSpan.FromTicks(totalTicks);
            double exMS = exTime.TotalMilliseconds / (runCnt - offset);

            Console.WriteLine("Exchange Time: {0} Stopwatch Resolution: {1}", exMS, Stopwatch.Frequency);

            using(var stw = new StreamWriter("test.csv"))
            {
                for(int ix=0; ix < distances.Count; ix++)
                {
                    stw.WriteLine("{0},{1}", ix, distances[ix]);
                }
                stw.Flush();
            }

            Console.ReadKey();
        }
    }
}
Это было полезно?

Решение

Windows не является ОС реального времени.Но ты это уже знал.Что вас убивает, так это время переключения контекста, а не обязательно время сообщения.Вы на самом деле не указали, КАК работает ваше межпроцессное взаимодействие.Если вы на самом деле просто используете несколько потоков, вы получите некоторые преимущества, если не будете использовать сообщения Windows в качестве протокола связи, а вместо этого попытаетесь развернуть свой собственный IPC, используя вместо этого очереди сообщений, размещенные в приложениях.

Наилучшее среднее значение, на которое вы можете рассчитывать, — это 1 мс для любой версии Windows при переключении контекста.Вы, вероятно, видите время в 1 мс, когда ваше приложение должно уступить ядру.Это сделано специально для приложений Ring-1 (пользовательское пространство).Если вам абсолютно необходимо получить время ниже 1 мс, вам необходимо переключить некоторые приложения на Ring-0, что означает написание драйвера устройства.

Драйверы устройств не страдают от такого же времени переключения контекста, как пользовательские приложения, и также имеют доступ к таймерам с разрешением в наносекунды и вызовам сна.Если вам все же необходимо это сделать, DDK (комплект разработки драйверов устройств) можно бесплатно получить от Microsoft, но я НАСТОЯТЕЛЬНО рекомендую вам приобрести сторонний комплект разработки.У них обычно есть действительно хорошие образцы и множество мастеров для правильной настройки, на изучение которых вам потребуются месяцы чтения документов DDK.Вам также понадобится что-то вроде SoftIce, потому что обычный отладчик Visual Studio не поможет вам отлаживать драйверы устройств.

Другие советы

Выполните 15 асинхронных операций иметь быть асинхронным?то естьвас вынуждают действовать таким образом из-за ограничений какой-то библиотеки или у вас есть возможность совершать синхронные вызовы?

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

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

Но вы «объединяете 15 асинхронных операций через порты и приемники».Это можно охарактеризовать как «досадно последовательный».Другими словами, одна и та же программа может быть логически написана в одном потоке.Но тогда вы потеряете любой параллелизм для работы, связанной с ЦП, происходящей между асинхронными операциями (при условии, что это имеет какое-либо значение).

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

Единственная причина запуска нескольких потоков заключается в том, что у вас есть значительный объем работы для процессоров, и поэтому вы хотели бы разделить ее между несколькими процессорами.Если отдельные фрагменты вычислений достаточно кратковременны, то переключение контекста будет значительным накладным расходом. любой ОПЕРАЦИОННЫЕ СИСТЕМЫ.Разбивая вычисления на 15 этапов, каждый из которых очень короткий, вы, по сути, просите ОС выполнить множество ненужных переключений контекста.

ThreadPriority.Highest не означает, что только сам планировщик потоков имеет более высокий приоритет.Win32 API имеет более детальный уровень приоритета потоков (щелкающий) с несколькими уровнями выше самого высокого (IIRC Highest обычно является наивысшим приоритетом, с которым может запускаться код, не являющийся администратором, администраторы могут запланировать более высокий приоритет, как и любые драйверы оборудования / код режима ядра), поэтому нет никакой гарантии, что они не будут вытеснены .

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

Даже в этом случае, как вы говорите, Windows не является ОС реального времени, и в любом случае она не гарантирует соблюдение приоритетов потоков.

Чтобы решить эту проблему по-другому, нужно ли вам иметь так много разделенных асинхронных операций?Возможно, будет полезно подумать:вертикальное секционирование работы (асинхронная обработка фрагментов данных в количестве numCores от начала до конца) вместо горизонтального секционирования работы (как сейчас, когда каждый фрагмент данных обрабатывается в 15 отдельных этапов);синхронно объединить некоторые из ваших 15 этапов, чтобы уменьшить общее количество.

Накладные расходы на межпотоковое взаимодействие всегда будут нетривиальными.Если некоторые из ваших 15 операций выполняют лишь небольшую часть работы, переключение контекста вас укусит.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top