File: BlockingLoop.h

package info (click to toggle)
dolphin-emu 5.0%2Bdfsg-5
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 29,052 kB
  • sloc: cpp: 213,146; java: 6,252; asm: 2,277; xml: 1,998; ansic: 1,514; python: 462; sh: 279; pascal: 247; makefile: 124; perl: 97
file content (219 lines) | stat: -rw-r--r-- 6,335 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#pragma once

#include <atomic>
#include <mutex>
#include <thread>

#include "Common/Event.h"
#include "Common/Flag.h"

namespace Common
{

// This class provides a synchronized loop.
// It's a thread-safe way to trigger a new iteration without busy loops.
// It's optimized for high-usage iterations which usually are already running while it's triggered often.
// Be careful when using Wait() and Wakeup() at the same time. Wait() may block forever while Wakeup() is called regularly.
class BlockingLoop
{
public:
	BlockingLoop()
	{
		m_stopped.Set();
	}

	~BlockingLoop()
	{
		Stop();
	}

	// Triggers to rerun the payload of the Run() function at least once again.
	// This function will never block and is designed to finish as fast as possible.
	void Wakeup()
	{
		// Already running, so no need for a wakeup.
		// This is the common case, so try to get this as fast as possible.
		if (m_running_state.load() >= STATE_NEED_EXECUTION)
			return;

		// Mark that new data is available. If the old state will rerun the payload
		// itself, we don't have to set the event to interrupt the worker.
		if (m_running_state.exchange(STATE_NEED_EXECUTION) != STATE_SLEEPING)
			return;

		// Else as the worker thread may sleep now, we have to set the event.
		m_new_work_event.Set();
	}

	// Wait for a complete payload run after the last Wakeup() call.
	// If stopped, this returns immediately.
	void Wait()
	{
		// already done
		if (IsDone())
			return;

		// notifying this event will only wake up one thread, so use a mutex here to
		// allow only one waiting thread. And in this way, we get an event free wakeup
		// but for the first thread for free
		std::lock_guard<std::mutex> lk(m_wait_lock);

		// Wait for the worker thread to finish.
		while (!IsDone())
		{
			m_done_event.Wait();
		}

		// As we wanted to wait for the other thread, there is likely no work remaining.
		// So there is no need for a busy loop any more.
		m_may_sleep.Set();
	}

	// Half start the worker.
	// So this object is in a running state and Wait() will block until the worker calls Run().
	// This may be called from any thread and is supposed to be called at least once before Wait() is used.
	void Prepare()
	{
		// There is a race condition if the other threads call this function while
		// the loop thread is initializing. Using this lock will ensure a valid state.
		std::lock_guard<std::mutex> lk(m_prepare_lock);

		if (!m_stopped.TestAndClear())
			return;
		m_running_state.store(STATE_LAST_EXECUTION); // so the payload will only be executed once without any Wakeup call
		m_shutdown.Clear();
		m_may_sleep.Set();
	}

	// Main loop of this object.
	// The payload callback is called at least as often as it's needed to match the Wakeup() requirements.
	// The optional timeout parameter is a timeout for how periodically the payload should be called.
	// Use timeout = 0 to run without a timeout at all.
	template<class F> void Run(F payload, int64_t timeout = 0)
	{
		// Asserts that Prepare is called at least once before we enter the loop.
		// But a good implementation should call this before already.
		Prepare();

		while (!m_shutdown.IsSet())
		{
			payload();

			switch (m_running_state.load())
			{
				case STATE_NEED_EXECUTION:
					// We won't get notified while we are in the STATE_NEED_EXECUTION state, so maybe Wakeup was called.
					// So we have to assume on finishing the STATE_NEED_EXECUTION state, that there may be some remaining tasks.
					// To process this tasks, we call the payload again within the STATE_LAST_EXECUTION state.
					m_running_state--;
					break;

				case STATE_LAST_EXECUTION:
					// If we're still in the STATE_LAST_EXECUTION state, then Wakeup wasn't called within the last
					// execution of the payload. This means we should be ready now.
					// But bad luck, Wakeup may have been called right now. So break and rerun the payload
					// if the state was touched.
					if (m_running_state-- != STATE_LAST_EXECUTION)
						break;

					// Else we're likely in the STATE_DONE state now, so wakeup the waiting threads right now.
					// However, if we're not in the STATE_DONE state any more, the event should also be
					// triggered so that we'll skip the next waiting call quite fast.
					m_done_event.Set();

				case STATE_DONE:
					// We're done now. So time to check if we want to sleep or if we want to stay in a busy loop.
					if (m_may_sleep.TestAndClear())
					{
						// Try to set the sleeping state.
						if (m_running_state-- != STATE_DONE)
							break;
					}
					else
					{
						// Busy loop.
						break;
					}

				case STATE_SLEEPING:
					// Just relax
					if (timeout > 0)
					{
						m_new_work_event.WaitFor(std::chrono::milliseconds(timeout));
					}
					else
					{
						m_new_work_event.Wait();
					}
					break;
			}
		}

		// Shutdown down, so get a safe state
		m_running_state.store(STATE_DONE);
		m_stopped.Set();

		// Wake up the last Wait calls.
		m_done_event.Set();
	}

	// Quits the main loop.
	// By default, it will wait until the main loop quits.
	// Be careful to not use the blocking way within the payload of the Run() method.
	void Stop(bool block = true)
	{
		if (m_stopped.IsSet())
			return;

		m_shutdown.Set();

		// We have to interrupt the sleeping call to let the worker shut down soon.
		Wakeup();

		if (block)
			Wait();
	}

	bool IsRunning() const
	{
		return !m_stopped.IsSet() && !m_shutdown.IsSet();
	}

	bool IsDone() const
	{
		return m_stopped.IsSet() || m_running_state.load() <= STATE_DONE;
	}

	// This function should be triggered regularly over time so
	// that we will fall back from the busy loop to sleeping.
	void AllowSleep()
	{
		m_may_sleep.Set();
	}

private:
	std::mutex m_wait_lock;
	std::mutex m_prepare_lock;

	Flag m_stopped;  // If this is set, Wait() shall not block.
	Flag m_shutdown; // If this is set, the loop shall end.

	Event m_new_work_event;
	Event m_done_event;

	enum RUNNING_TYPE {
		STATE_SLEEPING = 0,
		STATE_DONE = 1,
		STATE_LAST_EXECUTION = 2,
		STATE_NEED_EXECUTION = 3
	};
	std::atomic<int> m_running_state; // must be of type RUNNING_TYPE

	Flag m_may_sleep; // If this is set, we fall back from the busy loop to an event based synchronization.
};

}