TL;DR: We (hiroshige@, yhirano@ and kinuko@) are considering introducing a thread-safe string class in blink as cross-thread string handling with WTF::String is inherently broken no matter whether we use deep-copy or not, and we want feedback/comments.
We've been using non-thread-safe ref-counted String implementation in blink (a.k.a. WTF::String, you can find some contexts in [1]), which is supposed to be highly tuned for small string operations on single-threaded environment, but also is known not to work well in multi-threaded environment.
For cross-thread string handling we had been using CrossThreadCopier and WTF::String::isolatedCopy() to deep-copy the string data, but hiroshige@ found that this still has race issues in WTF::bind() [2] and base::Bind() [3]. In general, objects that contain non-thread-safe reference counters (e.g. WTF::String, KURL) cannot be bound to a copyable Closure.
A race issue of WTF::bind() has been fixed by making WTF::bind() to return a move-only Closure (so that it can be cleanly passed to another thread). But this solution is harder to apply to base::Bind(), because many existing code assumes base::Callback (the return type of base::Bind()) is copyable.
We started worrying that we’ll probably need to consider having thread-safe string in blink more seriously, as we could have more and more similar situations as we're integrating/merging blink into chromium's multi-threaded architecture. (Note: other non-thread-safe ref-counted objects have similar problems and we’ll have to fix them similarly; but WTF::String is the most used, most problematic one ;) afaik)
Possible solutions:
Use thread-safe string only when we use base::Bind() (or WTF::bind()). When we need to pass string data across threads, convert WTF::String into a thread-safe string, pass the thread-safe string across threads, and then convert back to WTF::String before using it in the target thread. The thread-safe string is to be used in base::Bind(), WTF::bind() and CrossThreadCopier (we always call CrossThreadCopier in Bind/bind, no matter whether it is used for cross-thread or within a single thread), and other code continues to use WTF::String.
We can possibly use std::string16 for this purpose once chromium/blink merge is done, but until then we're considering adding a small thread-safe string class (i.e. sort of non-ref-counted CString).
Use thread-safe strings in the entire Blink. This could be more controversial and could have performance penalty, but will make things easier for us to make more blink code multi-threaded. For performance-sensitive single-threaded code we may still want to continue to use WTF::String. (Some data: as a quick experiment, hiroshige@ tried changing StringImpl to use thread-safe atomic ref-counting [4], which resulted 20+% slower performance in some micro-benches [5]. So apparently naively changing RefCounted to ThreadSafeRefCounted isn’t a solution we want)
Currently we’re considering applying solution 1.
Any thoughts / comments?
[1] https://groups.google.com/a/chromium.org/d/msg/chromium-dev/EUqoIz2iFU4/k0t9JT-wRRkJ
[3] https://code.google.com/p/chromium/issues/detail?id=390851#c19
[4] https://codereview.chromium.org/751553004/
[5] Linux blink_perf.dom
http://storage.googleapis.com/chromium-telemetry/html-results/results-2014-12-08_00-35-55
(CL: https://codereview.chromium.org/751553004/)
For cross-thread string handling we had been using CrossThreadCopier and WTF::String::isolatedCopy() to deep-copy the string data, but hiroshige@ found that this still has race issues in WTF::bind() [2] and base::Bind() [3]. In general, objects that contain non-thread-safe reference counters (e.g. WTF::String, KURL) cannot be bound to a copyable Closure.
For cross-thread string handling we had been using CrossThreadCopier and WTF::String::isolatedCopy() to deep-copy the string data, but hiroshige@ found that this still has race issues in WTF::bind() [2] and base::Bind() [3]. In general, objects that contain non-thread-safe reference counters (e.g. WTF::String, KURL) cannot be bound to a copyable Closure.
Thanks for comments!One problem due to temporary objects is described by abarth@ above.Another problem caused by copyable base::Closure is as follows.When we execute code like:1. base::Closure a = base::Bind(func, str.isolatedCopy()); // on Thread 12. base::Closure b = a;3. Move Closure b to another Thread 24. Call b.Run() // on Thread 25. Destruct Closure b // on Thread 26. Destruct Closure a // on Thread 1After Step 2, the structure of the objects at this time is like:[base::Closure a]-scoped_refptr->[base:: ]-has->[WTF::String]-RefPtr->[WTF::StringImpl][base::Closure b]-scoped_refptr->[BindState]where the refcount of the StringImpl is one.At Step 4, more references to the StringImpl are added in func() from Thread 2:[base::Closure a (Thread 1)]-scoped_refptr->[base:: ]-has->[WTF::String]-RefPtr->[WTF::StringImpl]<-RefPtrs from Thread 2[base::Closure b (Thread 2)]-scoped_refptr->[BindState]At Step 6, Closure a is destructed, then BindState is destructed, then StringImpl's deref() is called from Thread 1.However, StringImpl's ref()/deref() can be called from Thread 2 (via references created at Step 4), which causes race conditions.abarth@, does your WTF::StringBuffer solution take a deep copy at |base::Bind| (String -> StringBuffer) anda deep copy at |base::Callback::Run| (StringBuffer -> String)?
If so, I think it will work.In the example above, RefPtrs from Thread 2 are not added at Step 4 to StringBuffer's StringImpl, but to a copy of it.The reference count of StringBuffer's StringImpl remains one, and is safely destructed at Step 6.
> 3) Added a move constructor to StringBuffer.How this move constructor used?
I don't think we would be able to make base::Closure move-only, but is there a reason we can't make it moveable?
I imagine that we'd want to eventually combine base::Bind/base::Closure and wtf::bind/wtf::Closure.
On Tue Dec 09 2014 at 1:16:18 AM Daniel Cheng <dch...@chromium.org> wrote:I don't think we would be able to make base::Closure move-only, but is there a reason we can't make it moveable?It might be valuable to make base::Closure movable, but it's not necessary to solve the problems described in this thread.I imagine that we'd want to eventually combine base::Bind/base::Closure and wtf::bind/wtf::Closure.Yes, I'd recommend deleting WTF::Bind. The only reason we started using it in the first place was that we weren't able to use base::Bind due to being in a separate repository from Chromium.
but StringBuffer approach seems like a good reasonable solution for the problem we have right now (and we might be going to do the opposite-- using these fancy Blink string classes in Chromium). I imagine we're going to have a separate discussion thread for this after repo merge though.On Tue Dec 09 2014 at 1:16:18 AM Daniel Cheng <dch...@chromium.org> wrote:I don't think we would be able to make base::Closure move-only, but is there a reason we can't make it moveable?It might be valuable to make base::Closure movable, but it's not necessary to solve the problems described in this thread.I imagine that we'd want to eventually combine base::Bind/base::Closure and wtf::bind/wtf::Closure.Yes, I'd recommend deleting WTF::Bind. The only reason we started using it in the first place was that we weren't able to use base::Bind due to being in a separate repository from Chromium.Yes, we assume we're subsuming wtf::bind into base::Bind after we merge repositories.
The StringBuffer approach just makes one copy, during the calling to isolatedCopy. The resulting StringImpl is stored inside the StringBuffer object. Instead of making another copy of the string during base::Callback::Run, code that wishes to take a reference to the string will need to call StringBuffer::release() to take the StringImpl out of the StringBuffer. In your example, at step 6, when the BindState is destructed, there's no longer a StringImpl to deref and therefore no data race.
The StringBuffer approach just makes one copy, during the calling to isolatedCopy. The resulting StringImpl is stored inside the StringBuffer object. Instead of making another copy of the string during base::Callback::Run, code that wishes to take a reference to the string will need to call StringBuffer::release() to take the StringImpl out of the StringBuffer. In your example, at step 6, when the BindState is destructed, there's no longer a StringImpl to deref and therefore no data race.This makes Closure (and its copies) callable only once.[Example 2]1. base::Closure a = base::Bind(func, str.isolatedCopy());2. base::Closure b = a;3. Call b.Run() // Good. Here StringBuffer.release() is called, and Closure a and b are no longer callable4. Call a.Run() // Invalid. func receives null StringImpl.// This is the first time Closure a is called, but StringBuffer's reference is already released at a call to its copy, Closure b, at Step 3.5. Call b.Run() again // Invalid. func receives null StringImpl.Also, the current implementation of StringBuffer::release() is not thread-safe, and there will be another race condition when a.Run() and b.Run() is executed simultaneously (in Steps 3 and 4 in Example 2 above) from different threads.(I assume base::Closure can (or should be able to) be passed across threads, copied, and have Run() called without limitation.)
On Wed, Dec 10, 2014 at 3:12 PM, Hiroshige Hayashizaki <hiro...@chromium.org> wrote:The StringBuffer approach just makes one copy, during the calling to isolatedCopy. The resulting StringImpl is stored inside the StringBuffer object. Instead of making another copy of the string during base::Callback::Run, code that wishes to take a reference to the string will need to call StringBuffer::release() to take the StringImpl out of the StringBuffer. In your example, at step 6, when the BindState is destructed, there's no longer a StringImpl to deref and therefore no data race.This makes Closure (and its copies) callable only once.[Example 2]1. base::Closure a = base::Bind(func, str.isolatedCopy());2. base::Closure b = a;3. Call b.Run() // Good. Here StringBuffer.release() is called, and Closure a and b are no longer callable4. Call a.Run() // Invalid. func receives null StringImpl.// This is the first time Closure a is called, but StringBuffer's reference is already released at a call to its copy, Closure b, at Step 3.5. Call b.Run() again // Invalid. func receives null StringImpl.Also, the current implementation of StringBuffer::release() is not thread-safe, and there will be another race condition when a.Run() and b.Run() is executed simultaneously (in Steps 3 and 4 in Example 2 above) from different threads.(I assume base::Closure can (or should be able to) be passed across threads, copied, and have Run() called without limitation.)We can have a type of callbacks that can be called only once with base::Callback (e.g. it can take scoped_ptr with base::Passed helper [1]). I think making callbacks which take StringImpl callable-only-once is probably ok.
base::Callbacks often capture move-only types which makes them callable only once. Whether a given callback can be called multiple times depends on the particular usage and is not a feature of the type itself.
(In contrast C++11's std::bind+std::function requires that all parameters be copyable which makes it completely useless in many situations).
- James
On Wed, Dec 10, 2014 at 3:23 PM, Kinuko Yasuda <kin...@chromium.org> wrote:On Wed, Dec 10, 2014 at 3:12 PM, Hiroshige Hayashizaki <hiro...@chromium.org> wrote:The StringBuffer approach just makes one copy, during the calling to isolatedCopy. The resulting StringImpl is stored inside the StringBuffer object. Instead of making another copy of the string during base::Callback::Run, code that wishes to take a reference to the string will need to call StringBuffer::release() to take the StringImpl out of the StringBuffer. In your example, at step 6, when the BindState is destructed, there's no longer a StringImpl to deref and therefore no data race.This makes Closure (and its copies) callable only once.[Example 2]1. base::Closure a = base::Bind(func, str.isolatedCopy());2. base::Closure b = a;3. Call b.Run() // Good. Here StringBuffer.release() is called, and Closure a and b are no longer callable4. Call a.Run() // Invalid. func receives null StringImpl.// This is the first time Closure a is called, but StringBuffer's reference is already released at a call to its copy, Closure b, at Step 3.5. Call b.Run() again // Invalid. func receives null StringImpl.Also, the current implementation of StringBuffer::release() is not thread-safe, and there will be another race condition when a.Run() and b.Run() is executed simultaneously (in Steps 3 and 4 in Example 2 above) from different threads.(I assume base::Closure can (or should be able to) be passed across threads, copied, and have Run() called without limitation.)We can have a type of callbacks that can be called only once with base::Callback (e.g. it can take scoped_ptr with base::Passed helper [1]). I think making callbacks which take StringImpl callable-only-once is probably ok.(.release() implementation needs to take care of thread safety if we take this option for sure)
On Tue Dec 09 2014 at 10:24:47 PM Kinuko Yasuda <kin...@chromium.org> wrote:On Wed, Dec 10, 2014 at 3:23 PM, Kinuko Yasuda <kin...@chromium.org> wrote:On Wed, Dec 10, 2014 at 3:12 PM, Hiroshige Hayashizaki <hiro...@chromium.org> wrote:The StringBuffer approach just makes one copy, during the calling to isolatedCopy. The resulting StringImpl is stored inside the StringBuffer object. Instead of making another copy of the string during base::Callback::Run, code that wishes to take a reference to the string will need to call StringBuffer::release() to take the StringImpl out of the StringBuffer. In your example, at step 6, when the BindState is destructed, there's no longer a StringImpl to deref and therefore no data race.This makes Closure (and its copies) callable only once.[Example 2]1. base::Closure a = base::Bind(func, str.isolatedCopy());2. base::Closure b = a;3. Call b.Run() // Good. Here StringBuffer.release() is called, and Closure a and b are no longer callable4. Call a.Run() // Invalid. func receives null StringImpl.// This is the first time Closure a is called, but StringBuffer's reference is already released at a call to its copy, Closure b, at Step 3.5. Call b.Run() again // Invalid. func receives null StringImpl.Also, the current implementation of StringBuffer::release() is not thread-safe, and there will be another race condition when a.Run() and b.Run() is executed simultaneously (in Steps 3 and 4 in Example 2 above) from different threads.(I assume base::Closure can (or should be able to) be passed across threads, copied, and have Run() called without limitation.)We can have a type of callbacks that can be called only once with base::Callback (e.g. it can take scoped_ptr with base::Passed helper [1]). I think making callbacks which take StringImpl callable-only-once is probably ok.(.release() implementation needs to take care of thread safety if we take this option for sure)Can you explain the thread safety issue you're worried about with StringBuffer::release() ?