C语言实现并行计算
1 全局变量和静态变量
1. 定义
- 全局变量:在所有函数之外声明的变量,同时包含所有声明的函数
- 静态变量:由static关键字声明的变量
int a = 0; // 全局变量
static int b = 0; // 静态全局变量
int main(int argc, char **argv)
{
static int c = 0; // 静态局部变量
return 0;
}
2. 相同
- 栈帧位置:已初始化的全局变量和静态变量都处于.data段,未初始化都处于.bss段(虽然此时静态变量被存储于.bss段,但其还是被自动初始化为0,很有意思)
- 生命周期:都在程序开始执行时就分配内存空间,只有在程序结束时才释放其内存空间
3. 区别
作用域:
- 全局变量:在程序执行期间,在任何地方都能够访问,可以用external直接在其他文件访问全局变量
- 静态全局变量:在程序执行期间,在该文件的任何地方都能够访问
- 静态局部变量:在程序执行期间,在该函数内部任何地方都能够内访问
2 external与.h头文件
1. 作用
用于在一个源文件中引用在其他源文件中定义的变量或函数
2. 区别
- external:用于在源文件中引用在其他源文件中定义的全局变量,但并不会插入实际的函数代码或变量,它只是告诉编译器这个函数或变量在其他地方定义
- .h头文件:在一个源文件中包含一个头文件时,头文件中的所有内容都会被插入到这个源文件中,是插入,可能会浪费内存空间,但是会拥有清晰简明的代码结构
3. 使用指南
- external:需要在一个源文件中访问另一个源文件中定义的全局变量,或对内存有严格控制时,可以使用extern关键字
- .h头文件:有一些函数或变量需要在多个源文件中共享时,或对于整个项目需要一个清晰代码结构时,可以使用.h头文件
3 inline内联函数
1. 作用
用于建议编译器进行内联优化,即将函数调用直接替换为函数体的代码,可以减少函数调用的开销,提高程序的运行速度
2. 特点
- 定义为内联函数的函数在编译器编译时会直接将函数体嵌入到每个调用点,从而避免了常规函数调用所需的压栈、跳转和返回等操作
- 内联函数通常应该尽可能小,因为一个大的内联函数会导致生成的代码体积增大,可能会影响程序的性能
- 内联函数的定义必须出现在每个使用它们的文件中,因此,它们通常定义在头文件中
- inline只是向编译器发出的一个建议,编译器可以选择忽略这个建议,如果函数体很大,或者函数中包含复杂的控制结构(如循环或递归),编译器可能会选择不进行内联优化,而且,如果编译的时候没有使用-O选项,编译器也会选择不进行内联优化
4 多线程编程
1. pthread
- **pthread_t:**线程ID,用于表示线程的唯一标识符,在创建线程时,pthread_create函数会返回一个 pthread_t类型的值
- **pthread_attr_t:**一个结构体,用于描述线程的属性,其值都可以用函数设定,但一般都设定p_state即可,其他的在非必要情况下不需要自主设定
- unsigned p_state:这个字段用于存储线程的状态信息,例如线程是否是分离状态
- void *stack:这个字段是一个指针,指向线程的堆栈的开始地址
- size_t s_size:这个字段表示线程堆栈的大小
- struct sched_param param:这个字段是一个结构体,用于存储线程的调度参数,例如线程的优先级
typedef struct pthread_attr_t pthread_attr_t;
struct pthread_attr_t
{
unsigned p_state;
void *stack;
size_t s_size;
struct sched_param param;
};
- **pthread_attr_init(pthread_attr_t *attr):**初始化一个线程属性对象,默认是分离状态
- **pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate):**设置线程的分离状态。detachstate可以是PTHREAD_CREATE_DETACHED(分离线程)或PTHREAD_CREATE_JOINABLE(可连接线程):
- 分离状态:这意味着当线程结束时,系统会自动回收线程的资源,不需要其他线程对其进行join操作
- 可连接状态:这意味着线程结束时,其资源不会被立即回收,需要其他线程对其进行join操作,才能回收其资源,但这时可以通过pthread_join函数获取线程返回结果
- **pthread_attr_destroy(pthread_attr_t *attr):**销毁一个线程属性对象,释放其占用的资源
const int NumThreads = 10;
pthread_t thread_ids[NumThreads];
pthread_attr_t attr[NumThreads];
// 初始化10个线程所有属性,并设置线程状态为可连接状态
for (int i = 0; i < NumThreads; i++)
{
pthread_attr_init(&attr[i]);
pthread_attr_setdetachstate(&attr[i], PTHREAD_CREATE_JOINABLE);
}
- **pthread_create(pthread_t *thread_id, const pthread_attr_t *attr, void *func(void *), void *argv):**用于创建新线程的函数
- thread_id:线程id
- attr:线程属性
- func:一个传入参数为void *,返回值为void *类型的函数,这是该线程需要执行的函数
- argv:传入线程的参数
const int NumThreads = 10;
pthread_t thread_ids[NumThreads];
pthread_attr_t attr[NumThreads];
// 初始化10个线程所有属性,并设置线程状态为可连接状态
for (int i = 0; i < NumThreads; i++)
{
pthread_attr_init(&attr[i]);
pthread_attr_setdetachstate(&attr[i], PTHREAD_CREATE_JOINABLE);
}
// 创建10个线程,分配资源
for (int i = 0; i < NumThreads; i++)
{
int err = pthread_create(&thread_ids[i], &attr[i], func, (void *)argv);
if (err != 0)
{
fprintf(stderr, "ERROR: pthread_create() return code: %d\n", err);
}
}
**pthread_exit(void *retval):**这个函数用于终止调用它的线程,并返回一个退出状态,retval是一个指向void *的指针,用于存储线程的返回值
**pthread_join(pthread_t thread_id, void * *retval):**等待线程结束,并获取它的退出状态,在这个过程中,被join的线程的资源会被回收
void *func(void *argv)
{
int *retval = (int *)malloc(sizeof(int));
...
pthread_exit((void *)retval);
}
- **pthread_mutex_t:**表示互斥锁(mutex),防止多个线程同时访问同一段代码或数据
- **pthread_mutex_init(pthread_mutex_t *lock, const pthread_mutexattr_t *a):**初始化一个互斥锁,通常设为NULL表示使用默认属性
- **pthread_mutex_destroy(pthread_mutex_t *lock):**销毁一个互斥锁。在销毁之前,互斥锁必须先解锁,而且没有任何线程在等待这个互斥锁
- **pthread_mutex_lock(pthread_mutex_t *lock):**锁定一个互斥锁。如果互斥锁已经被锁定,调用线程会阻塞,直到互斥锁变为可用
- **pthread_mutex_unlock(pthread_mutex_t *lock):**解锁一个互斥锁
double sum = 0.0;
pthread_mutex_t lock;
static void *calculate(void *argv)
{
double x;
double local_sum = 0.0;
for (int i = 0; i < *((int *)argv); i++)
{
x = (i + .5) * step;
local_sum = local_sum + 4.0 / (1. + x * x);
}
pthread_mutex_lock(&lock);
sum += local_sum;
pthread_mutex_unlock(&lock);
pthread_exit(0);
}
int main(int argc, char **argv)
{
for (int i = 0; i < NumThreads; i++)
{
num[i] = i + 1;
pthread_attr_init(&attr[i]);
pthread_attr_setdetachstate(&attr[i], PTHREAD_CREATE_JOINABLE);
}
pthread_mutex_init(&lock, NULL);
for (int i = 0; i < NumThreads; i++)
{
int err = pthread_create(&thread_ids[i], &attr[i], calculate, (void *)&num[i]);
if (err != 0)
{
printf("ERROR: pthread_create() return code: %d\n", err);
}
}
pthread_mutex_destroy(&lock);
}
2. OpenMP
- 并行化循环
#pragma omp parallel for
for (int i = 0; i < N; i++)
{
// 这里的代码将在多个线程上并行执行
}
- 创建并行区域:
#pragma omp parallel
{
// 这里的代码将在多个线程上并行执行
}
- 线程私有数据: 使用private子句来指定某些变量为线程私有,这样每个线程都会有这些变量的独立副本
#pragma omp parallel private(x)
{
// 每个线程都有自己的x变量
}
- 线程共享数据: 使用shared子句来指定某些变量为线程共享,这样所有线程都会共享这些变量
#pragma omp parallel shared(x)
{
// 所有线程都共享同一个x变量
}
- 并行化归约操作: 使用reduction子句来并行化归约操作,例如求和、求积
int sum = 0;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < N; i++) {
sum += i;
}
5 错误处理
- errno:在程序运行时设置的全局变量,表示在函数运行时发生的错误,0表示没有错误
- perror():显示字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式
如下一个处理文本打开错误的示例
int main() {
FILE *fp;
fp = fopen("non_existent_file.txt", "r");
if(fp == NULL)
{
fprintf(stderr, "错误号: %d\n", errno);
perror("Error opening file");
return(-1);
}
fclose(fp);
return(0);
}