Практические приемы использования многопоточных вычислений при работе с Revit API

в 12:13, , рубрики: api, C#, CAD/CAM, Revit API

Я архитектор, долгое время проектировал здания и сооружения, но вот с лета прошлого года начал программировать на C# используя Revit API. У меня уже есть несколько модулей-надстроек для Revit и теперь я хочу поделиться некоторым опытом разработки для Revit. Предполагается, что читатели умеют писать макросы для Revit на C#.

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

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

Если мы будем работать непосредственно с объектами Revit API (в данном случае с Wall) нам придется обрабатывать каждую стену последовательно, соответственно такие вычисления будут происходить в одном потоке. Попробуем выделить необходимые свойства стен, добавить их в собственные классы и провести необходимые вычисления уже с собственными классами, в нескольких потоках.

Сначала создадим макрос WallTesting. И теперь создадим класс, который будет получать необходимые для работы свойства объектов стен.

public class MyWall //Класс в который добавим необходимые параметры из элементов Wall
		{    
		    private LocationCurve wallLine; //Добавляем линию основание стены, она понадобится для вычислений
		    private XYZ p1 = new XYZ(); //Добавляем первую точку линии основания
		    private XYZ p2 = new XYZ();    //Добавляем вторую точку основания линии стены
		    public XYZ pCenter; //Добавляем среднюю точку стены, которую будем вычислять, но не задаем начальное значение   
		    
		    public MyWall (LocationCurve WallLine)     //Конструктор пользовательского класса стены
		    {
		        this.wallLine = WallLine;            //Делаем в конструкторе минимум работы - просто передаем нужные нам параметры для вычислений        		                
		    }    
		    public XYZ GetPointCenter () //Метод, вычисляющий среднюю точку стены
		    	//Тут можно бы сделать кэширование срединной точки, и не вычислять ее - если точка известна, но для упрощения кода не будем делать этого.
		    {
				p1 = wallLine.Curve.GetEndPoint(0);
		        
		        p2 = wallLine.Curve.GetEndPoint(1);
		        
		        return pCenter = new XYZ((p2.X + p1.X)/2, (p2.Y + p1.Y)/2, (p2.Z + p1.Z)/2);//Тут немножко вспоминаем векторную геометрию за 9 класс школы		            	
		    }    
		    public double GetLenght (XYZ x) //Метод, вычисляющий расстояние до предложенной ему средней точки другой стены
		    {	            	
		        XYZ vector = new XYZ((pCenter.X - x.X), (pCenter.Y - x.Y), (pCenter.Z - x.Z)); //Находим вектор между средней точкой первой стены и второй стены
		                      
		        return Math.Sqrt ( Math.Pow( vector.X,2)  + Math.Pow( vector.Y,2) + Math.Pow( vector.Z,2)); //Находим длину вектора между средними точками двух стен      
		    }    
		}

В общем состав класса и его работа расписана в комментариях, можно теперь написать главный рабочий метод WallTesting.

public void WallTesting ()
{
    UIDocument uidoc = this.ActiveUIDocument; //Получаем активный документ
    Document doc = uidoc.Document; //Создаем документ
    
    List<MyWall> wallList = new List<ThisApplication.MyWall>(); //Добавляем вспомогательные члены - список в который перенесем необходимые свойства из элементов Wall
    List <double> minPoints = new List<double>(); //В этом списке будут храниться минимальные расстояния от каждой стены    
    
    Selection selection = uidoc.Selection; // получаем выделенные объекты
    
    ICollection<ElementId> selectedIds = uidoc.Selection.GetElementIds(); //и помещаем их в коллекцию
    
    DateTime end; //Далее проверим как будет работать наша вычисления в многопоточном режиме
        
    DateTime start = DateTime.Now; //Засекаем время
    
    foreach (ElementId e in selectedIds) // Помещаем необходимые свойства элемента Wall в свои объекты MyWall.
    //Эту операцию нельзя выполнять в многопоточном режиме, так как мы пока работаем с объектами Revit API               
    {
        Element el = doc.GetElement(e); //получаем элемент по его Id
        Wall w = el as Wall; //Смотрим, стена ли это
        if (w != null) //Если стена - 
        {
            wallList.Add( new MyWall (w.Location as LocationCurve)); // Создаем объект MyWall и добавляем в его необходимые свойства из элемента Wall
        }
    }

    
    System.Threading.Tasks.Parallel.For(0, wallList.Count, x => //Далее будем последовательно у каждого объекта MyWall сравнивать 
     //расстояние от средней точки до средней точки всех остальных объектов (стен). Запускаем задачу в параллельном режиме
    {
            List <double> allLenght = new List<double>(); //Это вспомогательный список
            wallList[x].GetPointCenter(); //Находим срединную точку текущего объекта
        
        foreach (MyWall nn in wallList) //проверяем расстояние до каждой срединной точки остальных объектов(стен)
        {   
            double n = wallList[x].GetLenght( nn.GetPointCenter() );
            if (n != 0) //Исключаем добавление в список текущего объекта
                allLenght.Add(n); //И записываем все расстояния в этот вспомогательный список
        } 
        allLenght.Sort(); //Сортируем вспомогательный список
        
        minPoints.Add(allLenght[0]); //Добавляем наименьшее расстояние в соответствующий список
     });//Заканчиваем задачу           
    
    minPoints.Sort(); //Сортируем все минимальные расстояния
    
    double minPoint = minPoints[0]; //Берем самое маленькое расстояние между стенами
    
        end = DateTime.Now; // Записываем текущее время
        
        TimeSpan ts = (end - start);              
    
        TaskDialog.Show("Revit", "Минимальное расстояние между стенами - " + (minPoint*304.8).ToString() +
                        "nЗадача в параллельном режиме обрабатывалась - "  + ts.TotalMilliseconds.ToString() + " миллисекунд");
        		        
}

Уже можно запустить макрос и посмотреть как он шустро находит минимальное расстояние между средними точками стен, но все же немного потерпим и добавим метод WorkWithWall, который будет работать с объектами Revit API со стенами (Wall) и будет обрабатывать их последовательно. Код не комментирую — в нем методы и параметры аналогичны приведенным выше.

void WorkWithWall(Document doc, ICollection<ElementId> selectedIds)
{
	List<Wall> wallList = new List<Wall>();
    List <double> minPoints = new List<double>();  
	 
	DateTime end;              
    DateTime start = DateTime.Now;
    
    foreach (ElementId e in selectedIds)               
    {
        Element el = doc.GetElement(e);
        Wall w = el as Wall; 
        if (w != null) 
        {
        	wallList.Add(w);
        }                
    } 
    
    foreach (Wall w in wallList)
    {
    	List <double> allLenght = new List<double>();
    	LocationCurve wallLine = w.Location as LocationCurve;
    	XYZ pCenter =
    		new XYZ((wallLine.Curve.GetEndPoint(1).X + wallLine.Curve.GetEndPoint(0).X)/2, 
    		        (wallLine.Curve.GetEndPoint(1).Y + wallLine.Curve.GetEndPoint(0).Y)/2,
    		        (wallLine.Curve.GetEndPoint(1).Z + wallLine.Curve.GetEndPoint(0).Z)/2);
    	
    	foreach (Wall w2 in wallList)
    	{
    		LocationCurve wallLine2 = w2.Location as LocationCurve;
    		XYZ pCenter2 =
    			new XYZ((wallLine2.Curve.GetEndPoint(1).X + wallLine2.Curve.GetEndPoint(0).X)/2, 
    		        	(wallLine2.Curve.GetEndPoint(1).Y + wallLine2.Curve.GetEndPoint(0).Y)/2,
    		        	(wallLine2.Curve.GetEndPoint(1).Z + wallLine2.Curve.GetEndPoint(0).Z)/2);            		
    		
    		
    		XYZ vector = new XYZ((pCenter.X - pCenter2.X), (pCenter.Y - pCenter2.Y), (pCenter.Z - pCenter2.Z));
                              
            double lenght = Math.Sqrt ( Math.Pow( vector.X,2)  + Math.Pow( vector.Y,2) + Math.Pow( vector.Z,2));
            
            if (lenght !=0)
            	allLenght.Add(lenght);
    	}
    	allLenght.Sort();
        
        minPoints.Add(allLenght[0]);
    }
    
    minPoints.Sort();
    
    double minPoint = minPoints[0];
    
        end = DateTime.Now;
        
        TimeSpan ts = (end - start);              
    
        TaskDialog.Show("Revit", "Минимальное расстояние между стенами - " + (minPoint*304.8).ToString() +
                        "nЗадача в последовательном режиме обрабатывалась - "  + ts.TotalMilliseconds.ToString() + " миллисекунд");            		
}

Добавим в конец рабочего метода WallTesting метод вот так:

WorkWithWall(doc, selectedIds);

Теперь работа завершена, остается создать примерно 1000 стен, запустить макрос у увидеть, как он работает. Разница в параллельной и последовательной работе будет в 3 раза в пользу первой. Я не делал обработчик исключения, на тот случай, если стены не выделены перед запуском макроса. Так, что не забудьте сначала выделить стены.

image

Резюме

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

2. Если вам нужны многопоточные вычисления, сначала создайте собственные классы, в которые передайте необходимые свойства элементов Revit API и методы работы с этими свойствами. Создайте список из собственных классов и обрабатывайте его в многопоточном режиме.
Не забудьте — вы можете кэшировать некоторые свойства и члены класса, и не вычислять их, если уже раннее это когда-то было сделано.

3. Не пытайтесь в методы, работающие с разными потоками, передать объекты Revit API, например Wall. У вас возникнет ошибка времени выполнения.

PS: Приложил файлы с примерами. Это файл "Test1000Wall.rvt" в котором находится 1000 стен с расстоянием друг от друга 1000мм (в осях). Справа сверху расстояние между стенами в осях 700мм. Файл "TestParallelWall.cs" это готовый макрос для тестов.

Автор: Akunets

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js