aboutsummaryrefslogtreecommitdiff
path: root/core/thread/thread_pool.odin
blob: 15b3a28d2d39c46669747f20ca156bf0c556d9c2 (plain)
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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
package thread

/*
	thread.Pool
	Copyright 2022 eisbehr
	Made available under Odin's BSD-3 license.
*/

import "base:intrinsics"
import "core:sync"
import "core:mem"
import "core:container/queue"

Task_Proc :: #type proc(task: Task)

Task :: struct {
	procedure:  Task_Proc,
	data:       rawptr,
	user_index: int,
	allocator:  mem.Allocator,
}

// Do not access the pool's members directly while the pool threads are running,
// since they use different kinds of locking and mutual exclusion devices.
// Careless access can and will lead to nasty bugs. Once initialized, the
// pool's memory address is not allowed to change until it is destroyed.
Pool :: struct {
	allocator:     mem.Allocator,
	mutex:         sync.Mutex,
	sem_available: sync.Sema,

	// the following values are atomic
	num_waiting:       int,
	num_in_processing: int,
	num_outstanding:   int, // num_waiting + num_in_processing
	num_done:          int,
	// end of atomics

	is_running: bool,

	threads: []^Thread,


	tasks:      queue.Queue(Task),
	tasks_done: [dynamic]Task,
}

Pool_Thread_Data :: struct {
	pool: ^Pool,
	task: Task,
}

@(private="file")
pool_thread_runner :: proc(t: ^Thread) {
	data := cast(^Pool_Thread_Data)t.data
	pool := data.pool

	for intrinsics.atomic_load(&pool.is_running) {
		sync.wait(&pool.sem_available)

		if task, ok := pool_pop_waiting(pool); ok {
			data.task = task
			pool_do_work(pool, task)
			sync.guard(&pool.mutex)
			data.task = {}
		}
	}

	sync.post(&pool.sem_available, 1)
}

// Once initialized, the pool's memory address is not allowed to change until
// it is destroyed.
//
// The thread pool requires an allocator which it either owns, or which is thread safe.
pool_init :: proc(pool: ^Pool, allocator: mem.Allocator, thread_count: int) {
	context.allocator = allocator
	pool.allocator = allocator
	queue.init(&pool.tasks)
	pool.tasks_done = make([dynamic]Task)
	pool.threads    = make([]^Thread, max(thread_count, 1))

	pool.is_running = true

	for _, i in pool.threads {
		t := create(pool_thread_runner)
		data := new(Pool_Thread_Data)
		data.pool = pool
		t.user_index = i
		t.data = data
		pool.threads[i] = t
	}
}

pool_destroy :: proc(pool: ^Pool) {
	queue.destroy(&pool.tasks)
	delete(pool.tasks_done)

	for &t in pool.threads {
		data := cast(^Pool_Thread_Data)t.data
		free(data, pool.allocator)
		destroy(t)
	}

	delete(pool.threads, pool.allocator)
}

pool_start :: proc(pool: ^Pool) {
	for t in pool.threads {
		start(t)
	}
}

// Finish tasks that have already started processing, then shut down all pool
// threads. Might leave over waiting tasks, any memory allocated for the
// user data of those tasks will not be freed.
pool_join :: proc(pool: ^Pool) {
	intrinsics.atomic_store(&pool.is_running, false)
	sync.post(&pool.sem_available, len(pool.threads))

	yield()

	unstarted_count: int
	for t in pool.threads {
		flags := intrinsics.atomic_load(&t.flags)
		if .Started not_in flags {
			unstarted_count += 1
		}
	}

	// most likely the user forgot to call `pool_start`
	// exit here, so we don't hang forever
	if len(pool.threads) == unstarted_count {
		return
	}

	started_count: int
	for started_count < len(pool.threads) {
		started_count = 0
		for t in pool.threads {
			flags := intrinsics.atomic_load(&t.flags)
			if .Started in flags {
				started_count += 1
				if .Joined not_in flags {
					join(t)
				}
			}
		}
	}
}

// Add a task to the thread pool.
//
// Tasks can be added from any thread, not just the thread that created
// the thread pool. You can even add tasks from inside other tasks.
//
// Each task also needs an allocator which it either owns, or which is thread
// safe.
pool_add_task :: proc(pool: ^Pool, allocator: mem.Allocator, procedure: Task_Proc, data: rawptr, user_index: int = 0) {
	sync.guard(&pool.mutex)

	queue.push_back(&pool.tasks, Task{
		procedure  = procedure,
		data       = data,
		user_index = user_index,
		allocator  = allocator,
	})
	intrinsics.atomic_add(&pool.num_waiting, 1)
	intrinsics.atomic_add(&pool.num_outstanding, 1)
	sync.post(&pool.sem_available, 1)
}

// Forcibly stop a running task by its user index.
//
// This will terminate the underlying thread. Ideally, you should use some
// means of communication to stop a task, as thread termination may leave
// resources unclaimed.
//
// The thread will be restarted to accept new tasks.
//
// Returns true if the task was found and terminated.
pool_stop_task :: proc(pool: ^Pool, user_index: int, exit_code: int = 1) -> bool {
	sync.guard(&pool.mutex)

	for t, i in pool.threads {
		data := cast(^Pool_Thread_Data)t.data
		if data.task.user_index == user_index && data.task.procedure != nil {
			terminate(t, exit_code)

			append(&pool.tasks_done, data.task)
			intrinsics.atomic_add(&pool.num_done, 1)
			intrinsics.atomic_sub(&pool.num_outstanding, 1)
			intrinsics.atomic_sub(&pool.num_in_processing, 1)

			old_thread_user_index := t.user_index

			destroy(t)

			replacement := create(pool_thread_runner)
			replacement.user_index = old_thread_user_index
			replacement.data = data
			data.task = {}
			pool.threads[i] = replacement

			start(replacement)
			return true
		}
	}

	return false
}

// Forcibly stop all running tasks.
//
// The same notes from `pool_stop_task` apply here.
pool_stop_all_tasks :: proc(pool: ^Pool, exit_code: int = 1) {
	sync.guard(&pool.mutex)

	for t, i in pool.threads {
		data := cast(^Pool_Thread_Data)t.data
		if data.task.procedure != nil {
			terminate(t, exit_code)

			append(&pool.tasks_done, data.task)
			intrinsics.atomic_add(&pool.num_done, 1)
			intrinsics.atomic_sub(&pool.num_outstanding, 1)
			intrinsics.atomic_sub(&pool.num_in_processing, 1)

			old_thread_user_index := t.user_index

			destroy(t)

			replacement := create(pool_thread_runner)
			replacement.user_index = old_thread_user_index
			replacement.data = data
			data.task = {}
			pool.threads[i] = replacement

			start(replacement)
		}
	}
}

// Force the pool to stop all of its threads and put it into a state where
// it will no longer run any more tasks.
//
// The pool must still be destroyed after this.
pool_shutdown :: proc(pool: ^Pool, exit_code: int = 1) {
	intrinsics.atomic_store(&pool.is_running, false)
	sync.guard(&pool.mutex)

	for t in pool.threads {
		terminate(t, exit_code)

		data := cast(^Pool_Thread_Data)t.data
		if data.task.procedure != nil {
			append(&pool.tasks_done, data.task)
			intrinsics.atomic_add(&pool.num_done, 1)
			intrinsics.atomic_sub(&pool.num_outstanding, 1)
			intrinsics.atomic_sub(&pool.num_in_processing, 1)
		}
	}
}

// Number of tasks waiting to be processed. Only informational, mostly for
// debugging. Don't rely on this value being consistent with other num_*
// values.
pool_num_waiting :: #force_inline proc(pool: ^Pool) -> int {
	return intrinsics.atomic_load(&pool.num_waiting)
}

// Number of tasks currently being processed. Only informational, mostly for
// debugging. Don't rely on this value being consistent with other num_*
// values.
pool_num_in_processing :: #force_inline proc(pool: ^Pool) -> int {
	return intrinsics.atomic_load(&pool.num_in_processing)
}

// Outstanding tasks are all tasks that are not done, that is, tasks that are
// waiting, as well as tasks that are currently being processed. Only
// informational, mostly for debugging. Don't rely on this value being
// consistent with other num_* values.
pool_num_outstanding :: #force_inline proc(pool: ^Pool) -> int {
	return intrinsics.atomic_load(&pool.num_outstanding)
}

// Number of tasks which are done processing. Only informational, mostly for
// debugging. Don't rely on this value being consistent with other num_*
// values.
pool_num_done :: #force_inline proc(pool: ^Pool) -> int {
	return intrinsics.atomic_load(&pool.num_done)
}

// If tasks are only being added from one thread, and this procedure is being
// called from that same thread, it will reliably tell if the thread pool is
// empty or not. Empty in this case means there are no tasks waiting, being
// processed, or _done_.
pool_is_empty :: #force_inline proc(pool: ^Pool) -> bool {
	return pool_num_outstanding(pool) == 0 && pool_num_done(pool) == 0
}

// Mostly for internal use.
pool_pop_waiting :: proc(pool: ^Pool) -> (task: Task, got_task: bool) {
	sync.guard(&pool.mutex)

	if queue.len(pool.tasks) != 0 {
		intrinsics.atomic_sub(&pool.num_waiting, 1)
		intrinsics.atomic_add(&pool.num_in_processing, 1)
		task = queue.pop_front(&pool.tasks)
		got_task = true
	}

	return
}

// Use this to take out finished tasks.
pool_pop_done :: proc(pool: ^Pool) -> (task: Task, got_task: bool) {
	sync.guard(&pool.mutex)

	if len(pool.tasks_done) != 0 {
		task = pop_front(&pool.tasks_done)
		got_task = true
		intrinsics.atomic_sub(&pool.num_done, 1)
	}

	return
}

// Mostly for internal use.
pool_do_work :: proc(pool: ^Pool, task: Task) {
	{
		context.allocator = task.allocator
		task.procedure(task)
	}

	sync.guard(&pool.mutex)

	append(&pool.tasks_done, task)
	intrinsics.atomic_add(&pool.num_done, 1)
	intrinsics.atomic_sub(&pool.num_outstanding, 1)
	intrinsics.atomic_sub(&pool.num_in_processing, 1)
}

// Process the rest of the tasks, also use this thread for processing, then join
// all the pool threads.
pool_finish :: proc(pool: ^Pool) {
	for task in pool_pop_waiting(pool) {
		pool_do_work(pool, task)
	}
	pool_join(pool)
}