thread.odin 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. // Multi-threading operations to spawn threads and thread pools.
  2. package thread
  3. import "base:runtime"
  4. import "core:mem"
  5. import "base:intrinsics"
  6. _ :: intrinsics
  7. /*
  8. Value, specifying whether `core:thread` functionality is available on the
  9. current platform.
  10. */
  11. IS_SUPPORTED :: _IS_SUPPORTED
  12. /*
  13. Type for a procedure that will be run in a thread, after that thread has been
  14. started.
  15. */
  16. Thread_Proc :: #type proc(^Thread)
  17. /*
  18. Maximum number of user arguments for polymorphic thread procedures.
  19. */
  20. MAX_USER_ARGUMENTS :: 8
  21. /*
  22. Type representing the state/flags of the thread.
  23. */
  24. Thread_State :: enum u8 {
  25. Started,
  26. Joined,
  27. Done,
  28. Self_Cleanup,
  29. }
  30. /*
  31. Type representing a thread handle and the associated with that thread data.
  32. */
  33. Thread :: struct {
  34. using specific: Thread_Os_Specific,
  35. flags: bit_set[Thread_State; u8],
  36. // Thread ID. Depending on the platform, may start out as 0 (zero) until the thread
  37. // has had a chance to run.
  38. id: int,
  39. // The thread procedure.
  40. procedure: Thread_Proc,
  41. // User-supplied pointer, that will be available to the thread once it is
  42. // started. Should be set after the thread has been created, but before
  43. // it is started.
  44. data: rawptr,
  45. // User-supplied integer, that will be available to the thread once it is
  46. // started. Should be set after the thread has been created, but before
  47. // it is started.
  48. user_index: int,
  49. // User-supplied array of arguments, that will be available to the thread,
  50. // once it is started. Should be set after the thread has been created,
  51. // but before it is started.
  52. user_args: [MAX_USER_ARGUMENTS]rawptr,
  53. // The thread context.
  54. // This field can be assigned to directly, after the thread has been
  55. // created, but __before__ the thread has been started. This field must
  56. // not be changed after the thread has started.
  57. //
  58. // **Note**: If this field is **not** set, the temp allocator will be managed
  59. // automatically. If it is set, the allocators must be handled manually.
  60. //
  61. // **IMPORTANT**:
  62. // By default, the thread proc will get the same context as `main()` gets.
  63. // In this situation, the thread will get a new temporary allocator which
  64. // will be cleaned up when the thread dies. ***This does NOT happen when
  65. // `init_context` field is initialized***.
  66. //
  67. // If `init_context` is initialized, and `temp_allocator` field is set to
  68. // the default temp allocator, then `runtime.default_temp_allocator_destroy()`
  69. // procedure needs to be called from the thread procedure, in order to prevent
  70. // any memory leaks.
  71. init_context: Maybe(runtime.Context),
  72. // The allocator used to allocate data for the thread.
  73. creation_allocator: mem.Allocator,
  74. }
  75. when IS_SUPPORTED {
  76. #assert(size_of(Thread{}.user_index) == size_of(uintptr))
  77. }
  78. /*
  79. Type representing priority of a thread.
  80. */
  81. Thread_Priority :: enum {
  82. Normal,
  83. Low,
  84. High,
  85. }
  86. /*
  87. Create a thread in a suspended state with the given priority.
  88. This procedure creates a thread that will be set to run the procedure
  89. specified by `procedure` parameter with a specified priority. The returned
  90. thread will be in a suspended state, until `start()` procedure is called.
  91. To start the thread, call `start()`. Also the `create_and_start()`
  92. procedure can be called to create and start the thread immediately.
  93. */
  94. create :: proc(procedure: Thread_Proc, priority := Thread_Priority.Normal) -> ^Thread {
  95. return _create(procedure, priority)
  96. }
  97. /*
  98. Wait for the thread to finish and free all data associated with it.
  99. */
  100. destroy :: proc(thread: ^Thread) {
  101. _destroy(thread)
  102. }
  103. /*
  104. Start a suspended thread.
  105. */
  106. start :: proc(thread: ^Thread) {
  107. _start(thread)
  108. }
  109. /*
  110. Check if the thread has finished work.
  111. */
  112. is_done :: proc(thread: ^Thread) -> bool {
  113. return _is_done(thread)
  114. }
  115. /*
  116. Wait for the thread to finish work.
  117. */
  118. join :: proc(thread: ^Thread) {
  119. _join(thread)
  120. }
  121. /*
  122. Wait for all threads to finish work.
  123. */
  124. join_multiple :: proc(threads: ..^Thread) {
  125. _join_multiple(..threads)
  126. }
  127. /*
  128. Forcibly terminate a running thread.
  129. */
  130. terminate :: proc(thread: ^Thread, exit_code: int) {
  131. _terminate(thread, exit_code)
  132. }
  133. /*
  134. Yield the execution of the current thread to another OS thread or process.
  135. */
  136. yield :: proc() {
  137. _yield()
  138. }
  139. /*
  140. Run a procedure on a different thread.
  141. This procedure runs the given procedure on another thread. The context
  142. specified by `init_context` will be used as the context in which `fn` is going
  143. to execute. The thread will have priority specified by the `priority` parameter.
  144. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  145. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  146. in order to free the resources associated with the temporary allocations.
  147. */
  148. run :: proc(fn: proc(), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal) {
  149. create_and_start(fn, init_context, priority, true)
  150. }
  151. /*
  152. Run a procedure with one pointer parameter on a different thread.
  153. This procedure runs the given procedure on another thread. The context
  154. specified by `init_context` will be used as the context in which `fn` is going
  155. to execute. The thread will have priority specified by the `priority` parameter.
  156. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  157. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  158. in order to free the resources associated with the temporary allocations.
  159. */
  160. run_with_data :: proc(data: rawptr, fn: proc(data: rawptr), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal) {
  161. create_and_start_with_data(data, fn, init_context, priority, true)
  162. }
  163. /*
  164. Run a procedure with one polymorphic parameter on a different thread.
  165. This procedure runs the given procedure on another thread. The context
  166. specified by `init_context` will be used as the context in which `fn` is going
  167. to execute. The thread will have priority specified by the `priority` parameter.
  168. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  169. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  170. in order to free the resources associated with the temporary allocations.
  171. */
  172. run_with_poly_data :: proc(data: $T, fn: proc(data: T), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal)
  173. where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS {
  174. create_and_start_with_poly_data(data, fn, init_context, priority, true)
  175. }
  176. /*
  177. Run a procedure with two polymorphic parameters on a different thread.
  178. This procedure runs the given procedure on another thread. The context
  179. specified by `init_context` will be used as the context in which `fn` is going
  180. to execute. The thread will have priority specified by the `priority` parameter.
  181. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  182. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  183. in order to free the resources associated with the temporary allocations.
  184. */
  185. run_with_poly_data2 :: proc(arg1: $T1, arg2: $T2, fn: proc(T1, T2), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal)
  186. where size_of(T1) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS {
  187. create_and_start_with_poly_data2(arg1, arg2, fn, init_context, priority, true)
  188. }
  189. /*
  190. Run a procedure with three polymorphic parameters on a different thread.
  191. This procedure runs the given procedure on another thread. The context
  192. specified by `init_context` will be used as the context in which `fn` is going
  193. to execute. The thread will have priority specified by the `priority` parameter.
  194. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  195. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  196. in order to free the resources associated with the temporary allocations.
  197. */
  198. run_with_poly_data3 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, fn: proc(arg1: T1, arg2: T2, arg3: T3), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal)
  199. where size_of(T1) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS {
  200. create_and_start_with_poly_data3(arg1, arg2, arg3, fn, init_context, priority, true)
  201. }
  202. /*
  203. Run a procedure with four polymorphic parameters on a different thread.
  204. This procedure runs the given procedure on another thread. The context
  205. specified by `init_context` will be used as the context in which `fn` is going
  206. to execute. The thread will have priority specified by the `priority` parameter.
  207. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  208. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  209. in order to free the resources associated with the temporary allocations.
  210. */
  211. run_with_poly_data4 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, arg4: $T4, fn: proc(arg1: T1, arg2: T2, arg3: T3, arg4: T4), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal)
  212. where size_of(T1) + size_of(T2) + size_of(T3) + size_of(T4) <= size_of(rawptr) * MAX_USER_ARGUMENTS {
  213. create_and_start_with_poly_data4(arg1, arg2, arg3, arg4, fn, init_context, priority, true)
  214. }
  215. /*
  216. Run a procedure on a different thread.
  217. This procedure runs the given procedure on another thread. The context
  218. specified by `init_context` will be used as the context in which `fn` is going
  219. to execute. The thread will have priority specified by the `priority` parameter.
  220. If `self_cleanup` is specified, after the thread finishes the execution of the
  221. `fn` procedure, the resources associated with the thread are going to be
  222. automatically freed.
  223. **Do not** dereference the `^Thread` pointer, if this flag is specified.
  224. That includes calling `join`, which needs to dereference ^Thread`.
  225. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  226. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  227. in order to free the resources associated with the temporary allocations.
  228. */
  229. create_and_start :: proc(fn: proc(), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread) {
  230. thread_proc :: proc(t: ^Thread) {
  231. fn := cast(proc())t.data
  232. fn()
  233. }
  234. if t = create(thread_proc, priority); t == nil {
  235. return
  236. }
  237. t.data = rawptr(fn)
  238. if self_cleanup {
  239. intrinsics.atomic_or(&t.flags, {.Self_Cleanup})
  240. }
  241. t.init_context = init_context
  242. start(t)
  243. return t
  244. }
  245. /*
  246. Run a procedure with one pointer parameter on a different thread.
  247. This procedure runs the given procedure on another thread. The context
  248. specified by `init_context` will be used as the context in which `fn` is going
  249. to execute. The thread will have priority specified by the `priority` parameter.
  250. If `self_cleanup` is specified, after the thread finishes the execution of the
  251. `fn` procedure, the resources associated with the thread are going to be
  252. automatically freed.
  253. **Do not** dereference the `^Thread` pointer, if this flag is specified.
  254. That includes calling `join`, which needs to dereference ^Thread`.
  255. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  256. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  257. in order to free the resources associated with the temporary allocations.
  258. */
  259. create_and_start_with_data :: proc(data: rawptr, fn: proc(data: rawptr), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread) {
  260. thread_proc :: proc(t: ^Thread) {
  261. fn := cast(proc(rawptr))t.data
  262. assert(t.user_index >= 1)
  263. data := t.user_args[0]
  264. fn(data)
  265. }
  266. if t = create(thread_proc, priority); t == nil {
  267. return
  268. }
  269. t.data = rawptr(fn)
  270. t.user_index = 1
  271. t.user_args[0] = data
  272. if self_cleanup {
  273. intrinsics.atomic_or(&t.flags, {.Self_Cleanup})
  274. }
  275. t.init_context = init_context
  276. start(t)
  277. return t
  278. }
  279. /*
  280. Run a procedure with one polymorphic parameter on a different thread.
  281. This procedure runs the given procedure on another thread. The context
  282. specified by `init_context` will be used as the context in which `fn` is going
  283. to execute. The thread will have priority specified by the `priority` parameter.
  284. If `self_cleanup` is specified, after the thread finishes the execution of the
  285. `fn` procedure, the resources associated with the thread are going to be
  286. automatically freed.
  287. **Do not** dereference the `^Thread` pointer, if this flag is specified.
  288. That includes calling `join`, which needs to dereference ^Thread`.
  289. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  290. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  291. in order to free the resources associated with the temporary allocations.
  292. */
  293. create_and_start_with_poly_data :: proc(data: $T, fn: proc(data: T), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread)
  294. where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS {
  295. thread_proc :: proc(t: ^Thread) {
  296. fn := cast(proc(T))t.data
  297. assert(t.user_index >= 1)
  298. data := (^T)(&t.user_args[0])^
  299. fn(data)
  300. }
  301. if t = create(thread_proc, priority); t == nil {
  302. return
  303. }
  304. t.data = rawptr(fn)
  305. t.user_index = 1
  306. data := data
  307. mem.copy(&t.user_args[0], &data, size_of(T))
  308. if self_cleanup {
  309. intrinsics.atomic_or(&t.flags, {.Self_Cleanup})
  310. }
  311. t.init_context = init_context
  312. start(t)
  313. return t
  314. }
  315. /*
  316. Run a procedure with two polymorphic parameters on a different thread.
  317. This procedure runs the given procedure on another thread. The context
  318. specified by `init_context` will be used as the context in which `fn` is going
  319. to execute. The thread will have priority specified by the `priority` parameter.
  320. If `self_cleanup` is specified, after the thread finishes the execution of the
  321. `fn` procedure, the resources associated with the thread are going to be
  322. automatically freed.
  323. **Do not** dereference the `^Thread` pointer, if this flag is specified.
  324. That includes calling `join`, which needs to dereference ^Thread`.
  325. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  326. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  327. in order to free the resources associated with the temporary allocations.
  328. */
  329. create_and_start_with_poly_data2 :: proc(arg1: $T1, arg2: $T2, fn: proc(T1, T2), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread)
  330. where size_of(T1) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS {
  331. thread_proc :: proc(t: ^Thread) {
  332. fn := cast(proc(T1, T2))t.data
  333. assert(t.user_index >= 2)
  334. user_args := mem.slice_to_bytes(t.user_args[:])
  335. arg1 := (^T1)(raw_data(user_args))^
  336. arg2 := (^T2)(raw_data(user_args[size_of(T1):]))^
  337. fn(arg1, arg2)
  338. }
  339. if t = create(thread_proc, priority); t == nil {
  340. return
  341. }
  342. t.data = rawptr(fn)
  343. t.user_index = 2
  344. arg1, arg2 := arg1, arg2
  345. user_args := mem.slice_to_bytes(t.user_args[:])
  346. n := copy(user_args, mem.ptr_to_bytes(&arg1))
  347. _ = copy(user_args[n:], mem.ptr_to_bytes(&arg2))
  348. if self_cleanup {
  349. intrinsics.atomic_or(&t.flags, {.Self_Cleanup})
  350. }
  351. t.init_context = init_context
  352. start(t)
  353. return t
  354. }
  355. /*
  356. Run a procedure with three polymorphic parameters on a different thread.
  357. This procedure runs the given procedure on another thread. The context
  358. specified by `init_context` will be used as the context in which `fn` is going
  359. to execute. The thread will have priority specified by the `priority` parameter.
  360. If `self_cleanup` is specified, after the thread finishes the execution of the
  361. `fn` procedure, the resources associated with the thread are going to be
  362. automatically freed.
  363. **Do not** dereference the `^Thread` pointer, if this flag is specified.
  364. That includes calling `join`, which needs to dereference ^Thread`.
  365. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  366. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  367. in order to free the resources associated with the temporary allocations.
  368. */
  369. create_and_start_with_poly_data3 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, fn: proc(arg1: T1, arg2: T2, arg3: T3), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread)
  370. where size_of(T1) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS {
  371. thread_proc :: proc(t: ^Thread) {
  372. fn := cast(proc(T1, T2, T3))t.data
  373. assert(t.user_index >= 3)
  374. user_args := mem.slice_to_bytes(t.user_args[:])
  375. arg1 := (^T1)(raw_data(user_args))^
  376. arg2 := (^T2)(raw_data(user_args[size_of(T1):]))^
  377. arg3 := (^T3)(raw_data(user_args[size_of(T1) + size_of(T2):]))^
  378. fn(arg1, arg2, arg3)
  379. }
  380. if t = create(thread_proc, priority); t == nil {
  381. return
  382. }
  383. t.data = rawptr(fn)
  384. t.user_index = 3
  385. arg1, arg2, arg3 := arg1, arg2, arg3
  386. user_args := mem.slice_to_bytes(t.user_args[:])
  387. n := copy(user_args, mem.ptr_to_bytes(&arg1))
  388. n += copy(user_args[n:], mem.ptr_to_bytes(&arg2))
  389. _ = copy(user_args[n:], mem.ptr_to_bytes(&arg3))
  390. if self_cleanup {
  391. intrinsics.atomic_or(&t.flags, {.Self_Cleanup})
  392. }
  393. t.init_context = init_context
  394. start(t)
  395. return t
  396. }
  397. /*
  398. Run a procedure with four polymorphic parameters on a different thread.
  399. This procedure runs the given procedure on another thread. The context
  400. specified by `init_context` will be used as the context in which `fn` is going
  401. to execute. The thread will have priority specified by the `priority` parameter.
  402. If `self_cleanup` is specified, after the thread finishes the execution of the
  403. `fn` procedure, the resources associated with the thread are going to be
  404. automatically freed.
  405. **Do not** dereference the `^Thread` pointer, if this flag is specified.
  406. That includes calling `join`, which needs to dereference ^Thread`.
  407. **IMPORTANT**: If `init_context` is specified and the default temporary allocator
  408. is used, the thread procedure needs to call `runtime.default_temp_allocator_destroy()`
  409. in order to free the resources associated with the temporary allocations.
  410. */
  411. create_and_start_with_poly_data4 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, arg4: $T4, fn: proc(arg1: T1, arg2: T2, arg3: T3, arg4: T4), init_context: Maybe(runtime.Context) = nil, priority := Thread_Priority.Normal, self_cleanup := false) -> (t: ^Thread)
  412. where size_of(T1) + size_of(T2) + size_of(T3) + size_of(T4) <= size_of(rawptr) * MAX_USER_ARGUMENTS {
  413. thread_proc :: proc(t: ^Thread) {
  414. fn := cast(proc(T1, T2, T3, T4))t.data
  415. assert(t.user_index >= 4)
  416. user_args := mem.slice_to_bytes(t.user_args[:])
  417. arg1 := (^T1)(raw_data(user_args))^
  418. arg2 := (^T2)(raw_data(user_args[size_of(T1):]))^
  419. arg3 := (^T3)(raw_data(user_args[size_of(T1) + size_of(T2):]))^
  420. arg4 := (^T4)(raw_data(user_args[size_of(T1) + size_of(T2) + size_of(T3):]))^
  421. fn(arg1, arg2, arg3, arg4)
  422. }
  423. if t = create(thread_proc, priority); t == nil {
  424. return
  425. }
  426. t.data = rawptr(fn)
  427. t.user_index = 4
  428. arg1, arg2, arg3, arg4 := arg1, arg2, arg3, arg4
  429. user_args := mem.slice_to_bytes(t.user_args[:])
  430. n := copy(user_args, mem.ptr_to_bytes(&arg1))
  431. n += copy(user_args[n:], mem.ptr_to_bytes(&arg2))
  432. n += copy(user_args[n:], mem.ptr_to_bytes(&arg3))
  433. _ = copy(user_args[n:], mem.ptr_to_bytes(&arg4))
  434. if self_cleanup {
  435. intrinsics.atomic_or(&t.flags, {.Self_Cleanup})
  436. }
  437. t.init_context = init_context
  438. start(t)
  439. return t
  440. }
  441. _select_context_for_thread :: proc(init_context: Maybe(runtime.Context)) -> runtime.Context {
  442. ctx, ok := init_context.?
  443. if !ok {
  444. return runtime.default_context()
  445. }
  446. /*
  447. NOTE(tetra, 2023-05-31):
  448. Ensure that the temp allocator is thread-safe when the user provides a specific initial context to use.
  449. Without this, the thread will use the same temp allocator state as the parent thread, and thus, bork it up.
  450. */
  451. when !ODIN_DEFAULT_TO_NIL_ALLOCATOR {
  452. if ctx.temp_allocator.procedure == runtime.default_temp_allocator_proc {
  453. ctx.temp_allocator.data = &runtime.global_default_temp_allocator_data
  454. }
  455. }
  456. return ctx
  457. }
  458. _maybe_destroy_default_temp_allocator :: proc(init_context: Maybe(runtime.Context)) {
  459. if init_context != nil {
  460. // NOTE(tetra, 2023-05-31): If the user specifies a custom context for the thread,
  461. // then it's entirely up to them to handle whatever allocators they're using.
  462. return
  463. }
  464. if context.temp_allocator.procedure == runtime.default_temp_allocator_proc {
  465. runtime.default_temp_allocator_destroy(auto_cast context.temp_allocator.data)
  466. }
  467. }