Does CAPI support multi-threading?

When outputting Layout, I want to generate model viewports in multiple threads.

SDK_version: SDK_WIN_x64_2022-0-354
Source Code: source_code.zip (2.1 KB)
Test file:test3.skp

I got exception from SketchupViewerAPI.dll

Unhandled exception at 0x00007FFFE69EBBF5(SketchUpViewerAPI.dll)(SketchUpTest.exe):There is a conflict when read Address 0xFFFFFFFFFFFFFFFF.

image

#include<LayOutAPI\layout.h>
#include<SketchUpAPI\sketchup.h>
#include<assert.h>
#include<string>
#include<cstdio>
#include<vector>
#include"threadpool.h"

#define SU(api_function_call) {\
  SUResult su_api_result = api_function_call;\
  assert(SU_ERROR_NONE == su_api_result);\
}\

std::mutex _lockAddEntity;

int main() {
	std::string skp_file = "F:/Desktop/relation_skp/test3.skp";

	LOInitialize();
	LODocumentRef document = SU_INVALID;
	SU(LODocumentCreateEmpty(&document));

	LOLayerRef layer = SU_INVALID;
	SU(LODocumentGetLayerAtIndex(document, 0, &layer));

	LOPageRef page = SU_INVALID;
	SU(LODocumentGetPageAtIndex(document, 0, &page));

	ThreadPool pool(4);

	std::vector<std::future<int>> executors;
	// test3.skp have 43 pages.  page1 - page43
	int num_of_out_page = 43; // It's ok when num_of_out_page=4.
	for (int i = 0; i < num_of_out_page; i++) {
		executors.emplace_back(pool.enqueue([&document, skp_file, i] {

			std::string page_name = "page";
			char num_str[3] = { 0 };
			itoa(i + 1, num_str, 10);
			page_name.append(num_str);

			LOAxisAlignedRect2D rect;  // (0,0)  (5,5)
			rect.upper_left.x = rect.upper_left.y = 0;
			rect.lower_right.x = rect.lower_right.y = 5;
			LOSketchUpModelRef model = SU_INVALID;

			SUResult res = LOSketchUpModelCreate(&model, skp_file.c_str(), &rect);
			if (res == SU_ERROR_NONE) {
				std::unique_lock<std::mutex> lock{ _lockAddEntity };  // lock AddEntity

				res = LODocumentAddEntityUsingIndexes(document, LOSketchUpModelToEntity(model), 0, 0);
				if (res == SU_ERROR_NONE) {
					res = LOSketchUpModelSetCurrentScene(model, i + 1);
					if (res == SU_ERROR_NONE)
						printf("%d %s ok\n", i, page_name.c_str());
					else
						printf("%d %s SetScene Error %d\n", i, page_name.c_str(), res);
				}
				else
					printf("%d %s AddEntity Error %d\n", i, page_name.c_str(), res);

				lock.unlock();  // unlock AddEntity

				SU(LOSketchUpModelRelease(&model));  // release SketchUpModel
			}
			else {
				printf("%d %s Create Error %d\n", i, page_name.c_str(), res);
			}
			return i;
		}));
	}

	// main thread wait all executor
	for (auto && executor : executors)
		executor.get();

	SU(LODocumentSaveToFile(document, "F:/Desktop/relation_skp/test.layout", LODocumentVersion_Current));
	SU(LODocumentRelease(&document));

	LOTerminate();
	printf("finish\n");
	return 0;
}
1 Like

We cannot translate Chinese from an image. Please post an English translation of the exception message.

Is skp_file the same for all viewports ?

How would multiple threads be able to read from the same file (or it’s representation in memory) and be able to write to the same memory representation of a LayOut document, at the same time ?


Also I do not see that you’ve added the viewport to the LayOut document within the loop.
See: LODocumentAddEntity()

Generally, with the LayOut APIs, the entity needs to be first attached to a document before it’s properties can be changed. Otherwise the entity’s properties get set to default values.

Unhandled exception at 0x00007FFFE69EBBF5(SketchUpViewerAPI.dll)(SketchUpTest.exe):There is a conflict when read Address 0xFFFFFFFFFFFFFFFF.
SketchUpTest is my program.

Yes, It’s same file.

In my imagination, multithreaded reading should be ok, because it does not involve writing.

If the same file exists in SketchupViewAPI.dll, and the SKP file is parsed only once and there is an operation to write to memory, the API may not be applicable to multiple threads.

You are right. My focus is on the LOSketchUpModelCreate.
And I want test it, because the function is time-consuming.

I update.

What an interesting question you have asked. While I doubt that the API was intended to be used in this manner, I find that it is possible to create the LOSketchupModelRefs in parallel, greatly reducing the time needed to produce the layout file. I fully expect someone from SU to say this is a bad idea!

The code below seems to work with ‘modern’ Sketchup files while failing for older versions.

#include<LayOutAPI\layout.h>
#include <assert.h>
#include<string>
#include<cstdio>
#include<vector>
#include"threadpool.h"
//#include <conio.h>

#define SU(api_function_call) {\
  SUResult su_api_result = api_function_call;\
  assert(SU_ERROR_NONE == su_api_result);\
}\

int main() {
	std::string skp_file = "C:/Users/user/Desktop/test3.skp";

	LOInitialize();

	// Create a vector of LOSketchUpModelRefs using a thread pool
	// 
	int num_of_out_page = 25;
	std::vector<LOSketchUpModelRef> models(num_of_out_page, SU_INVALID);

	ThreadPool pool(4);
	std::vector<std::future<int>> executors;

	for (int i = 0; i < num_of_out_page; i++) { 
		printf("Queueing: %d\n", i);

		LOSketchUpModelRef*  model = &models[i];
		executors.emplace_back(pool.enqueue([ model, skp_file,  i] {
			
			LOAxisAlignedRect2D rect;  // (0,0)  (5,5)
			rect.upper_left.x = rect.upper_left.y = 0.2;
			rect.lower_right.x = 10.8;
			rect.lower_right.y = 7.8;

			SUResult res = LOSketchUpModelCreate(model, skp_file.c_str(), &rect);
			if (res == SU_ERROR_NONE) {
				printf("Created %d\n", i);
			}
			else
			{
				printf("%d Create Error %d\n", i, res);
			}
			return i;
			}));
	}

	// main thread wait all executors
	for (auto&& executor : executors)
		executor.get();

	// It is informative to stop here and look at the memory use.
	// To do so, uncomment these lines and #include <conio.h>
	// In Visual Studio open Debug>Windows>Diagnostic Tools
	// 
	//printf("The Models have been created. Press any key to continue.");
	//int chr = _getch();
	//printf("\n");

	// Add models to the Layout document
	LODocumentRef document = SU_INVALID;
	SU(LODocumentCreateEmpty(&document));

	for (int i = 0; i < num_of_out_page; i++) {

		LOPageRef page_ref = SU_INVALID;
		if (i < 1)
			LODocumentGetPageAtIndex(document, i, &page_ref);
		else
			LODocumentAddPage(document, &page_ref);

		// Set the page_ref name here!!!

		SUResult res;
		res = LOSketchUpModelSetCurrentScene(models[i], i + (size_t)1);
		if (res == SU_ERROR_NONE)
		{
			//printf("Scene Set: %d\n", i);
		}
		else
		{
			printf("SetScene Failed: %d\n", i);
		}

		res = LODocumentAddEntityUsingIndexes(document, LOSketchUpModelToEntity(models[i]), 0, i);
		if (res == SU_ERROR_NONE) 
		{
			printf("Added: %d\n", i);
		}
		else
		{
			printf("Add Failed: %d\n", i);
		}
	}

	SU(LODocumentSaveToFile(document, "C:/Users/user/Desktop/test3.layout", LODocumentVersion_Current));
	SU(LODocumentRelease(&document));
	
	//for (int i = 0; i < num_of_out_page; i++) { SU(LOSketchUpModelRelease(&models[i])); }  // release SketchUpModel

	LOTerminate();
	printf("Completed\n");
	return 0;
}

1 Like

Thank you. It’s work. But I still want to know why my writing will collapse.

LODocumentRef doc_ref = SU_INVALID;
LODocumentCreateEmpty(&doc_ref);
LORectangleRef rectangle_ref = SU_INVALID;
LOAxisAlignedRect2D rect_bounds = {{1.0, 2.0}, {4.0, 3.0}};
LORectangleCreate(&rectangle_ref, &rect_bounds);
// rectangle_ref now has a reference count of 1.
LOEntityRef entity_ref = LORectangleToEntity(rectangle_ref);
LODocumentAddEntityUsingIndexes(doc_ref, entity_ref, 0 /* layer */, 0 /* page */);
// The document now holds one or more internal references to rectangle_ref, so
// its reference count will be greater than 1.
LORectangleRelease(&rectangle_ref);
// rectangle_ref is still valid at this point, because the document still holds
// one or more references to it.
LODocumentRemoveEntity(doc_ref, &entity_ref);
// rectangle_ref is now invalid. Removing it from the document caused its
// reference count to reach 0, so the object was deleted.
LODocumentRelease(&doc_ref);

And why not release SketchUpModelView ?

I find you tried to release it.

High efficiency is too important.

In general, it is not safe for multiple threads to simultaneously read from and write to the same file or memory representation of a LayOut document without proper synchronization. This can lead to race conditions, data corruption, and other errors.

To avoid these issues, synchronization mechanisms such as locks, semaphores, or other concurrency primitives should be used to coordinate access to the shared resource. For example, a mutex lock can be used to ensure that only one thread at a time can read from or write to the file or memory representation. Alternatively, if the application is designed for high-concurrency scenarios, a more advanced synchronization mechanism such as a reader-writer lock or a concurrent data structure may be used to allow multiple threads to simultaneously read from the same resource while still ensuring data integrity.

It is important to note that implementing concurrent file or memory access can be complex and requires careful design and testing to ensure correctness and performance. Additionally, the choice of synchronization mechanism will depend on the specific requirements and characteristics of the application.

3 Likes

This topic has been discussed in the past …

2 Likes

Wow, all of this is way above my basic understanding, but if I got the basics of this, this sounds like it could dramatically improve Layout’s updating references performance.

One work around the issue of multiple threads reading the same data, might be to first make temporary copies of the master SketchUp file into RAM so each thread gets it’s own high speed copy. I have a 64GB Ram system and always have plenty of free RAM available. The next alternative, might be to use either a temporary location on the hard drive (preferably a solid state hard drive) or use the operating systems Pagefile mechanism.

Just some non-expert thoughts from a user that really really wants better Layout performance badly.

1 Like

Okay, but we are not discussing the LayOut application here. This is about the C API that either runs within a 3rd-party application or runs within SketchUp. Either way, it’s a .layout file read and write API only.

The process is about to terminate and all of the memory will be released when it does so. If this code is to be part of a Ruby C Extension then you’ll need to release the SketchUpModelView objects.

Back to why your initial attempt would fail. Did you notice that the process would seg_fault as soon as a thread was reused? You did mention that queueing four requests worked just fine in a pool of four threads but the fifth would crash. I’d image that the SketchUpAPI is doing something akin to the Ruby OR EQUAL operator at the startup of a new call to LODocumentAddEntityUsingIndexes which, when the thread is reused, would fail to reinitialize the value appropriately.

value  ||= some_initial_value

If you become ambitious you could rewrite the threadpool to start new threads instead of reusing the old ones.

Also a big thank you to @DanRathbun for digging up the previous discussion of this topic.

2 Likes

Thank you.

Creating entities that are not attached to a model might be fine, but I would not expect manipulating a model from multiple threads to work reliably.

1 Like