Практичні прийоми використання багатопоточних обчислень при роботі з 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() +
"\пЗадача в паралельному режимі оброблялася - " + 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() +
"\пЗадача в послідовному режимі оброблялася - " + 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" це готовий макрос для тестів.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.