-
Notifications
You must be signed in to change notification settings - Fork 278
Replace lock-free ThreadLocalStorage with SRW lock-per-bucket implementation #592
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
base: master
Are you sure you want to change the base?
Changes from all commits
6f9c71d
c1b95d6
7852833
e426334
55a6a55
7f07b5b
f207460
5d3842b
d5d40a5
05fac10
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 |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| . |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -375,73 +375,91 @@ namespace details_abi | |
| template <typename T> | ||
| class ThreadLocalStorage | ||
| { | ||
| public: | ||
| ThreadLocalStorage(const ThreadLocalStorage&) = delete; | ||
| ThreadLocalStorage& operator=(const ThreadLocalStorage&) = delete; | ||
|
|
||
| ThreadLocalStorage() = default; | ||
| struct Node | ||
| { | ||
| Node* next{nullptr}; | ||
| DWORD threadId = 0xffffffffU; | ||
| T value{}; | ||
| }; | ||
|
|
||
| ~ThreadLocalStorage() WI_NOEXCEPT | ||
| struct Bucket | ||
| { | ||
| for (auto& entry : m_hashArray) | ||
| wil::srwlock lock; | ||
| Node* head{nullptr}; | ||
|
|
||
| ~Bucket() WI_NOEXCEPT | ||
| { | ||
| Node* pNode = entry; | ||
| while (pNode != nullptr) | ||
| // Cleanup in a loop rather than recursively | ||
| while (head) | ||
| { | ||
| auto pCurrent = pNode; | ||
| #pragma warning(push) | ||
| #pragma warning(disable : 6001) // https://github.com/microsoft/wil/issues/164 | ||
| pNode = pNode->pNext; | ||
| #pragma warning(pop) | ||
| pCurrent->~Node(); | ||
| ::HeapFree(::GetProcessHeap(), 0, pCurrent); | ||
| auto tmp = head; | ||
| head = tmp->next; | ||
| tmp->~Node(); | ||
| details::FreeProcessHeap(tmp); | ||
| } | ||
| entry = nullptr; | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| Bucket m_hashArray[13]{}; | ||
|
|
||
| public: | ||
| ThreadLocalStorage(const ThreadLocalStorage&) = delete; | ||
| ThreadLocalStorage& operator=(const ThreadLocalStorage&) = delete; | ||
|
|
||
| ThreadLocalStorage() = default; | ||
| ~ThreadLocalStorage() WI_NOEXCEPT = default; | ||
|
|
||
| // Note: Can return nullptr even when (shouldAllocate == true) upon allocation failure | ||
| T* GetLocal(bool shouldAllocate = false) WI_NOEXCEPT | ||
| { | ||
| // Get the current thread ID | ||
|
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. Seems like a pretty unnecessary comment |
||
| DWORD const threadId = ::GetCurrentThreadId(); | ||
|
|
||
| // Determine the appropriate bucket for this thread | ||
| size_t const index = ((threadId >> 2) % ARRAYSIZE(m_hashArray)); // Reduce hash collisions; thread IDs are even. | ||
| for (auto pNode = m_hashArray[index]; pNode != nullptr; pNode = pNode->pNext) | ||
| Bucket& bucket = m_hashArray[index]; | ||
|
|
||
| // Lock the bucket and search for an existing entry | ||
| { | ||
| if (pNode->threadId == threadId) | ||
| auto lock = bucket.lock.lock_shared(); | ||
| for (auto pNode = bucket.head; pNode != nullptr; pNode = pNode->next) | ||
| { | ||
| return &pNode->value; | ||
| if (pNode->threadId == threadId) | ||
| { | ||
| return &pNode->value; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (shouldAllocate) | ||
| if (!shouldAllocate) | ||
| { | ||
| if (auto pNewRaw = details::ProcessHeapAlloc(0, sizeof(Node))) | ||
| { | ||
| auto pNew = new (pNewRaw) Node{threadId}; | ||
| return nullptr; | ||
| } | ||
|
|
||
| Node* pFirst; | ||
| do | ||
| { | ||
| pFirst = m_hashArray[index]; | ||
| pNew->pNext = pFirst; | ||
| } while (::InterlockedCompareExchangePointer(reinterpret_cast<PVOID volatile*>(m_hashArray + index), pNew, pFirst) != | ||
| pFirst); | ||
| // No entry for us, make a new one and insert it at the head | ||
| void* newNodeStore = details::ProcessHeapAlloc(0, sizeof(Node)); | ||
| if (!newNodeStore) | ||
| { | ||
| return nullptr; | ||
| } | ||
| auto node = new (newNodeStore) Node{nullptr, threadId}; | ||
|
|
||
| return &pNew->value; | ||
| // Look again and insert the new node | ||
|
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. It's looking for a matching thread id... Unless we're worried about interrupts (in which case deadlock is now on the table), this additional check seems unnecessary since the same thread can't race against itself.
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. Oh, good point. It can just acquire the lock and shove it into the head. |
||
| auto lock = bucket.lock.lock_exclusive(); | ||
| for (auto pNode = bucket.head; pNode != nullptr; pNode = pNode->next) | ||
| { | ||
| if (pNode->threadId == threadId) | ||
| { | ||
| node->~Node(); | ||
| details::FreeProcessHeap(node); | ||
| return &pNode->value; | ||
| } | ||
| } | ||
| return nullptr; | ||
| } | ||
|
|
||
| private: | ||
| struct Node | ||
| { | ||
| DWORD threadId = 0xffffffffU; | ||
| Node* pNext = nullptr; | ||
| T value{}; | ||
| }; | ||
|
|
||
| Node* volatile m_hashArray[10]{}; | ||
| node->next = bucket.head; | ||
| bucket.head = node; | ||
| return &bucket.head->value; | ||
| } | ||
| }; | ||
|
|
||
| struct ThreadLocalFailureInfo | ||
|
|
||
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.
noexceptis the defaultThere 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.
Lol. Thanks Copilot. Teaches me to review closer.
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.
To be fair, it was like this before, so not necessarily Copilot's fault (this time)
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.
Note that there is /Zc:implicitNoexcept- which disables the implicit noexcept on destructors.
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.
If folks are disabling language features, this one instance isn't going to have much of a relative impact on them...