SLIDE 1
Putting threads on a solid foundation: Some Remaining Issues - - PowerPoint PPT Presentation
Putting threads on a solid foundation: Some Remaining Issues - - PowerPoint PPT Presentation
Putting threads on a solid foundation: Some Remaining Issues Hans-J. Boehm Multithreaded language specifications have improved, but ... Real world constraints additional language features more complications Some remaining problems 1.
SLIDE 2
SLIDE 3
Some remaining problems
- 1. Out-of-thin-air results/dependency-based
- rdering.
○ Relaxed memory ordering is hard to specify.
- 2. “Managed” languages: Finalization.
○ Most Java finalization uses are incorrect. ○ “Known” for 10+ years, but ...
- 3. C++: Detached threads and object
destruction.
○ std::async() is problematic. ○ Discussed regularly in C++ standards committee.
SLIDE 4
A word on out-of-thin-air problem for relaxed atomics
… can’t be (?) precluded from yielding x = y = 42
- Purely a specification problem
○ Nobody has ever seen a real out-of-thin-air result. ○ Programs don’t really break. ○ We all “know what it should mean”. ○ We just can’t prove anything about ■ Java programs. ■ C++ programs using memory_order_relaxed.
- Frustrating because the problem doesn’t seem real.
- The other two problems result in real code breakage.
x = y; y = x;
Thread 1:: Thread 2:
SLIDE 5
Problem 2: Java finalization
- finalize() method runs when an object is
unreachable, as determined by garbage collector.
- Dubious design; some problems fixed by java.lang.
ref.
- For our purposes they’re essentially equivalent.
- Rarely used.
- But doing without it requires reimplementing GC.
SLIDE 6
Common finalization idiom
Mixed language Java program:
Class T has field native_ptr, holding pointer to C++
- bject.
T.foo(): { long x = native_ptr; native_func(x); } T.finalize(): { ... native_delete(native_ptr); native_ptr = 0; }
A thread: Finalizer thread:
SLIDE 7
Finalization problem, continued
- Assume this is last use
- f this object.
- this reference is dead.
- x is still live.
- Finalizer may run here.
- native_func gets a
dangling pointer argument.
T.foo(): { long x = native_ptr; native_func(x); }
A thread:
SLIDE 8
An official solution, since 2005
T.foo(): { long x = native_ptr; native_func(x); synchronized(this) {} } T.finalize(): { synchronized(this) {} ... native_delete(native_ptr); native_ptr = 0; }
A thread: Finalizer thread:
There are other, equally dubious, solutions. All add significant runtime cost, counterintuitive. Nobody does any of this!
SLIDE 9
Finalization problem, continued (2)
- Pointed out in 2003 POPL paper and 2005 JavaOne
talk.
- Pervasive problem in every source base I’ve seen.
- Java.lang.ref has the same problem.
- Not limited to native code.
- Most uses of finalization are broken.
○ The rest are mostly trivial, e.g. generate warnings.
SLIDE 10
Possible solutions
- Provide less expensive alternative to synchronized
(this) {} idiom. ○ Probably too little, too late.
- Prohibit compiler dead reference elimination.
○ Viewed as too expensive for “obscure” construct.
- Support annotation of class T that prevents compiler
eliminations of “dead” references to T. ○ Current favorite. ○ Seems to handle common cases with simple rule: ■ Annotate class if a field is invalidated by finalization or reference queue processing.
SLIDE 11
Problem 3: Detached threads and
- bject destruction
Detached thread: A thread that can no longer be waited for by joining it. Core problem:
- Objects in C++ have finite lifetime.
- There is (almost) no way to guarantee that a detached
thread completes before objects it needs are destroyed. C++11 and C++14:
- Thread class has detach() method.
- Library has APIs to make this sometimes somewhat
usable.
- My recommendation: Don’t use.
SLIDE 12
The real problem: Accidentally detached threads
What happens if an object representing an unjoined thread
- bject goes out of scope?
Traditional answer:
- Can no longer join, thread destructor calls detach().
Extremely dangerous!
SLIDE 13
Accidentally detached threads (contd)
psort (begin, end) { … thread t([]{psort(mid, end);}); a: … t.join(); … } Assume: Begin, end iterators point to array allocated in caller f(). An unexpected exception is thrown at a. Now:
- Thread t continues to run after
f() returns.
- Thread t ends up writing to
nonexistent memory in a different thread!
- This only took an unexpected
exception!
SLIDE 14
Detached threads and async()
Async() is a convenient way to run a function in a thread and return a future<T>: { Vector<T> x; auto r = async ([&] { return foo(x); }) a: … r.get() …; } Exception at a must not allow late access to x ⇒ destruction of future r waits for underlying thread.
SLIDE 15
But
Having future<T> destructor wait for thread is weird:
- Many future<T> objects don’t have an underlying
thread.
- Those that do behave differently from those that don’t.
○ Generic code becomes hard.
- async(void_func); doesn’t mean what you think.
○ async(f); async(g); runs f and g sequentially!
- Makes future<T> unusable in contexts in which blocking
is not allowed. C++11/14 provide blocking futures, but only if they are produced by async().
SLIDE 16
Current consensus
- std::async(), as it is, was a mistake.
- You need separate handles on the result and underlying
“execution agent”.
- We hope to have executors in C++17 to provide the
thread/execution agent handle.
SLIDE 17