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. Out-of-thin-air results/dependency-based ordering. ○ 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.
A word on out-of-thin-air problem for relaxed atomics Thread 1:: Thread 2: x = y; y = x; … 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.
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.
Common finalization idiom Mixed language Java program: Class T has field native_ptr , holding pointer to C++ object. A thread: Finalizer thread: T.foo(): T.finalize(): { { long x = native_ptr; ... native_func(x); native_delete(native_ptr); } native_ptr = 0; }
Finalization problem, continued ● Assume this is last use of this object. A thread: ● this reference is dead. ● x is still live. T.foo(): ● Finalizer may run here. { ● native_func gets a long x = native_ptr; dangling pointer native_func(x); argument. }
An official solution, since 2005 A thread: Finalizer thread: T.foo(): T.finalize(): { { long x = native_ptr; synchronized(this) {} native_func(x); ... synchronized(this) {} native_delete(native_ptr); } native_ptr = 0; } There are other, equally dubious, solutions. All add significant runtime cost, counterintuitive. Nobody does any of this!
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.
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.
Problem 3: Detached threads and object 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.
The real problem: Accidentally detached threads What happens if an object representing an unjoined thread object goes out of scope? Traditional answer: ● Can no longer join, thread destructor calls detach(). Extremely dangerous!
Accidentally detached threads (contd) Assume: Begin , end iterators psort (begin, end) point to array allocated in caller { f() . … thread t([]{psort(mid, end);}); An unexpected exception is a: … thrown at a . t.join(); … 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!
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.
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() .
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.
Questions?
Recommend
More recommend