On Thu, 30 May 2024 12:14:10 +0200
Bruno Haible wrote:
> Takashi Yano wrote in cygwin-patches:
> >  int
> >  pthread::once (pthread_once_t *once_control, void (*init_routine) (void))
> >  {
> > -  // already done ?
> > -  if (once_control->state)
> > +  /* Sign bit of once_control->state is used as done flag */
> > +  if (once_control->state & INT_MIN)
> >      return 0;
> >  
> > +  /* The type of &once_control->state is int *, which is compatible with
> > +     LONG * (the type of the first argument of InterlockedIncrement()). */
> > +  InterlockedIncrement (&once_control->state);
> >    pthread_mutex_lock (&once_control->mutex);
> > -  /* Here we must set a cancellation handler to unlock the mutex if needed 
> > */
> > -  /* but a cancellation handler is not the right thing. We need this in 
> > the thread
> > -   *cleanup routine. Assumption: a thread can only be in one pthread_once 
> > routine
> > -   *at a time. Stote a mutex_t *in the pthread_structure. if that's non 
> > null unlock
> > -   *on pthread_exit ();
> > -   */
> 
> Sorry, in a unified diff form this is unreadable. One needs to look at the
> entire function. A context diff would have been better. So:
> 
> int
> pthread::once (pthread_once_t *once_control, void (*init_routine) (void))
> {
>   /* Sign bit of once_control->state is used as done flag */
>   if (once_control->state & INT_MIN)
>     return 0;
> 
>   /* The type of &once_control->state is int *, which is compatible with
>      LONG * (the type of the first argument of InterlockedIncrement()). */
>   InterlockedIncrement (&once_control->state);
>   pthread_mutex_lock (&once_control->mutex);
>   if (!(once_control->state & INT_MIN))
>     {
>       init_routine ();
>       once_control->state |= INT_MIN;
>     }
>   pthread_mutex_unlock (&once_control->mutex);
>   if (InterlockedDecrement (&once_control->state) == INT_MIN)
>     pthread_mutex_destroy (&once_control->mutex);
>   return 0;
> }
> 
> 1) The overall logic seems right (using bit 31 of the state word as a
> 'done' bit), and the last thread that used the mutex destroys it.
> 
> 2) However, the 'state' field is not volatile, and therefore the memory
> model does not guarantee that an assignment
> 
>      once_control->state |= INT_MIN;
> 
> done in one thread has an effect on the values seen by other threads
> that do
> 
>      if (once_control->state & INT_MIN)
> 
> As long as there is no synchronization between the two CPUs that execute
> the two threads, one CPU may indefinitely see the old value of
> once_control->state, while the other CPU's new value is not being
> "broadcasted" to all CPUs.

OK. I'll add volatile attribute to state in thread.h.

> 3) Also, as noted by Noel Grandin, there is a race: If one thread does
> 
>      once_control->state |= INT_MIN;
> 
> while another thread does
> 
>      InterlockedIncrement (&once_control->state);
> or
>      InterlockedDecrement (&once_control->state)
> 
> the result can be that the increment or decrement is nullified. If it's
> an increment that gets nullified, the pthread_mutex_destroy occurs too
> early, with fatal consequences. If it's a decrement that get nullified,
> the pthread_mutex_destroy never happens, and there is therefore a resource
> leak.

That's right. I'll use InterlockedOr(&onece_control->state, INT_MIN)
here instead.

> 4) Does the test program that I posted in
> <https://cygwin.com/pipermail/cygwin/2024-May/255987.html> now pass?
> Notes:
>   - You should set
>       #define ENABLE_DEBUGGING 0
>     because with the debugging output, it nearly always succeeds.
>   - You should run it 10 times in a row, not just once. It may well
>     succeed 9 out of 10 times and fail 1 out of 10 times.

I start to run a few hours ago:
while true; do ./a.exe; done
and it does not detect any errors so far.

-- 
Takashi Yano <takashi.y...@nifty.ne.jp>

Reply via email to