208 lines
7.4 KiB
Plaintext
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";
|