When can a cond var be used to synchronize its own destruction/unmapping?
Asked Answered
P

3

6

According to POSIX,

It shall be safe to destroy an initialized condition variable upon which no threads are currently blocked.

Further, the signal and broadcast operations are specified to unblock one/all threads blocked on the condition variable.

Thus, it seems to me the following forms of self-synchronized destruction should be valid, i.e. calling pthread_cond_destroy:

  1. Immediately after a successful signal, in either the waiting or the signaling thread, when exactly one thread is blocked on the cond var.
  2. Immediately after a successful broadcast, in either any waiting thread or the broadcasting thread.

Of course this assumes no further waiters will arrive and no further signals shall be performed afterwards, which the application is responsible for guaranteeing if using pthread_cond_destroy.

Am I correct that destruction is valid in these situations? And are there other self-synchronized destruction scenarios to be aware of with condition variables?

Finally, for process-shared cond vars where unmapping the shared mapping without destruction might make sense, is it reasonable to expect unmapping to be valid in the same contexts destruction would be valid, or must further synchronization be performed if multiple threads in the same process (address space) are using the same mapping and want to unmap it in one of the above contexts?

Pantograph answered 29/9, 2011 at 14:0 Comment(3)
Bounty time is almost up and I really don't have any candidates to accept...Pantograph
Do you have a real program that depends on this?Frustration
I've encountered test code that asserts that it works, and I'm trying to assess whether these tests are valid and whether my implementation of condition variables needs to handle such usage.Pantograph
C
2

No, I don't think that most of your assumptions are correct. Returning from pthread_cond_signal or pthread_cond_broadcast does not indicate that any of the threads are yet "unblocked" from the condition variable, i.e that the threads that are to be unblocked don't need access to that variable anymore. The standard only says "shall unblock" and not "on successful return from this call they will be unblocked". The later would be very restrictive for implementations, so there is probably a good reason that this is formulated as it is.

So I think from the scenarios you describe only one is valid, namely the case were the solely blocked thread or process destroys the condition after being woken up.

Cismontane answered 29/9, 2011 at 18:17 Comment(11)
Interestingly, the glibc/NPTL tests (e.g. tst-cond20.c) specifically assert that the condition variable is immediately destroyable after broadcast returns. This is in contrast to NPTL's mutex and semaphore implementations whose destruction is completely impossible to synchronize.Pantograph
This may be a special feature of that implementation that they are eager to maintain, there are certainly upsides to it. But I don't think that you can or should rely on that in a general context. Maintaining that feature could really be a burden on platforms that don't have a good system support for lock structures.Cismontane
As for your answer, while I would hope from an implementation standpoint you're right, I'm hesitant to accept this explanation. Surely when the standard says memcpy "shall copy N bytes...", it means this action will be completed by the time memcpy returns. I don't see how "shall unblock (all|at least one) thread[s]" automatically involves a special relaxation regarding when the unblocking takes place...Pantograph
@R.. the relevant standard (C in this case) says for memcpy: "The memcpy function copies n characters from the object pointed to by s2 into the object pointed to by s1." No shall, no nothing it just does it. The semantics of synchronous / asynchronous events in a thread system are quite subtle (as you know :) Usually POSIX makes it quite explicit if a before / after relation between events in different threads is expected.Cismontane
Perhaps choosing a function from the C standard was a bad example. If we look at kill, which "shall send a signal...", it's later spelled out under what conditions the signal must be delivered before kill returns. Do you have any examples where something that "shall" happen clearly doesn't actually happen before the function returns?Pantograph
pthread_cancel is a good example. It says "shall request that" and afterwards it states that this is an asynchronous operation. So yes, I maintain that the POSIX standard defines very well places where requires a certain ordering or where asynchronism is permitted. Since here there is no specification you simply can't assumed anything and from an application perspective you have to be ready to deal with the worst case scenario.Cismontane
As for pthread_cancel, I'm pretty sure pthread_cancel(pthread_self()); followed by a call to any cancellation point without disabling cancellation requires cancellation to be acted upon, i.e. the cancellation request is required to be pending when pthread_cancel returns...Pantograph
Indeed, "Whenever a thread has cancelability enabled and a cancellation request has been made with that thread as the target, and the thread then calls any function that is a cancellation point (such as pthread_testcancel() or read()), the cancellation request shall be acted upon before the function returns." So the language "shall request" is not giving the implementation liberty to put the operation off until later; rather, a "request" for cancellation has formal meaning in terms of how cancellation is specified in 2.9.5.Pantograph
By the way, I hope I'm not coming across as too argumentative/stubborn here. I'm rather split on whether your answer is right or not, but I want a more rigorous argument either way. Perhaps I should get in the habit of annoying the Austin Group with interpretation requests...Pantograph
Open POSIX Test Suite test pthread_cond_destroy/2-1.c also asserts that a cond var can be immediately destroyed and clobbered after broadcast by the broadcasting thread. This should not be taken as authoritative, since I've found this test suite asserting a number of other dubious or outright-wrong claims, but it does indicate that at least somebody other than Ulrich Drepper thinks self-synchronized destruction of this form should be well-defined...Pantograph
@R.. it all ends up being a question of interpretation. So as an application programmer I would not rely on this, and always have some sort of reference count around. As an implementation programmer in turn I would go for the other side and try to implement conds such that they can be destroyed as you describe. But as things stand no side can rely on a specific behavior.Cismontane
N
0

While I agree with your interpretation (and that of the Open POSIX test suite) of this language, usage will be implementation dependent. As such, here is a quick rundown of some of the major implementations:

Safe

  • nptl - pthread_cond_destroy() will return EBUSY if there are threads still blocked on the condition. Destruction responsibility will pass to the threads that are signaled by not unblocked.
  • pthread-win32 MSVC - pthread_cond_destroy() will return EBUSY if there are threads still blocked on the condition. Threads that are signaled but not unblocked will be executed before pthread_cond_destroy() returns control to the application.

Safe but blocking

  • Darwin libc-391 - pthread_cond_destroy() will return EBUSY if there are threads still blocked on the condition. No provisions are made for blocked but signaled threads.
  • dietlibc 0.27 - pthread_cond_destroy() will return EBUSY if there are threads still blocked on the condition. No provisions are made for blocked but signaled threads.

Possibly not safe

  • Android - Depends on system implementation of __futex_wake_ex to be synchronous. Thus pthread_cond_broadcast() must block until all threads have woken (but not released their mutex).
  • various win32 implementations - Many rely on the PulseEvent() function to implement pthread_cond_broadcast(). This has a known race condition

Oddballs

  • OSKit 0.9 - Safe but returns EINVAL if pthread_cond_destroy() is called on a condition variable that is still referenced

Edit

The major issue, if I read your comments correctly is whether the pthread_cond_wait() can access the condition variable before it returns but after it is unblocked. And the answer is yes. The routine assumes that its arguments will remain valid.

This means that after broadcasting, your thread cannot assume that the condition variable is unused by other threads.

When you call pthread_cond_broadcast(), you do not acquire the associated mutex lock. While the threads waiting on your condition variable will execute sequentially, each acquiring the associated mutex in series. Because your waiters may block each other, your broadcasting thread may continue executing while waiters are still in pthread_cond_wait() blocked on the mutex (but not waiting for the condition).


Edit 2

[...]is it reasonable to expect unmapping to be valid in the same contexts destruction would be valid?

I don't think that this is a reasonable expectation based on the reasoning in Edit 1. Additional synchronization would definitely be required if you are precluded from using pthread_cond_destroy()

Neurocoele answered 14/10, 2011 at 20:16 Comment(9)
Thanks for the answer, but I'm not looking for whether pthread_cond_destroy is "safe" in the sense of reporting that the cond var is still busy (not required). Rather, I'm asking whether the cond var formally can be considered to still have blocked waiters after a signal/broadcast that's specified to "unblock" them returns.Pantograph
In that case, yes the broadcast does not ensure that there are no waiters. Between the moment you broadcast and the moment the routine returns, additional waiters can accumulate. So while all extant waiters are unblocked by the broadcast, there are no guarantees that by the time the function returns, the condition variable is unused.Neurocoele
"additional waiters can accumulate" depends on your program; it's certainly possible that you can ensure that case cannot happen. For example if each waiter increments a counter protected by the mutex before waiting, and if you call the broadcast with the mutex held once that count has reached the number of threads that could be waiting, then you know no further waiters can arrive. There are many other such scenarios.Pantograph
That is very true. But POSIX makes no requirements as such. So I think that by the standard, the cond var can still be formally required by elements of an arbitrary program.Neurocoele
As an addendum, many implementations use a mutex in the condition variable structure that gets locked before the pthread_cond_wait() returns but after the user mutex is locked. The problem is now that multiple waiters will block on the user mutex while the thread that calls pthread_cond_broadcast() returns. The threads that sequentially acquire the user mutex will still require a valid condition variable after they unblock, so an arbitrary program cannot formally consider the condition variable unused after the broadcastNeurocoele
@Neurocoele "a mutex in the condition variable structure that gets locked before the pthread_cond_wait() returns but after the user mutex is locked." what for?Frustration
@Frustration To atomically modify the content of the condition variable (number of waiter, e.g)Neurocoele
@Neurocoele I see, the number of waiters is a ref count used to prevent premature destruction by pthread_cond_destroy?Frustration
@Frustration While doesn't have to be the case, it is used in more than one implementation.Neurocoele
F
0

Comment (not answer):

Is that what you have in mind?

Global:

// protected by m:
pthread_mutex_t m;
pthread_cond_t c;
bool about_to_pthread_cond_wait = false;
bool condition_waited_on = false;

Thread A :

pthread_mutex_lock (&m);
{ // locked region
    about_to_pthread_cond_wait = true;
    while (condition_waited_on) {
        // pthread_cond_wait (&m, &c) is decomposed here:
        __pthread_mutex_cond_wait_then_unlock (&m, &c);
            // unlocked region
        pthread_mutex_lock (&m);
    }
}
pthread_mutex_unlock (&m);

Thread B:

for (bool break_loop = false; !break_loop;) {
    pthread_mutex_lock (&m);
    { // locked region
        condition_waited_on = true;

        if (about_to_pthread_cond_wait) {
            pthread_cond_signal (&c);
            pthread_cond_destroy (&c);
            break_loop = true;
        }
    }
    pthread_mutex_unlock (&m);

    pthread_yield ();
}

EDIT:

I assume pthread_cond_wait (&m, &c); does:

__pthread_mutex_cond_wait_then_unlock (&m, &c);
pthread_mutex_lock (&m);

__pthread_mutex_cond_wait_then_unlock will being monitoring the signals to the CV, then unlock the mutex, then go to sleep.

If the CV has an internal mutex that __pthread_mutex_cond_wait_then_unlock and pthread_cond_signal must lock internaly, then I assume __pthread_mutex_cond_wait_then_unlock does:

pthread_mutex_lock (&c.int_mutex);
{ // locked region for c.int_state
    __register_wakeup_cond (&c.int_state);
    pthread_mutex_unlock (&m);
}
pthread_mutex_unlock (&c.int_mutex);
// will not touch c.int_state any more 

__sleep_until_registered_wakeups ();

and pthread_cond_signal does:

pthread_mutex_lock (&c.int_mutex);
{ // locked region for CV internal state
    __wakeup_registered (&c.int_state);
}
pthread_mutex_unlock (&c.int_mutex);

Then pthread_cond_destroy will only be called after __pthread_mutex_cond_wait_then_unlock has finished using c.int_state.

Frustration answered 15/10, 2011 at 14:4 Comment(7)
This example will fail. You have mutex m locked while you signal and then try to destroy the condition. Because the pthread_cond_wait() will not allow thread A to continue until it has acquired sole control of the mutex m, your call to pthread_cond_destroy() will likely return EBUSY and the condition variable will remain intactNeurocoele
@Neurocoele "This example will fail" Do you have any example that will work?Frustration
@Neurocoele Is the assumption of mutual exclusion between __register_wakeup_cond and __wakeup_registered unreasonable?Frustration
I think that the assumption of uninterrupted execution of consecutive statements is not safe.Neurocoele
A working example (that is independent of the pthread implementation) could loop on pthread_cond_destroy() until success. There are other ways as well (obviously).Neurocoele
@Neurocoele "the assumption of uninterrupted execution of consecutive statements is not safe" Hug?Frustration
Sorry, I think I mis-interpreted what you were saying. __register_wakeup_cond() and __wakeup_registered() are mutually exclusive in your two functions above. But you rely on a global structure to maintain your wakeup conditions (where you register them). POSIX threads maintains the state in the condition variable.Neurocoele

© 2022 - 2024 — McMap. All rights reserved.