Статьи

Темы в Свифте с NSOperation: Грей Скотт Диффузия Реакции

Когда я впервые открыл для себя ActionScript Workers , одним из первых, что я сделал, было использование их для реализации модели реакционной диффузионной системы Грея Скотта . Теперь, когда я начал играть со Swift, я думал, что сделаю нечто подобное с NSOperation .

Прежде чем мы начнем, большое предостережение: лучшее место для запуска больших клеточных автоматов, таких как системы реакционной диффузии, — это источник GPU. Этот пост действительно посвящен моим экспериментам с потоками Swift. Я с нетерпением жду, чтобы поработать с Металлом в ближайшее время, тогда у нас будет некоторая правильная реакция распространения продолжается. 

Как бы я ни любил AS Workers, с ними немного сложно справиться: здесь много стандартного кода, и все данные, передаваемые между работниками, должны быть сериализованы. Классы, которые расширяют NSOperation, могут передавать любой тип данных назад и вперед в основной поток пользовательского интерфейса, и для их выполнения их просто необходимо добавить в очередь, и их завершение завершенияBlock вызывается по завершении:    

   let queue = NSOperationQueue();
        [...]
        solver = GrayScottSolver();
   
        solver.setGrayScott(grayScottData);
        solver.setParameterValues(f: f, k: k, dU: dU, dV: dV)
        solver.threadPriority = 0;
        solver.completionBlock = {self.didSolve(self.solver.getGrayScott())};
       queue.addOperation(solver);

Однако, как мы увидим позже, я не использую блок завершения .

Я создал два класса операций, GrayScottSolver и GrayScottRenderer

Класс решателя принимает одномерное представление двумерной сетки. Я начал с двумерного массива (из массива формы [x] [y] ), но обнаружил, что переключение обратно в одно измерение намного быстрее. Ее функция main () перебирает каждый элемент и создает временный массив по следующей формуле :

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

Класс рендерера принимает аналогичный массив, но создает экземпляр UIImage на основе данных. Для каждого пикселя я рисую прямоугольник 1 x 1 в графическом контексте, затем вызываю  UIGraphicsGetImageFromCurrentImageContext () для заполнения UIImage. Здесь не требуется сериализация — основной поток пользовательского интерфейса может легко получить доступ к свойствам фонового потока без преобразования. 

Виды ‘u’ системы отображаются на красный и зеленый, а виды ‘v’ на синий.

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

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

В любое время обе операции могут выполняться параллельно.

Чтобы добавить некоторые специальные функции для типов CGFloat и Int , я использовал расширения Swift . Расширения, также известные как ретроактивное моделирование, позволяют разработчикам добавлять новые методы в существующие классы. Например, чтобы сжать значения CGFloat между нулем и единицей , мой класс расширения так же прост:

     func clip() -> CGFloat
    {
        if self < 0
        {
            return 0.0;
        }
        else if self > 1.0
        {
            return 1.0;
        }
        else
        {
            return self;
        }
   }

И теперь любой экземпляр
CGFloat может быть зафиксирован так:

      let foo : CGFloat = 1234.5678;
      foo.clip()

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

Просто для забавы, если бы он работал на GPU, вот экранная запись этого проекта, действительно, очень ускоренная:

Проект доступен здесь, в моем репозитории GitHub . Если вы видите какие-либо способы ускорить процесс, я бы с удовольствием их услышал!