Diligent Engine  v.2.4.g
ThreadSignal.hpp
Go to the documentation of this file.
1 /*
2  * Copyright 2019-2021 Diligent Graphics LLC
3  * Copyright 2015-2019 Egor Yusov
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  *
17  * In no event and under no legal theory, whether in tort (including negligence),
18  * contract, or otherwise, unless required by applicable law (such as deliberate
19  * and grossly negligent acts) or agreed to in writing, shall any Contributor be
20  * liable for any damages, including any direct, indirect, special, incidental,
21  * or consequential damages of any character arising as a result of this License or
22  * out of the use or inability to use the software (including but not limited to damages
23  * for loss of goodwill, work stoppage, computer failure or malfunction, or any and
24  * all other commercial damages or losses), even if such Contributor has been advised
25  * of the possibility of such damages.
26  */
27 
28 #pragma once
29 
30 #include <mutex>
31 #include <condition_variable>
32 #include <atomic>
33 
34 #include "../../Platforms/Basic/interface/DebugUtilities.hpp"
35 
36 namespace ThreadingTools
37 {
38 
39 class Signal
40 {
41 public:
43  {
44  m_SignaledValue.store(0);
45  m_NumThreadsAwaken.store(0);
46  }
47 
48  // http://en.cppreference.com/w/cpp/thread/condition_variable
49  void Trigger(bool NotifyAll = false, int SignalValue = 1)
50  {
51  VERIFY(SignalValue != 0, "Signal value must not be zero");
52 
53  // The thread that intends to modify the variable has to
54  // * acquire a std::mutex (typically via std::lock_guard)
55  // * perform the modification while the lock is held
56  // * execute notify_one or notify_all on the std::condition_variable (the lock does not need to be held for notification)
57  {
58  // std::condition_variable works only with std::unique_lock<std::mutex>
59  std::lock_guard<std::mutex> Lock{m_Mutex};
60  VERIFY(SignalValue != 0, "Signal value must not be 0");
61  VERIFY(m_SignaledValue.load() == 0 && m_NumThreadsAwaken.load() == 0, "Not all threads have been awaken since the signal was triggered last time, or the signal has not been reset");
62  m_SignaledValue.store(SignalValue);
63  }
64  // Unlocking is done before notifying, to avoid waking up the waiting
65  // thread only to block again (see notify_one for details)
66  if (NotifyAll)
67  m_CondVar.notify_all();
68  else
69  m_CondVar.notify_one();
70  }
71 
72  // WARNING!
73  // If multiple threads are waiting for a signal in an infinite loop,
74  // autoresetting the signal does not guarantee that one thread cannot
75  // go through the loop twice. In this case, every thread must wait for its
76  // own auto-reset signal or the threads must be blocked by another signal
77 
78  int Wait(bool AutoReset = false, int NumThreadsWaiting = 0)
79  {
80  // Any thread that intends to wait on std::condition_variable has to
81  // * acquire a std::unique_lock<std::mutex>, on the SAME MUTEX as used to protect the shared variable
82  // * execute wait, wait_for, or wait_until. The wait operations atomically release the mutex
83  // and suspend the execution of the thread.
84  // * When the condition variable is notified, a timeout expires, or a spurious wakeup occurs,
85  // the thread is awakened, and the mutex is atomically reacquired:
86  // - The thread should then check the condition and resume waiting if the wake up was spurious.
87  std::unique_lock<std::mutex> Lock(m_Mutex);
88  // It is safe to check m_SignaledValue since we are holding
89  // the mutex
90  if (m_SignaledValue.load() == 0)
91  {
92  m_CondVar.wait(Lock, [&] { return m_SignaledValue.load() != 0; });
93  }
94  auto SignaledValue = m_SignaledValue.load();
95  // Update the number of threads awaken while holding the mutex
96  const auto NumThreadsAwaken = m_NumThreadsAwaken.fetch_add(1) + 1;
97  // fetch_add returns the original value immediately preceding the addition.
98  if (AutoReset)
99  {
100  VERIFY(NumThreadsWaiting > 0, "Number of waiting threads must not be 0 when auto resetting the signal");
101  // Reset the signal while holding the mutex. If Trigger() is executed by another
102  // thread, it will wait until we release the mutex
103  if (NumThreadsAwaken == NumThreadsWaiting)
104  {
105  m_SignaledValue.store(0);
106  m_NumThreadsAwaken.store(0);
107  }
108  }
109  return SignaledValue;
110  }
111 
112  void Reset()
113  {
114  std::lock_guard<std::mutex> Lock{m_Mutex};
115  m_SignaledValue.store(0);
116  m_NumThreadsAwaken.store(0);
117  }
118 
119  bool IsTriggered() const { return m_SignaledValue.load() != 0; }
120 
121 private:
122  std::mutex m_Mutex;
123  std::condition_variable m_CondVar;
124  std::atomic_int m_SignaledValue{0};
125  std::atomic_int m_NumThreadsAwaken{0};
126 
127  Signal(const Signal&) = delete;
128  Signal& operator=(const Signal&) = delete;
129 };
130 
131 } // namespace ThreadingTools
ThreadingTools
Definition: LockHelper.hpp:33
ThreadingTools::Signal::Wait
int Wait(bool AutoReset=false, int NumThreadsWaiting=0)
Definition: ThreadSignal.hpp:78
ThreadingTools::Signal::Reset
void Reset()
Definition: ThreadSignal.hpp:112
ThreadingTools::Signal
Definition: ThreadSignal.hpp:39
ThreadingTools::Signal::Signal
Signal()
Definition: ThreadSignal.hpp:42
ThreadingTools::Signal::IsTriggered
bool IsTriggered() const
Definition: ThreadSignal.hpp:119
VERIFY
#define VERIFY(...)
Definition: DebugUtilities.hpp:76
ThreadingTools::Signal::Trigger
void Trigger(bool NotifyAll=false, int SignalValue=1)
Definition: ThreadSignal.hpp:49