0% found this document useful (0 votes)
3 views16 pages

Shared Memory Parallel Programming

Uploaded by

Mubashir Akhtar
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views16 pages

Shared Memory Parallel Programming

Uploaded by

Mubashir Akhtar
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

Shared Memory Parallel Programming.

Guide to OpenMP
and PThreads

1. Learning Objectives

By the end of this study material, you should be able to:

· Introduction: Articulate the fundamental concepts of shared memory parallelism,


distinguish between the compiler directive-based model of OpenMP and the library-based
threading model of POSIX Threads (PThreads), and explain the Fork-Join execution
paradigm.
· Familiarity with Functions: Identify and correctly utilize the core functions and directives
of both OpenMP (e.g., #pragma omp parallel, omp_get_thread_num(), reduction) and PThreads
(e.g., pthread_create(), pthread_join(), pthread_mutex_lock()).
· Coding, Compiling, Running, and Benchmarking: Write correct C programs that
leverage multi-core processors, compile them with the appropriate GCC flags (-fopenmp, -
pthread), execute them in a Linux environment, and measure performance speedup using
wall-clock timing functions.
· Advantages and Limitations: Critically evaluate the trade-offs between OpenMP and
PThreads regarding ease of implementation, control granularity, and debugging complexity.
· Relevant Visual Elements: Interpret and create simple visual representations of thread
interaction, such as the Fork-Join model diagram and Mutex locking diagrams, to reason
about concurrency.
· Other Relevant Information: Recognize common pitfalls in shared memory programming,
including data races, false sharing, and deadlock, and apply basic strategies to avoid them.

1
2. Theoretical Foundations: Shared Memory and Threading
Models

2.1 The Shared Memory Paradigm

In shared memory parallel programming, multiple execution threads reside within a single
operating system process. They share a common address space. This means a global variable
modified by Thread A is immediately visible to Thread B.

· Advantage: Communication is fast and implicit (through memory reads/writes).


· Disadvantage: Uncontrolled access leads to Race Conditions, where the final result
depends on the non-deterministic timing of thread execution.

2.2 OpenMP vs. PThreads: A Tale of Two Approaches

Feature OpenMP POSIX Threads (PThreads)

Abstraction Low-level. You manage thread


High-level. You annotate the code.
Level lifecycle.

Compiler Directives (#pragma) and


Implementation Pure C API Library (pthread.h).
runtime library functions.

Code compiles as serial code if - Requires explicit thread logic; serial


Portability fopenmp is omitted. Excellent for fallback requires #ifdef or separate
incremental parallelization. code paths.

Fine-grained. Ideal for complex


Control Coarse-grained. Ideal for loops and
client/server models and custom
Granularity sections.
synchronization.

Easier to trace specific function


Ease of Often harder to debug low-level
calls, but easier to introduce
Debugging interaction due to compiler magic.
memory leaks.

2
2.3 The Fork-Join Model

Both libraries operate on the Fork-Join principle. A single master thread (main) executes until a
parallel region is encountered. The runtime Forks a team of worker threads. They execute
concurrently, then synchronize and terminate (or sleep) at an implicit barrier—this is the Join.

3. Part I: OpenMP Programming

3.1 Core Model and Compilation

OpenMP relies on compiler directives. To compile an OpenMP program named prog.c:

gcc -fopenmp -o prog prog.c

3.2 Essential Functions and Directives

Directive / Function Purpose

Creates a team of threads. Code block executes on every


#pragma omp parallel
thread.

omp_get_thread_num() Returns the ID of the calling thread (0 to N-1).

3
omp_get_num_threads() Returns the total number of threads in the current team.

Divides loop iterations among threads. Must be inside


#pragma omp for
a parallel region.

#pragma omp parallel for Shortcut for creating parallel region AND dividing the loop.

Creates private copies of sum for each thread, combines


reduction(+:sum)
them safely at the end.

#pragma omp critical Allows only one thread to execute the block at a time.

3.3 Code Example 1: "Hello World" and Thread Discovery

3.4 Code Example 2: Vector Addition with Reduction (Benchmarking)

This example demonstrates a data-parallel loop and proper timing.

#include <stdio.h>

4
#include <stdlib.h>

#include <omp.h>

#define N 100000000 // 100 Million elements

int main() {

double *a = (double*)malloc(N * sizeof(double));

double *b = (double*)malloc(N * sizeof(double));

double *c = (double*)malloc(N * sizeof(double));

// Initialize arrays

for(int i=0; i<N; i++) {

a[i] = i * 1.0;

b[i] = i * 2.0;

double start_time = omp_get_wtime();

// Parallelize the loop. Each thread works on a chunk of i.

#pragma omp parallel for

for(int i=0; i<N; i++) {

c[i] = a[i] + b[i];

double end_time = omp_get_wtime();

5
// Check a result to ensure compiler didn't optimize away the loop

printf("c[%d] = %f\n", N-1, c[N-1]);

printf("OpenMP Vector Add Time: %f seconds\n", end_time - start_time);

free(a); free(b); free(c);

return 0;

3.5 Code Example 3: The Danger of Data Races and the Fix

Incorrect (Race Condition):

int sum = 0;

#pragma omp parallel for

for(int i=0; i<1000; i++) {

sum += i; // Multiple threads read-modify-write "sum" -> WRONG

Correct Solution A: Critical Section (Slow)

int sum = 0;

#pragma omp parallel for

for(int i=0; i<1000; i++) {

#pragma omp critical

sum += i;

Correct Solution B: Reduction (Fast)

6
int sum = 0;

#pragma omp parallel for reduction(+:sum)

for(int i=0; i<1000; i++) {

sum += i;

4. Part II: PThreads Programming

4.1 Core Model and Compilation

PThreads requires explicit management of thread handles and attributes. Compile with:

gcc -pthread -o prog prog.c

4.2 Essential Functions and Types

Function Purpose

pthread_t Data type for a thread identifier.

pthread_create(&tid, NULL, func, arg) Starts a new thread executing func(arg).

pthread_join(tid, NULL) Waits for a specific thread to terminate (Join).

pthread_mutex_t Data type for a mutual exclusion lock.

pthread_mutex_lock(&mutex) Acquires the lock (blocks if already held).

pthread_mutex_unlock(&mutex) Releases the lock.

Synchronization point where all threads must


pthread_barrier_t
wait for each other.

7
4.3 Mutex Locking Mechanism

A mutex ensures Mutually Exclusive access to a critical section.

4.4 Code Example 1: Creating and Joining Threads

This example shows the boilerplate required to pass arguments to threads safely.

#include <stdio.h>

#include <stdlib.h>

#include <pthread.h>

#define NUM_THREADS 4

// Thread function signature MUST be: void* func(void* arg)

void* worker(void* arg) {

int tid = *((int*)arg);

printf("PThread %d is executing.\n", tid);

return NULL;

8
int main() {

pthread_t threads[NUM_THREADS];

int thread_ids[NUM_THREADS];

for(int i=0; i<NUM_THREADS; i++) {

thread_ids[i] = i;

// Create thread: handle, attributes, function, argument

if(pthread_create(&threads[i], NULL, worker, &thread_ids[i]) != 0) {

perror("Failed to create thread");

exit(1);

// Wait for all threads to finish

for(int i=0; i<NUM_THREADS; i++) {

pthread_join(threads[i], NULL);

printf("All threads have joined. Exiting.\n");

return 0;

4.5 Code Example 2: Vector Addition with PThreads (Structure Packing)

Because pthread_create passes only one argument, we must pack multiple parameters into a struct.

9
#include <stdio.h>

#include <stdlib.h>

#include <pthread.h>

#include <time.h>

#define N 100000000

#define NUM_THREADS 4

// Struct to hold the "slice" of work for each thread

typedef struct {

double *a;

double *b;

double *c;

int start;

int end;

} thread_data_t;

void* vector_add(void* arg) {

thread_data_t* data = (thread_data_t*)arg;

for(int i = data->start; i < data->end; i++) {

data->c[i] = data->a[i] + data->b[i];

return NULL;

int main() {

10
double *a = (double*)malloc(N * sizeof(double));

double *b = (double*)malloc(N * sizeof(double));

double *c = (double*)malloc(N * sizeof(double));

// Initialize

for(int i=0; i<N; i++) { a[i] = i; b[i] = i*2; }

pthread_t threads[NUM_THREADS];

thread_data_t tdata[NUM_THREADS];

struct timespec start, end;

clock_gettime(CLOCK_MONOTONIC, &start);

int chunk = N / NUM_THREADS;

for(int i=0; i<NUM_THREADS; i++) {

tdata[i].a = a;

tdata[i].b = b;

tdata[i].c = c;

tdata[i].start = i * chunk;

tdata[i].end = (i == NUM_THREADS - 1) ? N : (i+1) * chunk;

pthread_create(&threads[i], NULL, vector_add, &tdata[i]);

for(int i=0; i<NUM_THREADS; i++) {

pthread_join(threads[i], NULL);

11
clock_gettime(CLOCK_MONOTONIC, &end);

double time_taken = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;

printf("c[%d] = %f\n", N-1, c[N-1]);

printf("PThreads Vector Add Time: %f seconds\n", time_taken);

free(a); free(b); free(c);

return 0;

4.6 Code Example 3: Mutual Exclusion (Mutex) for a Shared Counter

#include <stdio.h>

#include <pthread.h>

#define ITERATIONS 1000000

int counter = 0;

pthread_mutex_t lock; // Global mutex

void* increment(void* arg) {

for(int i=0; i<ITERATIONS; i++) {

pthread_mutex_lock(&lock); // Enter Critical Section

counter++; // Safe update

pthread_mutex_unlock(&lock); // Exit Critical Section

return NULL;

12
}

int main() {

pthread_t t1, t2;

pthread_mutex_init(&lock, NULL); // Initialize mutex

pthread_create(&t1, NULL, increment, NULL);

pthread_create(&t2, NULL, increment, NULL);

pthread_join(t1, NULL);

pthread_join(t2, NULL);

printf("Final Counter Value: %d (Expected: %d)\n", counter, 2 * ITERATIONS);

pthread_mutex_destroy(&lock); // Clean up

return 0;

13
5. Benchmarking and Performance Considerations

5.1 Timing Your Code

· OpenMP: Always use omp_get_wtime() for portable, high-resolution wall-clock time.


· PThreads: Use clock_gettime(CLOCK_MONOTONIC, &ts) for reliable wall-clock time not
affected by system clock adjustments.

5.2 Speedup and Efficiency

· Speedup (S): 𝑆=𝑇 𝑠 𝑒𝑟 𝑖𝑎 𝑙 /𝑇 𝑝 𝑎𝑟 𝑎 𝑙𝑙𝑒 𝑙


· Efficiency (E): 𝐸=𝑆/ 𝑃 (where P is number of cores/threads)

Important Observation: You will notice that doubling the threads does not double the speed
(Amdahl's Law). The sequential portions of code (malloc, I/O) become the bottleneck.

5.3 False Sharing

A silent performance killer in both OpenMP and PThreads. Occurs when two threads modify
different variables that reside on the same cache line (typically 64 bytes). The cache coherence
protocol forces the cache line to bounce between cores, destroying performance.

False Sharing. Even though they touch different memory addresses, the hardware sees
contention for the same cache line.
Mitigation: Use padding (e.g., char pad[60]) or OpenMP's schedule(static, chunk_size) to spread work
across distinct cache lines.

6. Advantages and Limitations: A Comparative Analysis

OpenMP

Advantages Limitations

Less Control: Difficult to implement complex


Incremental Parallelism: Start with serial code,
pipeline patterns or producer-consumer
add one #pragma, and it's parallel.
queues.

14
Compiler Dependent: If the compiler has a
Portability: Same code compiles and runs
bug in its OpenMP implementation, you are
(serially) without OpenMP compiler.
stuck.

Overhead Spikes: Implicit barriers at the end


Productivity: Much less code than PThreads for
of for loops can cause idle time if work is
loop-level parallelism.
unbalanced.

PThreads

Advantages Limitations

Fine-Grained Control: You can precisely Verbose & Error-Prone: Easy to forget
manage thread affinity, scheduling policies, and a pthread_join (resource leak) or
signal masks. a pthread_mutex_unlock (deadlock).

Rich Synchronization Primitives: Condition Steep Learning Curve: Managing thread


variables allow threads to sleep until signaled, arguments via struct packing and handling
essential for server applications. return values adds complexity.

Deterministic Patterns: Ideal for persistent Code Clutter: Parallelism logic dominates the
worker thread pools. codebase, obscuring the core algorithm.

7. Additional Expert Guidance for Beginners

7.1 Common Pitfalls and Debugging Tips

The "Parallel For" Private Variable Trap:

int i; // DANGER: declared outside.

#pragma omp parallel for

for(i=0; i<N; i++) { ... }

15
Problem: i is shared by default! Thread 1 might set i=0, Thread 2 sets i=1, causing chaos.
Fix: Declare loop index inside the for statement: for(int i=0; ...) or explicitly list as private(i).

PThreads and Returning Local Pointers:


Never return a pointer to a local variable from a thread function. If you must return data,
allocate it dynamically (malloc) and let the joining thread free it.

Using printf for Debugging:


While printf is useful, it forces serialization (locks stdout), changing the timing of your
program and often hiding the race condition you are trying to find. Use it sparingly, and
consider logging to a memory buffer instead.

7.2 Recommended Environment

· OS: Linux (Ubuntu/Debian or WSL2 on Windows). The performance and tooling for
PThreads/OpenMP on native Linux is superior.
· Compiler: GCC (version 9+).
· Tool: htop or top (press 1) to visually see your threads saturating the CPU cores.

16

You might also like