-
Notifications
You must be signed in to change notification settings - Fork 12
Command line arguments
End of the line, folks!
Linux/Mac/WSL/Cygwin users: Follow the steps from the previous tutorials. Name the folder "Tutorial10" and the file "thread.cpp". Replace "program" in the "TARGET" line of the Makefile with "thread".
All platforms: Follow the steps in the Building Programs page on how to compile and run the program (this is a single-file program).
We've seen how TSGL draws shapes, handles colors, text, I/O events, and command-line arguments.
Now...how exactly does threading come into play in this library?
TSGL does stand for "Thread-Safe Graphics Library."
How can we utilize threading in our drawings?
Simple. Let's first write some skeleton code:
#include <tsgl.h>
using namespace tsgl;
int main() {
Canvas c(0, 0, 500, 500, "Threading", GRAY, nullptr, FRAME);
c.start();
c.wait();
}
Compile and run. A gray screen should appear.
When your program includes the header file tsgl.h, it also includes the header file (omp.h) needed in order to use OpenMP library functions such as omp_get_num_threads() and omp_get_thread_num() (which gets the number of threads spawned and the id number of a thread, respectively). It also allows us to utilize the #pragma omp directive, which enables us to write blocks of code that should be executed in parallel.
We're going to use the Canvas in another function that we create.
Now, we've seen drawing loops in the tutorial, Animation Loops, but we aren't going to be adding one to this code. Instead, we are going to do something a little differently:
#include <tsgl.h>
using namespace tsgl;
void drawTutorial(Canvas & c) {
double startTime = omp_get_wtime();
printf("\nTime to color pixels: %f\n", omp_get_wtime() - startTime);
c.wait();
}
int main() {
Canvas c(0, 0, 500, 500, "Threading", GRAY, nullptr, FRAME);
c.start();
drawTutorial(c);
c.close();
}
Let us explain what we did differently. We added a function stub ( drawTutorial() ) that takes in a reference to a Canvas object. We also added timing statements: double startTime = omp_get_wtime();, printf("\nTime to color pixels: %f\n", omp_get_wtime() - startTime);. These statements will keep track of how long our drawing took when we call our drawTutorial() function.
Next, let's define the function so that it actually draws something onto the passed Canvas. Note that we create a Background pointer bgp from Canvas c and call the function drawPixel.
#include <tsgl.h>
using namespace tsgl;
void drawTutorial(Canvas & c) {
double startTime = omp_get_wtime();
Background *bgp = c.getBackground();
ColorFloat color = RED;
for(int i = -c.getWindowWidth() / 2; i < c.getWindowWidth() / 2; i++) {
c.sleep();
for(int j = -c.getWindowHeight() / 2; j < c.getWindowHeight() / 2; j++) {
bgp->drawPixel(i, j, color);
}
}
printf("\nTime to color pixels: %f\n", omp_get_wtime() - startTime);
}
int main() {
Canvas c(0, 0, 500, 500, "Threading", GRAY, nullptr, FRAME / 2);
c.start();
drawTutorial(c);
c.wait();
}
Re-compile and run. The screen should become red from left to right until it hits the WindowWidth. It took 8.384696 seconds for the square to fill the screen on our machine. (Note that we are using the can.sleep() call to slow the computation down, so that you can see the main thread coloring the pixels.)
The main function passes our created Canvas, c, to the drawTutorial() function. That function colors each pixel red.
So where do threads come in?
As it stands, we just have the main thread creating and initializing a Canvas object, and then coloring it red in the drawTutorial() function. To spread the work of coloring the Canvas across multiple threads, we can add a parallel block and parallel for loop to our function:
#include <tsgl.h>
using namespace tsgl;
void drawTutorial(Canvas & c) {
double startTime = omp_get_wtime();
Background *bgp = c.getBackground();
ColorFloat color = RED;
#pragma omp parallel
{
#pragma omp for
for(int i = -c.getWindowWidth() / 2; i < c.getWindowWidth() / 2; i++) {
c.sleep();
for(int j = -c.getWindowHeight() / 2; j < c.getWindowHeight() / 2; j++) {
bgp->drawPixel(i, j, color);
}
}
}
printf("\nTime to color pixels: %f\n", omp_get_wtime() - startTime);
}
int main() {
omp_set_num_threads(4); // Set the number of threads to use
Canvas c(0, 0, 500, 500, "Threading", GRAY, nullptr, FRAME);
c.start();
drawTutorial(c);
c.wait();
}
Re-compile and run. The screen should still be red from left to right. It took 2.134641 seconds to color all of the pixels with four threads (about 1/4 of the original time) on our machine.
Let's take a look at the parallel block:
#pragma omp parallel
{
#pragma omp for
for(int i = -c.getWindowWidth() / 2; i < c.getWindowWidth() / 2; i++) {
c.sleep();
for(int j = -c.getWindowHeight() / 2; j < c.getWindowHeight() / 2; j++) {
bgp->drawPixel(i, j, color);
}
}
}
color is set to RED, and a parallel block is made with 4 threads (see the omp_set_num_threads() function in the main method before we create a Canvas).
Because of the omp_set_num_threads(4); statement in the main() function, the #pragma omp parallel directive causes the main thread to fork 3 new threads. Each of the four threads will then perform the statements in the block that follows the pragma. The #pragma omp for directive then divides up the iterations of the for loop that follows it among those four threads. Since the inner loop colors the pixels RED, all of the threads end up coloring their pixels the same color.
It also speeds up the coloring of the pixels in proportion to the number of threads being used; in this case, four threads were used, so the time to color all of the pixels red was about 1/4 of the original time.
This is nice and all....but we can't really visualize the four threads coloring different pixels. We know they are coloring them RED, but which ones?
Let's add some more code to better visualize this:
#include <tsgl.h>
using namespace tsgl;
void drawTutorial(Canvas & c) {
double startTime = omp_get_wtime();
Background *bgp = c.getBackground();
// Parallel block
#pragma omp parallel
{
// Current thread id & number of threads
float tid = omp_get_thread_num();
// Give it a color
ColorFloat color = Colors::highContrastColor(tid);
// Color the pixels
#pragma omp for
for(int i = -c.getWindowWidth() / 2; i < c.getWindowWidth() / 2; i++) {
c.sleep();
for(int j = -c.getWindowHeight() / 2; j < c.getWindowHeight() / 2; j++) {
bgp->drawPixel(i, j, color);
}
}
}
printf("\nTime to color pixels: %f\n", omp_get_wtime() - startTime);
}
int main() {
omp_set_num_threads(4); // Set the number of threads to use
Canvas c(0, 0, 500, 500, "Threading", GRAY, nullptr, FRAME);
c.start();
drawTutorial(c);
c.wait();
}
Recompile and run. There should now be four differently colored sections! (It took 2.139812 seconds to color the the screen four different colors on our machine!)
Since the changes were made only to drawTutorial(), let's take a closer look at that function:
void drawTutorial(Canvas & c) {
double startTime = omp_get_wtime();
Background *bgp = c.getBackground();
// Parallel block
#pragma omp parallel
{
// Current thread id & number of threads
float tid = omp_get_thread_num();
// Give it a color
ColorFloat color = Colors::highContrastColor(tid);
// Color the pixels
#pragma omp for
for(int i = -c.getWindowWidth() / 2; i < c.getWindowWidth() / 2; i++) {
c.sleep();
for(int j = -c.getWindowHeight() / 2; j < c.getWindowHeight() / 2; j++) {
bgp->drawPixel(i, j, color);
}
}
}
printf("\nTime to color pixels: %f\n", omp_get_wtime() - startTime);
}
This function allows us to see the threads coloring different sections of the Canvas and thereby make it MUCH easier to visualize.
Next, let's add support for command-line arguments so that we can control the number of threads being used:
#include <tsgl.h>
using namespace tsgl;
void drawTutorial(Canvas & c) {
double startTime = omp_get_wtime();
Background *bgp = c.getBackground();
// Parallel block
#pragma omp parallel
{
// Current thread id & number of threads
float tid = omp_get_thread_num();
// Give it a color
ColorFloat color = Colors::highContrastColor(tid);
// Color the pixels
#pragma omp for
for(int i = -c.getWindowWidth() / 2; i < c.getWindowWidth() / 2; i++) {
c.sleep();
for(int j = -c.getWindowHeight() / 2; j < c.getWindowHeight() / 2; j++) {
bgp->drawPixel(i, j, color);
}
}
}
printf("\nTime to color pixels: %f\n", omp_get_wtime() - startTime);
}
int main(int argc, char* argv[]) {
int width = (argc > 1) ? atoi(argv[1]) : 500;
int height = (argc > 2) ? atoi(argv[2]) : 500;
if(width <= 0 || height <= 0) {
width = height = 500;
}
// Threads...
int threads = (argc > 3) ? atoi(argv[3]) : omp_get_num_procs();
if(threads <= 0) {
// omp_get_num_procs() gets the number of available processors on your system
threads = omp_get_num_procs();
}
omp_set_num_threads(threads); // Set the number of threads to use
Canvas c(0, 0, width, height, "Threading", GRAY, nullptr, FRAME);
c.start();
drawTutorial(c);
c.wait();
}
We just added support for three command-line arguments: the width and height of the Canvas, and the number of threads to use in rendering. We added checks to make sure that they were valid arguments, and then we used the first two as the width and height of the Canvas.
We are then passing the third argument, the number of threads to use, into omp_set_num_threads() so that we can use that number in rendering.
Before continuing, note that we did not use an animation loop because our drawTutorial() function is not animating anything — it is just creating an inanimate drawing on the Canvas. We only need to use animation loops when we are animating something.
Compile and run the program, passing these command-line arguments: 600, 600, 10. There should now be 10 differently colored sections of the Canvas! (It took 1.059763 seconds to color the 10 different sections on our machine!)
Since we used 10 threads, the time should be about 1/10 of the original time. Using n threads, it should take about 1/n seconds of the original time (with one thread) to draw, provided (a) we have enough work for the threads to do, (b) the work of each thread is independent of the others, and (c) there are enough cores available for them to run independently.
In sum, OpenMP makes it easy to add multithreading to speed up a sequential program, and TSGL can be used to visualize what each thread is doing. You can use command-line arguments to control the number of threads; you can also a function that draws a particular drawing (with or without multithreading).
That concludes this tutorial!
We hope that these tutorial pages help you get started with TSGL, and that you make some amazing creations with this library!
Visit the tutorial Using 3D Aspects to learn about TSGL's 3D capabilities!!!
2D
- Using Background
- Using Canvas
- Using Shapes
- Using Text and Images
- Using Colors
- Animation Loops
- Using Functions
- Using Keyboard And Mouse
- Command-line arguments
- Bringing It All Together
3D