-
Notifications
You must be signed in to change notification settings - Fork 262
Add multi-threaded collections #692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
jonwis
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks generally good; is it fair to say that the threadedness is about "this operation increments the version" vs. "this one does not"?
|
|
||
| uint32_t container_size() const noexcept | ||
| { | ||
| return static_cast<uint32_t>(std::distance(static_cast<D const&>(*this).get_container().begin(), static_cast<D const&>(*this).get_container().end())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We seem inconsistent about the requirements we place on the container. Here, we only assume that it has begin() and end(), but make no assumptions about size(). But vector_base assumes that size() exists. I wonder why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Views can be constructed from iterator pairs and there's no size on winrt::impl::range_container.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. On a side note, I've been toying with the thought of introducing C++20 concepts to C++/WinRT in few places. The collection-related code, in particular, is an area that would benefit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, there are a few C++20 features I'd like to adopt including span and coroutines #676 - we should think about forking cppwinrt with a servicing C++17 branch and start work on C++20 features in master. Just a little nervous about how long it will take for C++20 to become mainstream since Visual C++ doesn't even have a /std:c++20 flag yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. At least with span, concepts, char8_t, consteval, chrono updates, and otheres, there are clear-cut feature test macros. Coroutine support has the potential to be messier.
DefaultRyan
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One minor nitpicky suggestion on the map writing test, but looks good.
strings/base_collections_base.h
Outdated
| this->increment_version(); | ||
| static_cast<D&>(*this).get_container()[index] = static_cast<D const&>(*this).wrap_value(value); | ||
| this->increment_version(); | ||
| static_cast<D&>(*this).get_container()[index] = static_cast<D const&>(*this).wrap_value(value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This destructs the previous value while inside the lock. Since the object being destructed can be an app-controlled object, there is danger of reentrancy and therefore deadlock. (We have found actual product bugs that were traced back to this category of deadlock.)
Need to swap the previous item into an object that exists outside the lock, so that it destructs outside the lock.
auto wrapped = static_cast<D const&>(*this).wrap_value(value);
static_cast<D&>(*this).perform_exclusive([&]
{
if (index >= ...) { ... }
this->increment_version();
swap(static_cast<D&>(*this).get_container()[index], wrapped);
});
// wrapped destructs here, outside the lock
}This needs to be done only for reference types. Value types do not contain app-controlled objects (the only object with a nontrivial destructor that can be in a value type is hstring, and the app doesn't own the hstring destructor.)
In theory, even AddRef is app-controlled, but apps are unlikely to do anything fancy in their AddRef. Doing fancy stuff in the destructor is far more common.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The latest iteration is a first stab at addressing this. I'm mostly looking for feedback/input on the approach, though it is fully functional. The most important thing is probably delaying destruction of the "wrapped" value vs. delaying destruction of the "unwrapped" value. Of course, delaying destruction of the "wrapped" value implies that the destruction of the "unwrapped"/"underlying" value is also delayed. The approach here is to delay destruction of the "wrapped" value. This seemed to be the safest approach, but is not without its pros/cons.
- This covers the scenario where the "wrapped" type's destructor does something potentially unsafe. I'm unsure how realistic this scenario is, however I figured it's best to be safe.
- We can make fewer assumptions about the "wrapped" type, at least compared to WinRT types. In particular how/if it's constructible. The approach here is to use
std::optionalso that we can delay (move) construct the value, and that is the only requirement on the type put in place. If the type is not move constructible, we fall back to running the destructor under the lock (a con, but unlikely to ever be a problem). There's also a potential issue if the moved-from value's destructor does something unsafe, however that's pretty much unavoidable no matter what we do. - As-is, we assume that anything that's not trivially destructible needs to be destructed outside of the lock. This is a bit more restrictive than necessary. E.g. I would assume that
hstringshould be safe to destroy under the lock (not necessarily a con; doing less under the lock is generally good). Similarly, I would assume that structs withIReferencemembers should be safe to destroy under the lock (but I'm not 100% sure, correct me if I'm wrong). - This doesn't take into account the "threadedness" of the collection. I.e. the single threaded collections still try and order destruction. This is mostly noteworthy for operations like
ClearandReplaceAllwhere we may otherwise be able to re-use the buffer. This is mostly a simplicity decision.
Mostly just wanted to get this out to get thoughts/feedback.
| }); | ||
| } | ||
|
|
||
| void Split(Windows::Foundation::Collections::IMapView<K, V>& first, Windows::Foundation::Collections::IMapView<K, V>& second) const noexcept |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I never did figure out what Split does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does nothing. In theory it was meant to split the container for parallel search algorithms. That wasn't a well thought out plan. Fortunately a "valid" implementation can do nothing.
…cppwinrt into dunhor/mt_collections
kennykerr
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
This mainly re-uses the existing collections, but adds support for doing shared/exclusive operations. For the existing single threaded collections, nothing special is done and operations are performed without any synchronization. For the new multi-threaded collections, an SRWLOCK is acquired in either shared or exclusive mode.
The most noteworthy "flaw" is that mutating iterator operations lock the collection exclusively. This is mostly a limitation imposed by the base IIterable type which uses opaque iterators for the IIterator objects. In theory, the collection only needs to be locked in shared mode, however that would require the IIterator objects themselves to have a separate lock, which seemed like overkill and unlikely to be worth it. Compare this to the existing ABI collection types which know the underlying vector/map types and can perform interlocked operations - in the case of vector - or forgo sequential consistency while still guaranteeing safety - in the case of map.
The following were also discussed, but are not addressed here as they can be done separately as a later change: