Сегодня: Вторник, 23.04.2024, 10:32 (МСК)| Здравствуйте, Гость| Мой профиль | Регистрация | Вход | RSS

Военные технологии на пользовательском рынке

Визуальная среда Flowstone

Роботы и экзоскелеты

Google Chrome. Таким должен быть браузер

Программы — виртуальные гитаристы
Главная » ПОПУЛЯРНО ОБ ИИ

Популярно об ИИ. Программирование. Потоки

26.07.2010
Данный материал носит объединенный характер и подпадает сразу под две серии вашего покорного слуги ("Популярно об ИИ" и "Ликбез по программированию"). Причиной тому послужило несколько писем. Изначально я просто хотел ответить на вопрос: 

«Кристофер, помоги разобраться с потоками для С++ —> Windows Application. Все книги в основном описывают MFC, а на лекциях дали вывод в консоли, для Windows Application это не работает… интересует как расставлять приоритеты, это тож непонятная тема. Или напиши, пожалуйста, материал по потокам…
С уважением,
Вячеслав»

Потом встретился с небольшими нападками, в которых говорилось конкретно о серии «Популярно об ИИ». То есть, отметили, что там хорошо представлен начальный уровень, популяризируется тема, но… показываются узкоспециализированные решения, многие из которых «не применимы для крупных проектов». Автор такого письма так распалился, что предложил вашему покорному слуге «перед тем как писать, консультироваться со специалистами данной области». Я и консультируюсь:))). Сам с собой, поскольку искусственным интеллектом занимаюсь сравнительно давно и даже… Хотя не стоит хвастаться. Укажу на одну очевидную вещь: как называется серия материалов? «ПОПУЛЯРНО(!) об ИИ». Методологий масса, некоторые книги вообще невозможно читать, да и многие постулаты дисциплины выглядят спорно. Это, во-первых. Во-вторых, если бы ваш покорный слуга начал эту серию с описания PROLOG или LISP, то бесспорно выглядел бы в глазах читателей умным, но они бы ничего не поняли.
Переходим к делу, отвечаем на первое письмо…


О потоках (С++, Windows Application)


Тема долгая... Поэтому попытаемся изложить кратко. Итак, лично я воспринимаю потоки в большинстве случаев как асинхронно работающие функции, скажем так. Если речь конкретно не заходит о симметричной многопроцессорнай обработке. Внутренние механизмы Windows позволяют представлять потоки как одновременно и синхронно выполняющиеся процессы. Это может быть действительно так, если используется симметричная многопроцессорная обработка либо создается видимость синхронно выполняющихся процессов. Потоки делятся по приоритетам. При этом можно сказать о существовании очереди выполнения. Все это делается с помощью специального компонента операционной системы, называемого планировщиком потоков (Thread Scheduler). 

В рамках нашего случая можно привести пример: одна функция отвечает за вывод окна на экран, другая что-то считает и т.п. В классическом программировании под MFC (!), если вы правильную информацию нашли, используется два вида потоков: потоки пользовательского интерфейса (UI threads) и рабочие потоки (worker threads). Первые отвечают за вывод интерфейсных окон и элементов, они обладают циклом сообщений (message loop), то есть можно обрабатывать сообщения, посылаемые этим окнам и так далее. Рабочие потоки в основном предназначены для фоновых вычислений, хотя их можно использовать по-разному.

Я понимаю проблему, с которой столкнулись вы, когда потоки, написанные вами, начинают «ссориться» с потоками пользовательских интерфейсов. Вернее, компьютер ведет себя правильно. Как результат — окно просто зависает. Почему так происходит? Фактически вы, не разобравшись с ситуацией, помещаете выполнение рабочего потока внутрь UI потока. А если там используется бесконечный цикл или серьезная рекурсия?   
 
Я покажу самый простой вариант нормального решения ситуации, так чтобы было понятно вам.
Перед Main() указываем:

DWORD WINAPI KakayaToFunkzia(LPVOID); 
Допустим, тело потока у нас будет таким:
DWORD WINAPI 
KakayaToFunkzia(LPVOID vThreadParm) 
{
while(1)
 {
   //что-то делаем
 }
}

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

HANDLE   myNewThread; 
DWORD    dwThreadID;
myNewThread = CreateThread(
NULL, 0, KakayaToFunkzia, 
&threadParm, 0, 
&dwThreadID);    

Запускайте приложение, оно будет работать без сбоев. Самое главное в данном случае следить за областями видимости для обмена данными между потоками. В нашем случае это может быть, например, вывод полученных из потока сообщений в каком-нибудь элементе графического интерфейса (textbox, listbox, combobox и т.п.) с постоянным обновлением данных и так далее. 
В простейшем варианте все делается с помощью глобальных переменных. Например, в потоке в бесконечном цикле из бинарного файла считывается некий параметр. Как его прочитать и отобразить в каком-нибудь элементе графического Windows-интерфейса? Решение простое, поэтому опишу не кодом, а словами. На форму ставите таймер, который обновляет, например, наполнение TextBox с частотой раз в 1 секунду (интервал 1000). Создаем некую глобальную переменную, значение которой переписывается в нашем потоке, а таймер ее считывает и переносит на TextBox. То есть, эта переменная должна быть видна везде.
 
Это только один из вариантов реализации, показанный для того, чтобы вы поняли структуру работы. На самом деле такая реализация не очень удобна для некоторых случаев, но эту тему нужно рассматривать отдельно. То есть, опять же все зависит от конкретной задачи. Например, нередко вы можете столкнуться со специальным проектированием программ, в которых есть отдельные интерфейсы/программные модули и т.п. Общение между ними могут реализовываться как напрямую (например, те же сокеты), либо же через текстовые или бинарные файлы.  


Переключение темы
 

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

Теперь производим переключение:). Отдельно стоит сказать о «семафоринге», то есть способности переключения между потоками, а также возможность одного потока управлять действиями другого. Это вызывает ряд сложностей в освоении, поэтому я опишу несколько другую ситуацию. 


Эмулируем многопоточность


Допустим, у вас есть задача реализовать многопоточность в рамках среды, которая ее не поддерживает. То есть, нам нужно самостоятельно создать планировщик потоков. Буквально перед этим Новым годом вашему покорному слуге позвонили друзья с очень веселой проблемой, а именно, они решили сделать свое решение кросс-платформенным, при этом исходный код у них был написан под Windows с использованием потоков на базе стандартных решений. Не многие ОС это поддерживают, а во многих случаях и поддерживают по-другому. Попросили придумать и реализовать решение. Я сразу сказал, что наиболее оптимальный вариант — перенести расчеты в процедурный язык Lua, сценарии которого легко встраиваемы в исполняемый С/С++/С# и т.п. код, а потоки преобразовать в асинхронные вызовы. Звучало сложно, пока не увидели результат.

Пример для вас? Специально сделал простой вариант, отображенный в листинге. Поскольку Lua знают не многие, ваш покорный слуга написал пример на C#. Главное — чтобы вы поняли суть. Для повторения этого примера просто создайте новый консольный проект в Visual C# и скопируйте представленный код. После разберитесь как он работает (описание работы также есть дальше в статье). 

«Эмуляция многопоточности»

using System;

namespace ConsoleApplication2
{
    class Program
    {
        //главная функция
        static void Main(string[] args)
        {
            //вызываем из Main() функцию "селектора потоков" 
            //SelectorFunc(). в C# это лучше делать так...
            Program anInstanceofProgram = new Program();
            anInstanceofProgram.SelectorFunc();
            //консольное окно должно остаться открытым
            Console.ReadLine();
        }

        //функция переключения потоков
        void SelectorFunc() 
        {
            //счетчик вызовов
            int counter = 0;
            //шаг вызовов
            int counter_step;
            //переключатель между двумя функциями selector
            //ВНИМАНИЕ!!! Если функций много (не две как в нашем 
            // примере), то эта переменная будет как int, и вместо
            //if в цикле далее используем switch-case
            bool selector = true;
            //запускаем цикл
            for (counter = 0; counter < 100; counter += counter_step)
            {
                //"качели"... если selector == true, то вызываем 
                //функцию AFunction(), которой сообщаем номер вызова
                //и шаг, потом переводим selector в false, чтобы вызвать
                //BFunction().
                if (selector)
                {
                    //"эмулируем" приоритетность потоков
                    counter_step=3;
                    //запускаем AFunction()
                    AFunction(counter, counter_step);
                    selector = false;
                }
                else
                {
                    //"эмулируем" приоритетность потоков
                    counter_step = 1;
                    //запускаем BFunction()
                    BFunction(counter, counter_step);
                    selector = true;
                }

            }
        }

        //функция AFunction() принимает текущее значение счетчика
        //и шаг, выводит порядковый номер вызова и строку "Hello"
        void AFunction(int a, int b)
        {
            for (int i = a; i < 100; i++)  
            {   //обратите внимание на то, как
                //функционирует цикл, то есть, он
                //стартует с текущей позиции до варианта
                //позиция + шаг
                if (i == a+b) break;
                Console.Write(i.ToString()+" ");
                Console.WriteLine("Hello");
                
 
            }

        }

        //идентично BFunction()... выводит строку ", World!"
        void BFunction(int a, int b)
        {
            for (int i = a; i < 100; i++)
            {   if (i == a + b) break;
                Console.Write(i.ToString() + " ");
                Console.WriteLine(", World!");
                
            }
        }

    }


}

Результат



Объяснение


У нас есть две функции, которые мы хотим «запустить синхронно». Условно мы их обозначили AFunction() и BFunction(). Для простоты сделали так, что одна выводит в консольном окне строку «Hello», а другая «, World!». При этом все сделано в циклах. Зачем? Допустим, у вас AFunction() будет не просто выводить строку, а заполнять массив, производить чтение из файла, делать какую-нибудь циклическую работу. Функция BFunction() производит то же самое, но с другими файлами и данными. Нам нужна та самая пресловутая «синхронизация», а вернее, реализация переключателя между работой двух функций. Мы его и сделали в рамках SelectorFunc(). 

Что вообще делает эта программа?:))) Сначала наш селектор вызывает одну функцию, но, при этом, потом переключает вызов на другую, потом опять на первую (простые качели true-false, если функций больше используем конструкцию switch-case). Шаг вызовов — это ничто иное, как приоритет. Допустим, мы сделали для AFunction() значение selector_step равным 3-м, а для BFunction() — единице, и можем сказать, что приоритетным является процесс выполнения функции AFunction(). Как видите, все просто, понятно и работает. Можно сказать, что мы заменили конструкцию потоков асинхронными вызовами. 

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

Также обратите внимание на то, что мы можем из одной функции блокировать вызовы другой. В нашем случае это проще всего делать с помощью более тонкой настройки переключений на базе переменной selector (возможно несколько вариантов). 

Причем здесь ИИ?

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

Давайте возьмем какой-нибудь простейший пример. Допустим, мы делаем стратегическую игру с множеством юнитов. Рассмотрим ситуацию, когда к укрепленным позициям подходит противник (реальный игрок, а мы считаем за компьютер). В этом случае у нас должны быть приоритеты, а именно в первую очередь рассчитывается поведение для юнитов, которые находятся ближе всего к сопернику. Параллельно с этим производится оценка хода боя, и во вторую очередь рассчитывается поведение юнитов, которые могут наиболее оптимально решить исход битвы (нападает пехота — вызываются танки) и так далее. 

То есть в данном случае мы должны правильно указать приоритеты расчетов, которые расставляются в зависимости от расстояния до противника, а также от типа юнитов противника. 


Кристофер

Перепечатка материалов или их фрагментов возможна только с согласия автора.







Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Ассоциация боевых роботов
Рекомендуем...
Новости

Разделы

Опросы

Какой язык программирования вы считаете наиболее актуальным сегодня?
Всего ответов: 329

Друзья

3D-кино






Найти на сайте:








Об авторе       Контакты      Вопрос-ответ        Хостинг от uCoz