[Rd] [External] most robust way to call R API functions from a secondary thread

Tierney, Luke |uke-t|erney @end|ng |rom u|ow@@edu
Mon May 20 19:29:50 CEST 2019


Your analysis looks pretty complete to me and your solutions seems
plausible.  That said, I don't know that I would have the level of
confidence yet that we haven't missed an important point that I would
want before going down this route.

Losing stack checking is risky; it might be eventually possible to
provide some support for this to be handled via a thread-local
variable. Ensuring that R_ToplevelExec can't jump before entering the
body function would be a good idea; if you want to propose a patch we
can have a look.

Best,

luke

On Sun, 19 May 2019, Andreas Kersting wrote:

> Hi,
>
> As the subject suggests, I am looking for the most robust way to call an (arbitrary) function from the R API from another but the main POSIX thread in a package's code.
>
> I know that, "[c]alling any of the R API from threaded code is ‘for experts only’ and strongly discouraged. Many functions in the R API modify internal R data structures and might corrupt these data structures if called simultaneously from multiple threads. Most R API functions can signal errors, which must only happen on the R main thread." (https://cran.r-project.org/doc/manuals/r-release/R-exts.html#OpenMP-support)
>
> Let me start with my understanding of the related issues and possible solutions:
>
> 1) R API functions are generally not thread-safe and hence one must ensure, e.g. by using mutexes, that no two threads use the R API simultaneously
>
> 2) R uses longjmps on error and interrupts as well as for condition handling and it is undefined behaviour to do a longjmp from one thread to another; interrupts can be suspended before creating the threads by setting R_interrupts_suspended = TRUE; by wrapping the calls to functions from the R API with R_ToplevelExec(), longjmps across thread boundaries can be avoided; the only reason for R_ToplevelExec() itself to fail with an R-style error (longjmp) is a pointer protection stack overflow
>
> 3) R_CheckStack() might be executed (indirectly), which will (probably) signal a stack overflow because it only works correctly when called form the main thread (see https://cran.r-project.org/doc/manuals/r-release/R-exts.html#Threading-issues); in particular, any function that does allocations, e.g. via allocVector3() might end up calling it via GC -> finalizer -> ... -> eval; the only way around this problem which I could find is to adjust R_CStackLimit, which is outside of the official API; it can be set to -1 to disable the check or be changed to a value appropriate for the current thread
>
> 4) R sets signal handlers for several signals and some of them make use of the R API; hence, issues 1) - 3) apply; signal masks can be used to block delivery of signals to secondary threads in general and to the main thread while other threads are using the R API
>
>
> I basically have the following questions:
>
> a) Is my understanding of the issues accurate?
> b) Are there more things to consider when calling the R API from secondary threads?
> c) Are the solutions proposed appropriate? Are there scenarios in which they will fail to solve the issue? Or might they even cause new problems?
> d) Are there alternative/better solutions?
>
> Any feedback on this is highly appreciated.
>
> Below you can find a template which, combines the proposed solutions (and skips all non-illustrative checks of return values). Additionally, R_CheckUserInterrupt() is used in combination with R_UnwindProtect() to regularly check for interrupts from the main thread, while still being able to cleanly cancel the threads before fun_running_in_main_thread() is left via a longjmp. This is e.g. required if the secondary threads use memory which was allocated in fun_running_in_main_thread() using e.g. R_alloc().
>
> Best regards,
> Andreas Kersting
>
>
>
> #include <Rinternals.h>
> #include <pthread.h>
> #include <signal.h>
> #include <stdint.h>
>
> extern uintptr_t R_CStackLimit;
> extern int R_PPStackTop;
> extern int R_PPStackSize;
>
> #include <R_ext/libextern.h>
> LibExtern Rboolean R_interrupts_suspended;
> LibExtern int R_interrupts_pending;
> extern void Rf_onintr(void);
>
> // mutex for exclusive access to the R API:
> static pthread_mutex_t r_api_mutex = PTHREAD_MUTEX_INITIALIZER;
>
> // a wrapper arround R_CheckUserInterrupt() which can be passed to R_UnwindProtect():
> SEXP check_interrupt(void *data) {
>  R_CheckUserInterrupt();
>  return R_NilValue;
> }
>
> // a wrapper arround Rf_onintr() which can be passed to R_UnwindProtect():
> SEXP my_onintr(void *data) {
>  Rf_onintr();
>  return R_NilValue;
> }
>
> // function called by R_UnwindProtect() to cleanup on interrupt
> void cleanfun(void *data, Rboolean jump) {
>  if (jump) {
>    // terminate threads cleanly ...
>  }
> }
>
> void fun_calling_R_API(void *data) {
>  // call some R API function, e.g. mkCharCE() ...
> }
>
> void *threaded_fun(void *td) {
>
>  // ...
>
>  pthread_mutex_lock(&r_api_mutex);
>
>  // avoid false stack overflow error:
>  intptr_t R_CStackLimit_old = R_CStackLimit;
>  R_CStackLimit = -1;
>
>
>  // R_ToplevelExec() below will call PROTECT 4x:
>  if (R_PPStackTop > R_PPStackSize - 4) {
>    // ppstack would overflow in R_ToplevelExec() -> handle this ...
>  }
>
>  // avoid longjmp to different thread:
>  Rboolean ok = R_ToplevelExec(fun_calling_R_API, (void *) &some_data);
>
>  // re-enable stack size checking:
>  R_CStackLimit = R_CStackLimit_old;
>  pthread_mutex_unlock(&r_api_mutex);
>
>  if (!ok) {
>    // handle error ...
>  }
>
>  // ...
> }
>
> SEXP fun_running_in_main_thread() {
>
>  // ...
>
>  /* create continuation token for R_UnwindProtect():
>   *
>   * do this explicitly here before the threads are created because this might
>   * fail in allocation or with pointer protection stack overflow
>   */
>  SEXP cont = PROTECT(R_MakeUnwindCont());
>
>  /* block all signals:
>   *
>   * do this before the threads are created such that they inherit the mask
>   */
>  sigset_t block_set, prev_mask;
>  sigfillset(&block_set);
>  pthread_sigmask(SIG_SETMASK, &block_set, &prev_mask);
>
>  // suspend interrupts:
>  Rboolean __oldsusp__ = R_interrupts_suspended;
>  R_interrupts_suspended = TRUE;
>
>  // create threads running threaded_fun() ...
>
>  for(;;) {
>    // timed blocking check if threads are done ...
>
>    // unblock signals, check for interrupts and run cleanfun if there is one:
>    pthread_mutex_lock(&r_api_mutex);
>    pthread_sigmask(SIG_SETMASK, &prev_mask, NULL);
>
>    R_interrupts_suspended = __oldsusp__;
>    if (R_interrupts_pending && ! R_interrupts_suspended) {
>      R_UnwindProtect(my_onintr, NULL, cleanfun, (void *) clean_data, cont);
>    }
>
>    R_UnwindProtect(check_interrupt, NULL, cleanfun, (void *) clean_data, cont);
>
>    R_interrupts_suspended = TRUE;
>
>    pthread_sigmask(SIG_SETMASK, &block_set, NULL);
>    pthread_mutex_unlock(&r_api_mutex);
>  }
>
>  // now all threads are dead
>
>  UNPROTECT(1);  // continuation token
>
>  // unblock signals:
>  pthread_sigmask(SIG_SETMASK, &prev_mask, NULL);
>
>  // reset interrupt-suspension:
>  R_interrupts_suspended = __oldsusp__;
>  if (R_interrupts_pending && ! R_interrupts_suspended) {
>    Rf_onintr();
>  }
>
>  // ...
> }
> ______________________________________________
> R-devel using r-project.org mailing list
> https://stat.ethz.ch/mailman/listinfo/r-devel
>

-- 
Luke Tierney
Ralph E. Wareham Professor of Mathematical Sciences
University of Iowa                  Phone:             319-335-3386
Department of Statistics and        Fax:               319-335-3017
    Actuarial Science
241 Schaeffer Hall                  email:   luke-tierney using uiowa.edu
Iowa City, IA 52242                 WWW:  http://www.stat.uiowa.edu


More information about the R-devel mailing list