mcap/modules/Thread/examples/thread_group.jai

208 lines
7.4 KiB
Plaintext

thread_group: Thread_Group;
pool: Pool;
main :: () {
// First, we initialize the Thread_Group that we will use to do the work.
init_thread_group();
// Schedule work for all the threads.
// Because this is an example, and we don't want to complicate it with
// lots of code that is beside the point, the work that we will give to our threads
// is very simple: it will be to sum the range of integers from 1 to N, where N is
// chosen randomly each time. We will do this for many N.
// In order to use CPU and make it seem like the threads are really doing something,
// they will compute the answer by iterating in a loop and summing the integers.
// But, Karl Friedrich Gauss once figured out that you don't have to do this; you can
// just compute N(N+1)/2.
// Our test here will get back the answers from the threads and verify that the
// answers are correct by comparing them to Gauss' result.
// First, let's start the thread group:
start(*thread_group);
// We want to give lots of units of work to the thread group. The thread group will
// then distribute that work to its various threads.
// These units of work need to occupy memory that stays alive until the worker thread
// finishes computing its result and returns the work back to us. In many programs,
// the work will go for a while, for example across many frames of an interactive
// application, so, let's use a Pool to allocate the work.
set_allocators(*pool); // Init the Pool to use context.allocator.
// The struct for the work is just 'Summation', defined below after the end of main().
// (It needs to be visible to our worker procedure to see it, so we don't
// declare it here).
allocator: Allocator;
allocator.proc = pool_allocator_proc;
allocator.data = *pool;
for 1..NUM_WORK_ITEMS {
work := New(Summation,, allocator);
// random_get returns a u64, so, to keep our jobs reasonable, let's only take a few bits.
work.input_number = random_get() & 0xFF_FFFF;
if work.input_number == 0 work.input_number = 1;
// Give the work to the Thread_Group.
add_work(*thread_group, work, tprint("Work item %", it));
}
// Now that we have given the Thread_Group all the work, we can go about
// our business.
// Occasionally as our program runs, we should call get_completed_work()
// to see what is done, and to do whatever processing we need.
// Because this is a simple example, we'll just make a loop that sleeps
// and keeps checking for finished work. Instead of sleeping, a real program
// might be drawing the display, performing a simulation, and so forth.
// We will exit the loop when we have received as many completed pieces
// of work as we issued.
work_remaining := NUM_WORK_ITEMS;
while work_remaining > 0 {
sleep_milliseconds(10);
results := get_completed_work(*thread_group);
for results {
// The work we receive back here, in 'results', is done,
// and will no longer be touched by either the threads or
// the Thread_Group utility functions. We can deallocate our
// data structures if we want (though in this case, to be
// efficient, we allocated them with a Pool, so there is no
// need to deallocate them individually).
// Here we check the sums, to see if they are correct.
// So that we don't have to make Thread_Group polymorphic,
// the work pointer is just a *void, and we cast it here.
summation := cast(*Summation) it;
n := summation.input_number;
desired_result := (n * (n + 1)) / 2;
assert(summation.output_sum == desired_result);
log("Successful result: Sum(1..%) == %.\n", n, desired_result);
}
work_remaining -= results.count;
// A nontrivial program would probably use lots of Temporary_Storage
// and thus reset it here. Since the return value of get_completed_work()
// uses Temporary_Storage, we will go ahead and do this.
reset_temporary_storage();
}
// When we are done, we shut down the Thread_Group.
shutdown(*thread_group);
print("Done!\n");
}
// Summation is the struct representing the work that we pass to our Thread_Group.
Summation :: struct {
input_number: u64; // The max number of the sum; the thread will sum 1..input_number.
output_sum: u64; // Our thread procedure will write the result here.
}
init_thread_group :: () {
// We want to make about as many threads as we have CPUs. More than that
// will overwhelm our computer with context-switching waste work!
num_cpus := get_number_of_processors();
assert(num_cpus >= 1);
if num_cpus > 200 num_cpus = 200; // Clamp to a value that is reasonable as of 2021, in case we get weird data.
#if (OS == .WINDOWS) || (OS == .LINUX) {
// This routine reports hyperthreads, so, divide by two, because of Intel marketing.
num_cpus /= 2;
}
// In a typical program, you want the "main thread" to be able to use a CPU, so,
// only allocate as many threads as we have other CPUs.
// Launch at least 2 threads, because for this example, we want to show multiple threads
// running at the same time.
num_threads := max(num_cpus - 1, 2);
log("This machine reports % CPUs; starting % threads.\n", num_cpus, num_threads);
init(*thread_group, num_threads, summation_proc);
// After initializing a Thread_Group, there are various data members you can set
// to control behavior. See the definition of Thread_Group in modules/Thread.
thread_group.name = "Example";
thread_group.logging = DO_LOGGING;
{
// You are free to set the Thread.user_data member on any of the threads created by the group.
// But ... you have to do that before calling start(). Here we will just set a dummy user_data,
// to show it's working.
Dummy_Data :: struct {
name := "Lord Dimwit Flathead the Excessive";
}
dummy_data := New(Dummy_Data);
for * thread_group.worker_info {
it.thread.data = dummy_data;
}
}
}
// summation_proc will be called by the Thread_Group, once for each
// Summation that we gave it, each time in a thread that is different from
// the main thread.
summation_proc :: (group: *Thread_Group, thread: *Thread, work: *void) -> Thread_Continue_Status {
// So that we don't have to make Thread_Group polymorphic, work is just a *void,
// but you can cast it to the expected type here.
summation := cast(*Summation) work;
result: u64;
for 1..summation.input_number {
result += it;
}
summation.output_sum = result;
// We return .CONTINUE to tell the Thread_Group that we should keep running.
// Returning .STOP will stop this thread, and we don't want to do that.
return .CONTINUE;
}
// Just to see how the example works, by default we set NUM_WORK_ITEMS to 10.
// For a more-serious test, you might try a much larger number of work items.
// This will be spammy so you might want to think about setting
// DO_LOGGING to false.
SERIOUS_TEST :: false;
#if SERIOUS_TEST {
NUM_WORK_ITEMS :: 10000;
} else {
NUM_WORK_ITEMS :: 10;
}
DO_LOGGING :: true;
#import "Basic";
#import "System";
#import "Thread";
#import "Pool";
#import "Random";