Search code examples
multithreadingconcurrencycgal

Is CGAL 2D Regularized Boolean Set-Operations lib thread safe?


I am currently using the library mentioned in the title, see

CGAL 2D-reg-bool-set-op-pol

The library provides types for polygons and polygon sets which are internally represented as so called arrangements.

My question is: How far is this library thread safe, that is, fit for parallel computation on its objects?

There could be several levels in which thread safety is guaranteed:

1) If I take an object from a library like an arrangement

Polygon_set_2 S;

I might be able to execute

Polygon_2 P;
S.join(P);

and

Polygon_2 Q;
S.join(Q);

in two different concurrent execution units/threads in parallel without harm and get the right result, as if I had done everything sequentially. That would be the highest degree of thread safety/possible parallelism.

2) In fact for me a much lesser degree would be enough. In that case S and P would be members of a class C so that two class instances have different S and P instances. Then I would like to compute (say) S.join(P) in parallel for a list of instances of the class C, say, by calling a suitable member function of C with std::async

Just to be complete, I insert here a bit of actual code from my project which gives more flesh to these terse descriptions.

// the following typedefs are more or less standard from the
// CGAL library examples.


typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;
typedef Kernel::Point_2                                   Point_2;
typedef Kernel::Circle_2                                  Circle_2;
typedef Kernel::Line_2                                    Line_2;
typedef CGAL::Gps_circle_segment_traits_2<Kernel>         Traits_2;

typedef CGAL::General_polygon_set_2<Traits_2>             Polygon_set_2;
typedef Traits_2::General_polygon_2                       Polygon_2;
typedef Traits_2::General_polygon_with_holes_2            Polygon_with_holes_2;
typedef Traits_2::Curve_2                                 Curve_2;
typedef Traits_2::X_monotone_curve_2                      X_monotone_curve_2;
typedef Traits_2::Point_2                                 Point_2t;
typedef Traits_2::CoordNT                                 coordnt;
typedef CGAL::Arrangement_2<Traits_2>                     Arrangement_2;
typedef Arrangement_2::Face_handle                        Face_handle;

// the following type is not copied from the CGAL library example code but
// introduced by me
typedef std::vector<Polygon_with_holes_2> pwh_vec_t;


// the following is an excerpt of my full GerberLayer class,
// that retains only data members which are used in the join()
// member function. These data is therefore local to the class instance.

class GerberLayer
{
public:

    GerberLayer();
    ~GerberLayer();

    void join();

    pwh_vec_t raw_poly_lis;
    pwh_vec_t joined_poly_lis;

    Polygon_set_2 Saux;

    annotate_vec_t annotate_lis;

    polar_vec_t polar_lis;


};

//
// it is not necessary to understand the working of the function
// I deleted all debug and timing output etc. It is just to "showcase" some typical
// operations from the CGAL reg set boolean ops for polygons library from
// Efi Fogel et.al.
//
void GerberLayer::join()
{

    Saux.clear();

    auto it_annbase = annotate_lis.begin();

    annotate_vec_t::iterator itann = annotate_lis.begin();

    bool first_block = true;

    int cnt = 0;

    while (itann != annotate_lis.end()) {

        gpolarity akt_polar = itann->polar;

        auto itnext = std::find_if(itann, annotate_lis.end(),
                [=](auto a) {return a.polar != akt_polar;});

        Polygon_set_2 Sblock;

        if (first_block) {
            if (akt_polar == Dark) {

                Saux.join(raw_poly_lis.begin() + (itann - it_annbase),
                            raw_poly_lis.begin() + (itnext - it_annbase));
            }
            first_block = false;
        } else {
            if (akt_polar == Dark) {

                Saux.join(raw_poly_lis.begin() + (itann - it_annbase),
                                raw_poly_lis.begin() + (itnext - it_annbase));

            } else {


                Polygon_set_2 Saux1;

                Saux1.join(raw_poly_lis.begin() + (itann - it_annbase),
                                raw_poly_lis.begin() + (itnext - it_annbase));


                Saux.complement();

                pwh_vec_t auxlis;

                Saux1.polygons_with_holes(std::back_inserter(auxlis));

                Saux.join(auxlis.begin(), auxlis.end());

                Saux.complement();

            }
        }

        itann = itnext;
    }

    ende:
    joined_poly_lis.clear();

    annotate_lis.clear();

    Saux.polygons_with_holes (std::back_inserter (joined_poly_lis));

}



int join_wrapper(GerberLayer* p_layer)
{
    p_layer->join();
    return 0;
}

// here the parallelism (of the "embarassing kind") occurs:
// for every GerberLayer a dedicated task is started, which calls
// the above GerberLayer::join() function

void Window::do_unify()
{
    std::vector<std::future<int>> fivec;

    for(int i = 0; i < gerber_layer_manager.num_layers(); ++i) {
        GerberLayer* p_layer = gerber_layer_manager.at(i);

        fivec.push_back(std::async(join_wrapper, p_layer));

    }

    int sz = wait_for_all(fivec); // written by me, not shown

}

One might think, that 2) must be possible trivially as only "different" instances of polygons and arrangements are in the play. But: It is imaginable, as the library works with arbitrary precision points (Point_2t in my code above) that, for some implementation reason or other, all the points are inserted in a list static to the class Point_2t, so that identical points are represented only once in this list. So there would be nothing like "independent instances of Point_2t" and as a consequence also not for "Polygon_2" or "Polygon_set_2" and one could say farewell to thread safety.

I tried to resolve this question by googling (not by analyzing the library code, I have to admit) and would hope for an authoritative answer (hopefully positive as this primitive parallelism would greatly speed up my code).

Addendum: 1) I implemented this already and made a test run with nothing exceptional occurring and visually plausible results, but of course this proves nothing.

2) The same question for the CGAL 2D-Arrangement-package from the same authors.

Thanks in advance!

P.S.: I am using CGAL 4.7 from the packages supplied with Ubuntu 16.04 (Xenial). A newer version on Ubuntu 18.04 gave me errors so I decided to stay with 4.7. Should a version newer than 4.7 be thread-safe, but not 4.7, of course I will try to use that newer version.

Incidentally I could not find out if the libcgal***.so libraries as supplied by Ubuntu 16.04 are thread safe as described in the documentation. Especially I found no reference to the Macro-Variable CGAL_HAS_THREADS that is mentioned in the "thread-safety" part of the docs, when I looked through the build-logs of the Xenial cgal package on launchpad.


Solution

  • Indeed there are several level of thread safety. The 2D Regularized Boolean operation package depends of the 2D Arrangement package, and both packages depend on a kernel. For most operations the EPEC kernel is required. Both packages are thread-safe, except for the rational-arc traits (Arr_rational_function_traits_2). However, the EPEC kernel is not thread-safe yet when sharing number-type objects among threads. So, if you, for example, construct different arrangements in different threads, from different input sets of curves, respectively, you are safe.