-
Notifications
You must be signed in to change notification settings - Fork 9k
Fix output stuttering using a ticket lock #10653
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -866,19 +866,19 @@ WORD Terminal::_TakeVirtualKeyFromLastKeyEvent(const WORD scanCode) noexcept | |
| // Return Value: | ||
| // - a shared_lock which can be used to unlock the terminal. The shared_lock | ||
| // will release this lock when it's destructed. | ||
| [[nodiscard]] std::shared_lock<std::shared_mutex> Terminal::LockForReading() | ||
| [[nodiscard]] std::unique_lock<til::ticket_lock> Terminal::LockForReading() | ||
| { | ||
| return std::shared_lock<std::shared_mutex>(_readWriteLock); | ||
| return std::unique_lock{ _readWriteLock }; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there's no difference any more between Locking for Writing and Reading thanks to the mechanics of the ticket lock... wouldn't it make sense to have one call the other to make that super clear until the future revision where you build that more complicated fair read/write lock for funsies? |
||
| } | ||
|
|
||
| // Method Description: | ||
| // - Acquire a write lock on the terminal. | ||
| // Return Value: | ||
| // - a unique_lock which can be used to unlock the terminal. The unique_lock | ||
| // will release this lock when it's destructed. | ||
| [[nodiscard]] std::unique_lock<std::shared_mutex> Terminal::LockForWriting() | ||
| [[nodiscard]] std::unique_lock<til::ticket_lock> Terminal::LockForWriting() | ||
| { | ||
| return std::unique_lock<std::shared_mutex>(_readWriteLock); | ||
| return std::unique_lock{ _readWriteLock }; | ||
| } | ||
|
|
||
| Viewport Terminal::_GetMutableViewport() const noexcept | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,8 @@ | |
| #include "../../cascadia/terminalcore/ITerminalApi.hpp" | ||
| #include "../../cascadia/terminalcore/ITerminalInput.hpp" | ||
|
|
||
| #include <til/ticket_lock.h> | ||
|
|
||
| static constexpr std::wstring_view linkPattern{ LR"(\b(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|$!:,.;]*[A-Za-z0-9+&@#/%=~_|$])" }; | ||
| static constexpr size_t TaskbarMinProgress{ 10 }; | ||
|
|
||
|
|
@@ -77,8 +79,8 @@ class Microsoft::Terminal::Core::Terminal final : | |
| // WritePastedText goes directly to the connection | ||
| void WritePastedText(std::wstring_view stringView); | ||
|
|
||
| [[nodiscard]] std::shared_lock<std::shared_mutex> LockForReading(); | ||
| [[nodiscard]] std::unique_lock<std::shared_mutex> LockForWriting(); | ||
| [[nodiscard]] std::unique_lock<til::ticket_lock> LockForReading(); | ||
| [[nodiscard]] std::unique_lock<til::ticket_lock> LockForWriting(); | ||
|
|
||
| short GetBufferHeight() const noexcept; | ||
|
|
||
|
|
@@ -244,6 +246,15 @@ class Microsoft::Terminal::Core::Terminal final : | |
| std::function<void()> _pfnWarningBell; | ||
| std::function<void(std::wstring_view)> _pfnTitleChanged; | ||
| std::function<void(std::wstring_view)> _pfnCopyToClipboard; | ||
|
|
||
| // I've specifically put this instance here as it requires | ||
| // alignas(std::hardware_destructive_interference_size) | ||
| // for best performance. | ||
| // | ||
| // But we can abuse the fact that the surrounding members rarely change and are huge | ||
| // (std::function is like 64 bytes) to create some natural padding without wasting space. | ||
| til::ticket_lock _readWriteLock; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you add the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I ask because... this is a micro-optimization somebody could easily accidentally remove or not fully understand and fail to maintain.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the comment is OK. I wonder if we'd ever be possessed to remove all the |
||
|
|
||
| std::function<void(const int, const int, const int)> _pfnScrollPositionChanged; | ||
| std::function<void(const til::color)> _pfnBackgroundColorChanged; | ||
| std::function<void()> _pfnCursorPositionChanged; | ||
|
|
@@ -299,8 +310,6 @@ class Microsoft::Terminal::Core::Terminal final : | |
| SelectionExpansionMode _multiClickSelectionMode; | ||
| #pragma endregion | ||
|
|
||
| std::shared_mutex _readWriteLock; | ||
|
|
||
| // TODO: These members are not shared by an alt-buffer. They should be | ||
| // encapsulated, such that a Terminal can have both a main and alt buffer. | ||
| std::unique_ptr<TextBuffer> _buffer; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
|
|
||
| #pragma once | ||
|
|
||
| namespace til | ||
| { | ||
| template<typename T> | ||
| inline void atomic_wait(const std::atomic<T>& atomic, const T& current, DWORD waitMilliseconds = INFINITE) noexcept | ||
| { | ||
| static_assert(sizeof(atomic) == sizeof(current)); | ||
| #pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile | ||
| WaitOnAddress(const_cast<std::atomic<T>*>(&atomic), const_cast<T*>(¤t), sizeof(current), waitMilliseconds); | ||
| } | ||
|
|
||
| template<typename T> | ||
| inline void atomic_notify_one(const std::atomic<T>& atomic) noexcept | ||
| { | ||
| #pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile | ||
| WakeByAddressSingle(const_cast<std::atomic<T>*>(&atomic)); | ||
| } | ||
|
|
||
| template<typename T> | ||
| inline void atomic_notify_all(const std::atomic<T>& atomic) noexcept | ||
| { | ||
| #pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile | ||
| WakeByAddressAll(const_cast<std::atomic<T>*>(&atomic)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "atomic.h" | ||
|
|
||
| namespace til | ||
| { | ||
| // ticket_lock implements a classic fair lock. | ||
| // | ||
| // Compared to a SRWLOCK this implementation is significantly more unsafe to use: | ||
| // Forgetting to call unlock or calling unlock more than once, will lead to deadlocks, | ||
| // as _now_serving will remain out of sync with _next_ticket and prevent any further lockings. | ||
| // | ||
| // I recommend to use the following with this class: | ||
| // * A low number of concurrent accesses (this lock doesn't scale well beyond 2 threads) | ||
| // * alignas(std::hardware_destructive_interference_size) to prevent false sharing | ||
| // * std::unique_lock or std::scoped_lock to prevent unbalanced lock/unlock calls | ||
| struct ticket_lock | ||
| { | ||
| void lock() noexcept | ||
| { | ||
| const auto ticket = _next_ticket.fetch_add(1, std::memory_order_relaxed); | ||
|
|
||
| for (;;) | ||
| { | ||
| const auto current = _now_serving.load(std::memory_order_acquire); | ||
| if (current == ticket) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is provably no case where now_serving will jump multiple steps above the expected ticket value, correct?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is possible, if someone where to call |
||
| { | ||
| break; | ||
| } | ||
|
|
||
| til::atomic_wait(_now_serving, current); | ||
| } | ||
| } | ||
|
|
||
| void unlock() noexcept | ||
| { | ||
| _now_serving.fetch_add(1, std::memory_order_release); | ||
| til::atomic_notify_all(_now_serving); | ||
| } | ||
|
|
||
| private: | ||
| // You may be inclined to add alignas(std::hardware_destructive_interference_size) | ||
| // here to force the two atomics on separate cache lines, but I suggest to carefully | ||
| // benchmark such a change. Since this ticket_lock is primarily used to synchronize | ||
| // exactly 2 threads, it actually helps us that these atomic are on the same cache line | ||
| // as any change by one thread is flushed to the other, which will then read it anyways. | ||
| // | ||
| // Integer overflow doesn't break the algorithm, as these two | ||
| // atomics are treated more like "IDs" and less like counters. | ||
| std::atomic<uint32_t> _next_ticket{ 0 }; | ||
lhecker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| std::atomic<uint32_t> _now_serving{ 0 }; | ||
| }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.