Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes to render thread-safe the package, with test. #69

Merged
merged 19 commits into from
Jun 11, 2024

Conversation

fpl
Copy link
Contributor

@fpl fpl commented Nov 21, 2023

This patch closes #58 . Even removed the string_pointer use which is obsolete since FFI::Platypus 0.61. Added a t/threads.t test to verify that multi-threading does not crash.

Basically, CPLPushErrorHandler() must be performed per thread, not at new() time for the whole program, that is after creation of any thread. That implies that there are two possibilities:

  • avoid the singleton pattern, so perform the Geo::GDAL::FFI initialization for any thread after its creation.
  • separate error handling initialization from general initialization.

This patch follows the second approach. Note the use of sticky mode for closure() as required in the documentation.
Not sure the DESTROY block makes sense, but it seems it does not hurt.

@shawnlaffan
Copy link
Collaborator

Works for me on with Strawberry Perl 5.38, and unthreaded perl 5.36 on Ubuntu.

t/open.t is now noisier although the tests pass.

t/open.t ............... ERROR 4: `test.shp' not recognized as a supported file format.
t/open.t ............... ok

@shawnlaffan
Copy link
Collaborator

It seems the VERBOSE_ERROR output in that test is going straight to stderr.

@fpl
Copy link
Contributor Author

fpl commented Nov 22, 2023

It seems the VERBOSE_ERROR output in that test is going straight to stderr.

That's because now

 Geo::GDAL::FFI->get_instance->SetErrorHandling;

should be used before any other thing. That is the side effect of removing it in the new() sub for thread safety.
A proper way to manage this kind of thing is hiding all those details in a new one-line init() sub to be used just before any other method as

Geo::GDAL::FFI->init();

That could be safely called in single/main threads or even in any other threads. In practice, a little change in the API.
Sorry, but I cannot see any other way to deal with this sort of thing.

Of course if this change is acceptable, I can append it to this PR, and properly change documentation (and propose a version change to track the API modification).

@shawnlaffan
Copy link
Collaborator

Having an explicit init call might be a reasonable thing to do.

Alternately the new method could be refactored to not do any of the function bindings, instead only setting the error handler and any other thread specific properties that come up. I think it would still need a singleton pattern, though, as there should be only one error handler per thread.

Any of the above changes would be separate to this PR.

@ajolma - what do you think?

@ajolma
Copy link
Owner

ajolma commented Nov 23, 2023

I haven't followed this much so I don't have a very educated opinion.

One this is that those changes to the attach method calls should be done to build-tools/parse_h.pl, which is used to create them by running against GDAL sources.

@shawnlaffan
Copy link
Collaborator

parse_h.pl is changed as part of the first commit.

@fpl
Copy link
Contributor Author

fpl commented Nov 23, 2023

One this is that those changes to the attach method calls should be done to build-tools/parse_h.pl, which is used to create them by running against GDAL sources.

Talking about that, even issue #53, which is managed by PR #68, would require an extensive change to the same build tool, as explained in the same issue.

@fpl
Copy link
Contributor Author

fpl commented Nov 23, 2023

Having an explicit init call might be a reasonable thing to do.

Alternately the new method could be refactored to not do any of the function bindings, instead only setting the error handler and any other thread specific properties that come up. I think it would still need a singleton pattern, though, as there should be only one error handler per thread.

Any of the above changes would be separate to this PR.

@ajolma - what do you think?

IMHO, the new() method does the right thing as a singleton. The only setup that needs to be moved out of the BEGIN block is the GDAL error handling that needs to be performed per thread instead. As a general pattern, any lib binding should have a double-step initialization, one for the general setup and one per thread, to ensure thread safety. Other packages do not have a general setup and require multiple instances (one for each thread) to work. In this case, I think it's a bit too heavy loading all the FFI stuff for every thread by removing the singleton constraint at all, if that is what you suggest.

@shawnlaffan
Copy link
Collaborator

IMHO, the new() method does the right thing as a singleton.

The issue is that this would be a breaking change for existing code.

The FFI attach calls only need to be done once when the module is loaded, so they can be refactored out of the new sub. The new sub could then be called per-thread and set the error handling as it does now. This means it is essentially the init call.

Unless of course there are complications due to threads...

@fpl
Copy link
Contributor Author

fpl commented Nov 25, 2023

IMHO, the new() method does the right thing as a singleton.

The issue is that this would be a breaking change for existing code.

The FFI attach calls only need to be done once when the module is loaded, so they can be refactored out of the new sub. The new sub could then be called per-thread and set the error handling as it does now. This means it is essentially the init call.

Unless of course there are complications due to threads...

Are you proposing to move the all and only the problematic thread-unsafe stuff in new() (to be called per thread) and keeping all the rest in a new sub init(), to be called in the BEGIN block to initialize the $instance singleton var?

@fpl fpl mentioned this pull request Nov 25, 2023
@fpl
Copy link
Contributor Author

fpl commented Dec 15, 2023

@shawnlaffan It would be nice if someone could review my proposal to split the initialization code into two parts, one for the BEGIN section and one to be called per thread. Of course it would represent a change in the current API, but I can't see any other way to ensure thread safety.

@shawnlaffan
Copy link
Collaborator

The issue is that the changes break the current API and now non-threaded systems do not have an error handler set by default.

I wonder if the new call should generate an object that keeps track of which thread it is in. Then it can set the error handler if the thread field is undef. That might need a lot more engineering, though, depending on how GDAL does things.

@fpl
Copy link
Contributor Author

fpl commented Dec 19, 2023

The issue is that the changes break the current API and now non-threaded systems do not have an error handler set by default.

I wonder if the new call should generate an object that keeps track of which thread it is in. Then it can set the error handler if the thread field is undef. That might need a lot more engineering, though, depending on how GDAL does things.

That's my fault: RTFM applies as often in a dev life.

I think a better approach would be de-registering the error handler in a CLONE_SKIP sub and re-registering per thread in a CLONE sub. That would render the module thread-safe and avoid the need to change API. Ok, I'm a silly dumb, guys...
I'll change the PR and make some checks, but this approach seems the most reasonable.

shawnlaffan and others added 5 commits December 19, 2023 18:12
Also generate two features to ensure the
write works properly.  Otherwise a single
feature was being written but followed
by a failure.
Refactor the expected structure in the process.
… disabling GDAL error hanlding before creating new threads and re-enabling in each thread after.

No change is required in exisiting code. Just in case of multi-threading
the main thread needs to issue a SetErrorHandling() once after creating
all required threads just before using GDAL again.
@fpl
Copy link
Contributor Author

fpl commented Dec 19, 2023

Ok, this PR uses a CLONE_SKIP to disable GDAL error handling in the main thread, just before creating a new thread, then re-enables it in the created thread after within a CLONE sub. The existing code requires no API change if it is a single-thread application. The only change is that in multi-threaded apps (which never worked until now), the main thread needs to re-enable GDAL error handling before using GDAL and after the creation of all its threads (CLONE_SKIP is called at every thread creation). An alternative would be returning 1 in CALL_SKIP and forcing the creation of a new() instance in every thread before using GDAL. I have a slight preference for this approach instead, because it is less impacting on performance.

@shawnlaffan
Copy link
Collaborator

This passes tests on a local perl 5.38.2 built with ithreads.

@fpl
Copy link
Contributor Author

fpl commented Dec 20, 2023

I would add a note into the FFI pod about re-enabling error handling in the main thread after thread pool creation, if acceptable. That would complete the PR.

@fpl
Copy link
Contributor Author

fpl commented Jan 4, 2024

Add a note in the FFI.pm POD section as said.

@fpl
Copy link
Contributor Author

fpl commented Jun 8, 2024

Hi there, any hope to see this PR considered for next release? This patch is much less intrusive and solves the support of multi-threading app without big problems for existing code (which I guess do not use threads, else it would simply not work at all, as was mine). Even, the drop of the obsolete StringPointer module using is simply a due act and solves even memory leaks under the same MTs conditions and possibly other situations.
Thanks for reviewing, just in case.

@ajolma
Copy link
Owner

ajolma commented Jun 10, 2024

Sure, it's just that I don't actively work on this and have no regular timeslots dedicated for this. @shawnlaffan, I believe you have full rights on this repo and on this distribution on CPAN, so you can make releases too.

@shawnlaffan
Copy link
Collaborator

@ajolma - I have commit privileges to this repo but I don't think I have PAUSE access to upload to CPAN.

lib/Geo/GDAL/FFI.pm Outdated Show resolved Hide resolved
lib/Geo/GDAL/FFI.pm Outdated Show resolved Hide resolved
lib/Geo/GDAL/FFI.pm Outdated Show resolved Hide resolved
lib/Geo/GDAL/FFI.pm Outdated Show resolved Hide resolved
@shawnlaffan
Copy link
Collaborator

@fpl - I've added some nit-picky comments to the non-code components above. I'll have a closer look at the code itself soon.

@ajolma
Copy link
Owner

ajolma commented Jun 11, 2024

@ajolma - I have commit privileges to this repo but I don't think I have PAUSE access to upload to CPAN.

You're a co-maintainer of Geo::GDAL::FFI* modules.

@shawnlaffan
Copy link
Collaborator

@fpl - the code looks good but unfortunately the recent merger of #70 is now causing merge conflicts.

Are you able to resolve them? Ideally the commits in this PR would also be rebased on the current master branch and force-pushed but I can also do that at merge time if needed.

@fpl
Copy link
Contributor Author

fpl commented Jun 11, 2024

Ok, I rebased on master, and my branch is now c.lean. Unfortunately I have no write-access here, so you should solve the remaining conflict yourself, it is immediate.

@shawnlaffan shawnlaffan merged commit 1d6fa91 into ajolma:master Jun 11, 2024
6 checks passed
@shawnlaffan
Copy link
Collaborator

Thanks @fpl - changes merged. I'll make a few other changes and cut a new release to CPAN.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Thread safety issue.
3 participants