-
Notifications
You must be signed in to change notification settings - Fork 778
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
Refactor Document class #3586
base: master
Are you sure you want to change the base?
Refactor Document class #3586
Conversation
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 for the PR! LGTM. Please check that the document is always locked by the caller when the operator=
is called, just to make sure.
Do you know how I can do something like assert(thread_has_lock(gmutex))? Overwriting the = operator seems to be really bad style to me. It took me a bit to find out about that definiton while debugging. Is there any benefit by doing that? |
I've found a lot more code that uses tryLock and some other dubious lines in the Document class. I'm gonna look into that once I know how to assert for having the lock. |
I don't think you can.
There are pros and cons. In this case, more cons than pros, I think. The operator is used only in |
For the moment, you could cherry-pick c8d690c at the root of your PR to work with those changes already. |
Why not merging release-1.1 into master? |
I decided to move the actual bug fix to another PR since this one will get big. |
Yes, of course, merging is better! |
078cd02
to
50667af
Compare
I've done it a bit different. I've cherry-picked my commits to release-1.1. |
That is really not what you want. This PR can never be merged into a minor release as it has a way too high potential to break something. This should be seen more like a new feature than a bugfix and is therefore best merged into |
You could write some enclosing guard named Document with the following function: template With the private member internalDocument, which contains all the fields in the doc. |
We could actually overload the |
Oh, yes. So how do we do that? I base this PR on master and merge release-1.1. And then we merge this PR when we merge release-1.1 onto master? |
50667af
to
078cd02
Compare
No, we will have to merge Once I merged both branches, you can just fast-forward your branch to the HEAD of |
@fabian-thomas you can start your work on top of master now. |
078cd02
to
e9f6e93
Compare
I've began to look into this. I started by searching for a monitor like implementation that doesn't use some crazy cpp syntax and is easy to understand. What also mattered was that we need some reentrant functionality (locking the document for multiple function calls). Also a reader-writer functionality would be nice to have. I've settled on the following: For the reentrant functionality we just do something like:
What do you think about this approach? |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
You can ask your questions here if you have one.
The document's Mutex must not be accessible. It would be better to implement the lock and try_lock function, but not unlock. Using a recursive lock has a smell, but it's ok, to fix a deadlock (when a better aproach is e.g. not feasible because a large amount of code has to be refactored). That's why an observer based approach would be better, but your approach with my change request is way better than the current situation. |
src/core/model/Document.cpp
Outdated
cairo_surface_t* Document::getPreview() { | ||
std::shared_lock<std::recursive_mutex> lock(mutex); | ||
return this->preview; | ||
} |
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.
Returning a reference or a pointer, even a const& is not thread safe. You have to transfer the guard also.
cairo_surface_t* Document::getPreview() { | |
std::shared_lock<std::recursive_mutex> lock(mutex); | |
return this->preview; | |
} | |
using ReadGuard = std::shared_lock<std::recursive_mutex>; | |
std::pair<cairo_surface_t*, ReadGuard> Document::getPreview() { | |
return {this->preview, mutex}; | |
} |
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.
Yeah, I've rewritten it a little bit to make sure that I will take a look at this later.
In the best case we would write a wrapper for this cairo_surface that holds the document lock until it itself is freed.
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.
But I think this is not feasible. There are some other classes that would need this treatment too. Does somebody know another way to do this?
What is wrong with recursive locks, apart from their minimal overhead?
Could you elaborate that observer approach further? I guess we should go for the best pattern, since we'll already refactor a lot of code. |
It's a sign of bad design: It basically means, that none knows, and can say, which thread has when and where access to the data. The best solution, would be, to have the |
But that would require a lot of redundant definitions of each function. In DocumentHandle header and source + Document header and source. |
Yep, that's a solution which is easy, understandable and explicit.
, which overloads the
The document is itself not accessible from outside, it is also not lockable. But a function which is transferred to the DocumentHandle will retrieve a reference to the Document. |
48cd593
to
263cfd5
Compare
This will take a while to complete... The most interesting class to review by now is therefore the Monitor one. Pls provide feedback on that one (as stated in the class I've copied the most interesting code from stackoverflow). For the process of refactoring I need some more information on what is working concurrently on the Document. Currently all calls to the Document will be synchronized. If it would turn out that only some fields in the Document need to be synchronized this would add a big(???) overhead. If there are no bigger flaws with my work until now I would continue refactoring from time to time to get to a point where I can build the application again. |
src/core/model/Monitor.cpp
Outdated
#include "Monitor.h" | ||
|
||
/* | ||
* Credit to Mike Vine on StackOverflow. | ||
* https://stackoverflow.com/a/48408987 | ||
*/ | ||
|
||
template <class T> | ||
template<typename ...Args> | ||
Monitor<T>::Monitor(Args&&... args) : model(std::forward<Args>(args)...) {} | ||
|
||
|
||
template <class T> | ||
Monitor<T>::LockedMonitor::LockedMonitor(Monitor* monitor) : mon(monitor), lock(monitor->mutex) {} | ||
|
||
template <class T> | ||
T* Monitor<T>::LockedMonitor::operator->() { return &mon->model;} | ||
|
||
template <class T> | ||
void Monitor<T>::LockedMonitor::ReplaceModel(T model) { | ||
mon->model = model; | ||
} | ||
|
||
|
||
template <class T> | ||
Monitor<T>::TryLockedMonitor::TryLockedMonitor(Monitor* monitor) { | ||
this->lock = std::unique_lock<std::mutex>(monitor->mutex, std::defer_lock); | ||
this->lockAcquired = lock.try_lock(); | ||
if (this->lockAcquired) { | ||
this->mon = monitor; | ||
} | ||
} | ||
|
||
template <class T> | ||
T* Monitor<T>::TryLockedMonitor::operator->() { | ||
assert(lockAcquired); | ||
return &mon->model; | ||
}; | ||
|
||
template <class T> | ||
void Monitor<T>::TryLockedMonitor::ReplaceModel(T model) { | ||
assert(lockAcquired); | ||
mon->model = model; | ||
} | ||
|
||
|
||
template <class T> | ||
auto Monitor<T>::operator->() -> typename Monitor<T>::LockedMonitor { return LockedMonitor(this); } | ||
|
||
template <class T> | ||
auto Monitor<T>::lock() -> typename Monitor<T>::LockedMonitor { return LockedMonitor(this); } | ||
|
||
template <class T> | ||
auto Monitor<T>::tryLock() -> typename Monitor<T>::TryLockedMonitor { return TryLockedMonitor(this); } | ||
|
||
template <class T> | ||
T& Monitor<T>::getUnsafeAccess() { return model; } |
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.
In C++ template code must be either in the header file, or directly in the source, where it is used. (The compiler don't know how to generate the code).
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 actually works (it compiles for me) but is really annoying to read. Should I just remove the header and move everything to the source file again?
|
||
private: | ||
T model; | ||
std::mutex mutex; |
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 want a read and write access, to reduce contention.
std::mutex mutex; | |
std::shared_mutex mutex; |
|
||
|
||
LockedMonitor operator->(); | ||
|
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.
Add
SharedMonitor lock_shared();
std::optional<SharedMonitor> try_lock_shared();
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.
Do we really need this differentiation? This will add a lot of complexity.
When we add something like that the caller is again responsible for only doing read operations while holding the shared monitor. We have no way of forcing a uniquely locked monitor for some function calls to Document.
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.
When the SharedMonitor
only returns a Document const*
the caller can't access non const functions.
src/core/model/Monitor.h
Outdated
struct TryLockedMonitor | ||
{ | ||
TryLockedMonitor(Monitor* monitor); | ||
T* operator->(); | ||
void ReplaceModel(T model); | ||
bool lockAcquired; | ||
private: | ||
Monitor* mon = nullptr; | ||
std::unique_lock<std::mutex> lock; | ||
}; |
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.
struct TryLockedMonitor | |
{ | |
TryLockedMonitor(Monitor* monitor); | |
T* operator->(); | |
void ReplaceModel(T model); | |
bool lockAcquired; | |
private: | |
Monitor* mon = nullptr; | |
std::unique_lock<std::mutex> lock; | |
}; | |
struct SharedMonitor | |
{ | |
SharedMonitor(Monitor* monitor); | |
T const * operator->(); | |
void ReplaceModel(T model); | |
bool lockAcquired; | |
private: | |
Monitor* mon = nullptr; | |
std::shared_lock<std::shared_mutex> lock; | |
}; |
src/core/model/Monitor.cpp
Outdated
template <class T> | ||
void Monitor<T>::TryLockedMonitor::ReplaceModel(T model) { | ||
assert(lockAcquired); | ||
mon->model = model; | ||
} |
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 think, overloading operator== is what we want. Also, for the Monitor class itself.
Co-authored-by: Fabian Keßler <fabian_kessler@gmx.de>
Co-authored-by: Fabian Keßler <fabian_kessler@gmx.de>
@fabian-thomas What is the status of this PR? Making the document thread-safe(r) seems like an important thing to do, so merging a PR along those lines would be great! |
I absolutely agree. Currently I have no time for working on this. Additionnaly, I think that it might be better if someone who has a better overview of the whole codebase would do this. I'm gonna try to finish and merge the other PRs that I've currently open in the next few weeks. |
I need your input on the code I removed from the Document initialisation. I'm gonna comment on what I think the lines were intended to do.
TODO: