Access violations writing task scheduler

Hello,
I am trying to write a task scheduler for an OpenGL engine I am writing.
The idea is that I can have a class with pure virtual functions and a get_task function that gets the function as a task using std::bind. Schedule the task on a thread with the OpenGL context, and the thread will execute the task. Heres my code thus far:

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
220
221
222
223
224
225
226
227
228
229
230
231
232

/*------------------------------- Task Class------------------------------- */ 
template <class R, class ...Args>
class Task;

template <class R, class ...Args>
class Task<R(Args...)> {
public:
	Task(std::function<R(Args...)> t);

	virtual void operator()(Args... args);

	virtual void wait();
	virtual R get();
protected:
	Task();
private:
	mutable std::mutex mu;

	std::future<R> future;
	std::packaged_task<R(Args...)> task;
};

template <class R, class ...Args>
inline Task<R(Args...)>::Task(std::function<R(Args...)> t) : task(t) {
	std::lock_guard<std::mutex> lock(this->mu);
	this->future = this->task.get_future();
}

template <class R, class ...Args>
inline Task<R(Args...)>::Task() {}

template <class R, class ...Args>
inline void Task<R(Args...)>::operator()(Args... args) {
	std::lock_guard<std::mutex> lock(this->mu);
	std::move(this->task)();
}

template <class R, class ...Args>
inline void Task<R(Args...)>::wait() {
	std::lock_guard<std::mutex> lock(this->mu);
	if (!this->future.valid()) {
		throw std::future_error(std::future_errc::no_state);
	}
	this->future.wait();
}

template <class R, class ...Args>
inline R Task<R(Args...)>::get() {
	std::lock_guard<std::mutex> lock(this->mu);
	if (!this->future.valid()) {
		throw std::future_error(std::future_errc::no_state);
	}
	return this->future.get();
}

/*------------------------------- Task Bundle Class------------------------------- */

template<class R, class ...Args>
class TaskBundle;

template<class R, class ...Args>
class TaskBundle<R(Args...)> : public Task<R(Args...)> {
public:
	template <class Task, class ...Rest>
	TaskBundle(Task task, Rest ...rest) : TaskBundle(rest...) {
		this->tasks.push_front(task); 
	}

	void operator()(Args... args) override;

	void wait() override;
protected:
	TaskBundle();
private:
	std::list<std::shared_ptr<Task<R(Args...)>>> tasks;
};



template<class R, class ...Args>
inline TaskBundle<R(Args...)>::TaskBundle() {}

template<class R, class ...Args>
inline void TaskBundle<R(Args...)>::operator()(Args... args) {
	while (!this->tasks.empty()) {
		(*this->tasks.front())(args...);
		this->tasks.pop_front();
	}
}

template<class R, class ...Args>
inline void TaskBundle<R(Args...)>::wait() {
	while (!this->tasks.empty());
}

/*------------------------------- Task Factory Class------------------------------- */

template <class R, class ...Args>
class TaskFactory;

template <class R, class ...Args>
class TaskFactory<R(Args...)> {
public:
	static TaskFactory<R(Args...)>* get_instance();

	template <class ...T>
	std::unique_ptr<TaskBundle<R(Args...)>> create_bundle(T ...task);

	std::unique_ptr<Task<R(Args...)>> create(std::function<R(Args...)> task);
private:
	TaskFactory();
};

template<class R, class ...Args>
inline TaskFactory<R(Args...)>::TaskFactory() {}

template<class R, class ...Args>
inline TaskFactory<R(Args...)>* TaskFactory<R(Args...)>::get_instance() {
	static TaskFactory<R(Args...)> instance;
	return &instance;
}

template<class R, class ...Args>
inline std::unique_ptr<Task<R(Args...)>> TaskFactory<R(Args...)>::create(std::function<R(Args...)> task) {
	//return std::unique_ptr<Task<R(Args...)>>(new Task<R(Args...)>(task));
	return std::make_unique<Task<R(Args...)>>(task);
}

template<class R, class ...Args>
template<class ...T>
inline std::unique_ptr<TaskBundle<R(Args...)>> TaskFactory<R(Args...)>::create_bundle(T ...task) {
	//return std::unique_ptr<TaskBundle<R(Args...)>>(new TaskBundle<R(Args...)>(task...));
	return std::make_unique<TaskBundle<R(Args...)>>(task...);
}

/*------------------------------- Task Manager Class------------------------------- */

template <class R, class ...Args>
class TaskManager;

template <class R, class ...Args>
class TaskManager<R(Args...)> {
public:
	enum Priority {
		LOW = -1, NORMAL = 0, HIGH = 1
	};

	struct PrioritizedTask {
		std::shared_ptr<Task<R(Args...)>> task;
		Priority priority;

		bool operator<(const PrioritizedTask &t) const {
			return this->priority < t.priority;
		}

		bool operator>(const PrioritizedTask &t) const {
			return this->priority > t.priority;
		}

		bool operator==(const PrioritizedTask &t) const {
			return this->priority == t.priority;
		}

		bool operator!=(const PrioritizedTask &t) const {
			return this->priority != t.priority;
		}
	};

	TaskManager();

	bool has_task();

	void execute_top();
	void execute_all();
	void execute_n(unsigned int n);

	void schedule(std::shared_ptr<Task<R(Args...)>> task, Priority priority = Priority::NORMAL);

	template <class T, class ...Rest>
	void schedule_all(T task, Rest ...rest);

private:
	std::mutex mu;
	std::priority_queue<PrioritizedTask, std::vector<PrioritizedTask>, std::less<PrioritizedTask>> queue;
};

template <class R, class ...Args>
inline TaskManager<R(Args...)>::TaskManager() {}

template <class R, class ...Args>
inline bool TaskManager<R(Args...)>::has_task() {
	std::lock_guard<std::mutex> queue_lock(this->mu);
	return !this->queue.empty();
}

template <class R, class ...Args>
inline void TaskManager<R(Args...)>::execute_top() {
	std::lock_guard<std::mutex> queue_lock(this->mu);
	if (!this->queue.empty()) {
		(*this->queue.top().task)();
		this->queue.pop();
	}
}

template <class R, class ...Args>
inline void TaskManager<R(Args...)>::execute_all() {
	while (!this->queue.empty()) {
		this->execute_top();
	}
}

template <class R, class ...Args>
inline void TaskManager<R(Args...)>::execute_n(unsigned int n) {
	for (int i = 0; i < (n <= this->queue.size() ? n : this->queue.size()); i++) {
		this->execute_top();
	}
}

template <class R, class ...Args>
inline void TaskManager<R(Args...)>::schedule(std::shared_ptr<Task<R(Args...)>> task, Priority priority) {
	std::lock_guard<std::mutex> queue_lock(this->mu);
	this->queue.push(PrioritizedTask{ task, priority });
}

template <class R, class ...Args>
template <class T, class ...Rest>
inline void TaskManager<R(Args...)>::schedule_all(T task, Rest ...rest) {
	std::lock_guard<std::mutex> queue_lock(this->mu);
	this->queue.push(task);
	this->schedule_all(rest...);
}


This is an example of a class that has a pure virtual function and provides a task for it:

1
2
3
4
5
6
7
8
9
10
11
12
13
	
class Generator {
public:
	TaskUnique_t generate_task() {
            return TaskFactory_t::get_instance()->create(std::bind(&Generator::generate, this));
         }
         TaskUnique_t destroy_task() {
             return TaskFactory_t::get_instance()->create(std::bind(&Generator::destroy, this));
         }
protected:
	virtual void generate() = 0;
	virtual void destroy() = 0;
};



This works perfectly fine in my test environment, but when I use it in my engine I get access violations. Something is getting deleted and I can't figure out what. My assumption is that it has to do with the 'this' pointer in the std::bind call. Maybe I need an to write my own bind that takes a std::shared_ptr to the object?
This could be very obvious however I am still relatively new to C++.
Last edited on
I would recommend to use asio (independent library) or boost::asio. As far out as it seems because this is a library for networking to most people, this library has something called an io_context which pretty much does what you are trying to do, which is just a manager for calling functions. You queue a function by calling post, and you call the functions by calling run or a single function with poll_one.

For prioritized execution here is an example but it is quite boost intensive, and it uses multiple threads (which shouldn't be too hard to rip out).
https://stackoverflow.com/questions/16311389/boostasio-and-active-object/16346092#16346092

But a problem that I may be naive to bring up since I don't use opengl, but why would you want to call render functions from another thread, or even with a priority? Everything in opengl needs to be sequentially called I believe (unless you are using a depth buffer, but you still need to call in order for menus to render on top).

I think the way how engines work is the logic is multi threaded, but you need to stop the logic when you are doing rendering, or you could have the renderer have its own state of the world which has everything the renderer needs to try to have a lock-free system, but I believe you seriously need to do some bench marking to see if you actually make a performance gain by doing that.

Also what does your debuggee tell you? It should be able to tell you exactly where the line of code breaks if the fault is in your code.

And if you want real help, you should have a full (short) compile-able example (with your test cases), and also spend a little time trying to reproduce it in the small sample (if you haven't already, remember, you must have test cases that actually stress your code out like for example sleeping before calling the functions make it more likely for there to be a collision), and does the first function in the engine crash or is it random? If your debugger refuses to give you any information, you should do some cave man debugging with logs.

Also you have some data races, like execute_all and execute_n access the queue without a lock, but I am unsure if this is your problem, since it doesn't seem like it.

Also note that std::bind does allow shared pointers as a replacement for this. Just make sure you don't call shared_from_this() inside the bind, you need a copy of it before calling it. And don't call it inside a constructor which has enabled shared from this.
Topic archived. No new replies allowed.