Skip to content

Commit fc694f3

Browse files
author
ranch
committed
added instructions for multithreading
1 parent ea936c3 commit fc694f3

2 files changed

Lines changed: 120 additions & 1 deletion

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Project 6
2+
3+
## Intro
4+
5+
Project 6 is a fairly simple program to implement. You don't have to do it this way but I recommend doing it in 2 steps.
6+
7+
1) Implement a single threaded version where you implement a function is_prime()
8+
2) Multithread it
9+
10+
Multithreading is a slightly tough topic because it is one of those features where you have to have it in mind while you build your project.
11+
12+
Think about it: A thread takes some function and all of its necessary parameters as its own constructor arguments. The thread has a pointer to the function, does some magic under the hood, and invokes that function with the arguments. This means, that any work we want done has to have some function as an entry point. In other words, it's not really a good idea to make a monolithic procedural section that does all of our work for us.
13+
14+
## Single Thread Version
15+
16+
The main function that's important is some function called is_prime(). is_prime should be a predicate (boolean) function that simply takes a number, and calculates if it's prime or not. The instructions lay it out but for clarity, I'll provide a solution:
17+
18+
```cpp
19+
bool is_prime(int num){
20+
int candidate = num / 2;
21+
for(int i = 2; i <= candidate; i++){
22+
if(num % i == 0){
23+
return false;
24+
}
25+
}
26+
return true;
27+
}
28+
```
29+
The instructions for Project6 say to stop when you get to the target number divided by 2. In other words, if we want to check if 1000 is prime, we keep dividing it by numbers until we hit 500. We simply iterate for every number up until that candidate point, looking for a number that will cleanly divide the target number.
30+
31+
Now, in our main function, we can simply do a loop calculating every number up to a million to see if each number is prime. This is a basic for loop or whatever just going up to some const TARGET = 1'000'000 or however you want to do it.
32+
33+
## Multi Thread Version
34+
35+
Now we need to multi thread it. But first let's think about the work that needs to be done from a thread's perspective.
36+
37+
1) A Thread needs some function to work on. This means, if we're calculating if a number is prime for a million numbers, we need a function that can loop over a million numbers.
38+
39+
2) If we have a function that loops over a million numbers, we need a way for each thread to know the correct number to work on. Otherwise, we're just gonna have threads working on the same numbers just at different times. What we REALLY want is for each thread to work on a unique number out of the million numbers. We're splitting up the work to be done with threads.
40+
41+
3) We also need to print out to a file or to console at least. In both of these scenarios, we need a way to make sure that the counter we're accessing, and the io device that we're accessing is protected while the thread is using it. So, we need a mutex (mutual exclusion) and lock for critical sections.
42+
43+
So, let's create a helper function that will be the main entry point for each thread. Our helper will be passed to a thread's constructor, and it'll have sections to check a global counter that all the threads will use to see which number needs to be calculated next using is_prime. We'll also have a section where we can write our output. IO devices need to be accessed one at a time, so this section will also be protected with a lock and mutex. We also need to update the global count of prime numbers, so this should be protected as well.
44+
45+
Remember, if there is some resource that is going to be shared by threads, you need to protect. This means isprime doesn't need to be protected, each thread will have its own instance of is_prime, but if a thread is reading from or writing to something like a shared counter, that counter needs to be protected with a mutex and its lock.
46+
47+
Here's a version from class that's slightly different from what the project requires. Don't just copy paste this, because the requirements for the project are slightly different.
48+
49+
```cpp
50+
void find_primes()
51+
{
52+
size_t local_current = 0;
53+
bool flag = true;
54+
while(flag)
55+
{
56+
{ //we've created a block here so that we can create a guard
57+
//and then we leave the scope, immediately freeing the mutex
58+
lock_guard<mutex> first_guard(current_mutex);
59+
local_current = current_number++;
60+
if(local_current > TARGET){
61+
flag = false;
62+
break;
63+
}
64+
cout << "Checking number: " << local_current << endl;
65+
}
66+
if(is_prime(local_current))
67+
{
68+
lock_guard<mutex> second_guard(write_mutex);
69+
out << local_current << endl;
70+
counter++;
71+
}
72+
}
73+
}
74+
```
75+
76+
Finally in main, you just need to create threads to do the job. Note: I have not included the globals you need. You'll need a global mutex or two. You'll need a global current_number that each thread will check. You'll need a global counter that will count the total number of primes found. You'll also need a global ofstream to write to (primes.dat).
77+
78+
Each function will have its own non-shared local_counter that you can use for reporting how many primes that thread found.
79+
80+
In main you can do the threads like so:
81+
82+
```cpp
83+
thread t1(find_primes);
84+
thread t2(find_primes);
85+
...
86+
t1.join();
87+
t2.join();
88+
```
89+
90+
OR you can use a loop
91+
92+
```cpp
93+
for(auto i = 0; i < num_threads; i++){
94+
threads.push_back(thread(find_primes));
95+
}
96+
for(auto& t : threads){
97+
t.join();
98+
}
99+
```
100+
101+
Slightly more elegant.

docs/Teaching/C++/CS-2370/Week 12 - Multithreading.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,24 @@ void print_block (int n, char c) {
8080
8181
Here we have a unique_lock of template type mutex. It's called lck, and it takes an existing mutex as its constructor argument. It will now manage that mutex. It's like bodyguard that has the key. Now we don't have to worry about locking and unlocking.
8282
83+
*IMPORTANT PATTERN*
84+
Since unique locks automatically unlock, you may have a section of code that you want to lock, but you don't want to keep your code locked for the full duration of a function. A common pattern is to put your lock in an unnamed scope block like so:
85+
86+
```cpp
87+
std::mutex mtx;
88+
89+
void doing_something(){
90+
//...doing stuff
91+
{
92+
std::unique_lock<std::mutex> lck (mtx);
93+
std::cout << "An Example" << endl;
94+
}
95+
//...continue doing other stuff
96+
}
97+
```
98+
99+
In the situation above, the lock is acquired, it prints to console, and then the scope is exited, which triggers the unlocking of the unique_lock.
100+
83101
### Condition Variable
84102

85103
A condition variable is an object that receives a lock object like a unique_lock, and blocks access, or stops it from executing. It's the big boss. This thing basically says, hey, I have a signal to wait, and I have a signal to notify you to go. Generally, when we spin up a bunch of threads, they start ripping as soon as they're active. So, we can make those threads wait until we're ready to start. Once we're ready, we can send a notify_all, and then the threads will all start going ham trying to get their jobs done. You use std::condition_variable, call it cv, and use cv.wait() and cv.notify_all().
@@ -92,4 +110,4 @@ Atomic types are types that geared specifically toward multithreading. They won'
92110

93111
### Videos and Extra Materials
94112

95-
https://www.youtube.com/watch?v=7ENFeb-J75k&t=82s
113+
https://www.youtube.com/watch?v=7ENFeb-J75k

0 commit comments

Comments
 (0)