C\C++ 多线程编程
文章目录
一级目录
二级目录
三级目录# 常用技巧
c++计算时间方法
#include <iostream>
#include <chrono>
using namespace std;
using namespace chrono;
int main(int argc, char *argv[])
{
auto start = steady_clock::now();
// 中间代码
auto end = steady_clock::now();
auto t = duration_cast<milliseconds>(end-start);
printf("总计耗时:%ld ms\n",t.count());
return 0;
}
多线程编程技术
用到的头文件:
#include <iostream>
#include <thread>
#include <windows.h>
#include <mutex>
线程
join()
void function_1() {
cout << "线程1被创建" << endl;
Sleep(3000);
cout << "线程结束" << endl;
}
int main() {
thread t1(function_1); // 创建一个新线程
t1.join(); // 阻塞主线程,在该线程完成时,主线程才继续执行
cout << "程序结束" << endl;
}
运行结果:
更深入理解join函数:
在C++中,
std::thread
类提供了创建和管理线程的功能。join()
是其中一个函数,用于等待子线程执行完毕后再继续执行主线程。具体来说,当我们创建一个新的线程时,主线程会继续执行,而新线程会在后台并发执行。如果在主线程中调用子线程的
join()
函数,则主线程会阻塞,等待子线程执行完毕后再继续执行。
join()
函数的作用是等待子线程执行完毕后,将子线程加入到主线程中,释放子线程占用的资源。如果不调用join()
函数,则子线程可能会在主线程结束之前继续执行,导致程序异常或崩溃。需要注意的是,只有在子线程执行完毕后才能调用
join()
函数,否则主线程会一直阻塞等待,直到子线程执行完毕。此外,一个线程只能被加入到一个线程中,因此如果多次调用join()
函数会导致程序崩溃。总之,
join()
函数是等待子线程执行完毕后,将子线程加入到主线程中并释放其占用的资源的函数,可以保证线程的安全和正确执行。
示例代码:
void function_4() {
string s = "t1 : ";
for (int i = 0; i < 100; i++) {
s += to_string(i);
s += '\n';
cout << s;
s = "t1 : ";
}
}
int main() {
thread t1(function_4);
string s = "main : ";
//t1.join(); // join在主程序的for循环之前调用,主程序会被阻塞,直到子线程t1完成
for (int i = 0; i < 100; i++) {
s += to_string(i);
s += '\n';
cout << s;
s = "main : ";
}
//t1.join(); // join在主程序的for循环之后调用,主程序会等待线程t1执行结束。t1结束后重新加入主线程
}
t1在for循环前:t1在for循环之后:
detach()主线程与子线程分离
void function_1() {
cout << "线程1被创建" << endl;
for (int i = 0; i < 3; i++) {
Sleep(1000);
cout << "thread function_1 excute\n";
}
cout << "线程结束" << endl;
}
int main() {
thread t1(function_1); // 创建一个新线程
//t1.join(); // 阻塞主线程,在该线程完成时,主线程才继续执行
t1.detach(); // 主线程与子线程分离,主线程完成子线程可能还在运行,与join不能同时使用
for (int i = 0; i < 4; i++) {
Sleep(1000);
cout << "thread main excute\n";
}
cout << "程序结束" << endl;
}
从运行结果可以看到,主线程与子线程同时进行:
注意:主线程与子线程分离后,主线程不会再等待子线程,会出现主程序结束而子线程尚未完成的情况
函数带参线程
void function_2(string s) {
cout << s << endl;
}
void function_3(string& s) {
s="qqq";
}
int main() {
thread t1(function_2, "程序正在运行");
string s="hhh";
thread t2(function_3, ref(s)) // 使用ref声明引用参数
t1.join();
t2.join();
}
线程id与线程数
可以通过get_id()函数获取线程的id
子线程id:t1.get_id()
主线程id:this_thread::get_id()
查看自己的电脑最多可调用多少个线程:thread::hardware_concurrency()
互斥锁
互斥锁引用原因
先看示例代码
int sum = 0;
void work1() {
for (int i = 0; i < 500; i++) {
sum += i;
}
}
void work2() {
for (int i = 500; i < 1000; i++) {
sum += i;
}
}
int function_sum() {
int fsum = 0;
for (int i = 0; i < 1000; i++) {
fsum += i;
}
return fsum;
}
int main() {
thread t1(work1);
thread t2(work2);
t1.join();
t2.join();
cout << "sum1 = " << sum << endl;
cout << "sum2 = " << function_sum() << endl;
}
结果如图:
可以看到,sum1与sum2并不相等,那么问题的原因在哪呢?
因为可能会出现两个线程完全同时的情况,即以下情况
- work1读取sum=0
work2读取sum=0 - work1中sum+=1,可得sum=1
work2中sum+=500,可得sum=500 - 此时sum=1与sum=500是两个不同的值,所以一个值必定会被另一个所覆盖,从而导致数并未被加上
从此出现错误
为了解决这种问题,引出了互斥锁的技术
互斥锁可以理解为一个锁,这种锁将共享变量锁住,只有当前线程可以访问该变量,其他线程无法访问,只有当该锁被解开时,其他线程才可以访问共享变量。这种机制保证了在同一时间只有一个线程访问共享变量,避免了发生冲突的条件。
互斥锁的使用示例
int sum = 0;
mutex mylock;
void work1() {
for (int i = 0; i < 5000; i++) {
mylock.lock();
sum += i;
mylock.unlock();
}
}
void work2() {
for (int i = 5000; i < 10000; i++) {
mylock.lock();
sum += i;
mylock.unlock();
}
}
死锁问题
问题1
int sum = 0;
mutex mylock;
void work1() {
for (int i = 0; i < 5000; i++) {
mylock.lock();
sum += i;
mylock.unlock();
}
}
void work2() {
for (int i = 5000; i < 10000; i++) {
mylock.lock();
work1();
sum += i;
mylock.unlock();
}
}
当在work2中调用work1时,锁中有锁,从而造成了死锁的局面
问题2
使用多个锁时,容易造成死锁问题
void a1() {
for (int i = 0; i < 10; i++) {
mt1.lock();
Sleep(200);
mt2.lock();
mt2.unlock();
mt1.unlock();
}
}
void a2() {
for (int i = 0; i < 10; i++) {
mt2.lock();
Sleep(200);
mt1.lock();
mt1.unlock();
mt2.unlock();
}
}
int main() {
thread t1(a1);
thread t2(a2);
t1.join();
t2.join();
}
逻辑如下所示:
- 线程1将mt1锁死
同时间,线程2将mt2锁死 - 线程2继续执行,发现mt1被锁死,于是等待mt1被释放
线程1继续执行,发现mt2被锁死,于是等待mt2被释放 - 双方陷入死循环,出现死锁情况
在这里需要简单的调整锁的顺序即可解决问题,即,将a1中锁的顺序与a2中锁的顺序保持一致
使用lockguard
mutex mt1, mt2;
void a1() {
for (int i = 0; i < 10; i++) {
lock(mt1, mt2);
lock_guard<mutex>(mt1, adopt_lock);
lock_guard<mutex>(mt2, adopt_lock);
Sleep(200);
}
}
void a2() {
for (int i = 0; i < 10; i++) {
lock(mt1, mt2);
lock_guard<mutex>(mt2, adopt_lock);
lock_guard<mutex>(mt1, adopt_lock);
Sleep(200);
}
}
使用lock与lock_guard可以自动上锁解锁,不会造成死锁的问题
MPI
参考文章
安装
mpi库的安装,安装环境:Ubuntu
sudo apt install mpich
检查是否安装成功:
which mpicc
which mpif90
which mpirun
测试代码如下:
#include <stdio.h>
#include "mpi.h"
int main(int argc, char *argv[])
{
int rank;
int size;
MPI_Init(0, 0);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
printf("Hello world from process %d of %d\n", rank, size);
MPI_Finalize();
return 0;
}
编译:mpicc -o hello hellow.c
运行:mpirun -np 4 ./hello
结果:
MPI常用六个函数
MPI_Init
用法如下:
int main(int *argc,char* argv[])
{
MPI_Init(&argc,&argv);
}
MPI_Finalize
任何MPI程序结束时,都需要调用该函数
MPI_COMM_RANK
用法:
int MPI_Comm_Rank(MPI_Comm comm, int *rank)
该函数是获得当前进程的进程标识,如进程0在执行该函数时,可以获得返回值0。可以看出该函数接口有两个参数,前者为进程所在的通信域,后者为返回的进程号。通信域可以理解为给进程分组,比如有0-5这六个进程。可以通过定义通信域,来将比如[0,1,5]这三个进程分为一组,这样就可以针对该组进行“组”操作,比如规约之类的操作。这类概念会在之后的MPI进阶一章中讲解。MPI_COMM_WORLD是MPI已经预定义好的通信域,是一个包含所有进程的通信域,目前只需要用该通信域即可。
在调用该函数时,需要先定义一个整型变量如myid,不需要赋值。将该变量传入函数中,会将该进程号存入myid变量中并返回。
比如,让进程0输出Hello,让进程1输出Hi就可以写成如下方式。
示例:
#include <mpi.h>
#include <iostream>
int main(int argc, char *argv[])
{
int rank;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
switch (rank)
{
case 0:
std::cout << "process 0" <<std::endl;
break;
case 1:
std::cout << "process 1" <<std::endl;
break;
case 2:
std::cout << "process 2" <<std::endl;
break;
case 3:
std::cout << "process 3" <<std::endl;
break;
default:
break;
}
MPI_Finalize();
return 0;
}
结果:
MPI_COMM_SIZE
定义:
int MPI_Comm_Size(MPI_Comm, int *size)
作用:
该函数是获取该通信域内的总进程数,如果通信域为MP_COMM_WORLD,即获取总进程数,使用方法和MPI_COMM_RANK相近。
MPI_SEND
定义:
int MPI_Send(type* buf, int count, MPI_Datatype, int dest, int tag, MPI_Comm comm)
该函数参数过多,不过这些参数都很有必要存在。
这些参数均为传入的参数,其中buf为你需要传递的数据的起始地址,比如你要传递一个数组A,长度是5,则buf为数组A的首地址。count即为长度,从首地址之后count个变量。datatype为变量类型,注意该位置的变量类型是MPI预定义的变量类型,比如需要传递的是C++的int型,则在此处需要传入的参数是MPI_INT,其余同理。dest为接收的进程号,即被传递信息进程的进程号。tag为信息标志,同为整型变量,发送和接收需要tag一致,这将可以区分同一目的地的不同消息。比如进程0给进程1分别发送了数据A和数据B,tag可分别定义成0和1,这样在进程1接收时同样设置tag0和1去接收,避免接收混乱。
MPI_RECV
定义:
int MPI_Recv(type* buf, int count, MPI_Datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)
参数和MPI_SEND大体相同,不同的是source这一参数,这一参数标明从哪个进程接收消息。最后多一个用于返回状态信息的参数status。
在C和C++中,status的变量类型为MPI_Status,分别有三个域,可以通过status.MPI_SOURCE,status.MPI_TAG和status.MPI_ERROR的方式调用这三个信息。这三个信息分别返回的值是所收到数据发送源的进程号,该消息的tag值和接收操作的错误代码。
SEND和RECV需要成对出现,若两进程需要相互发送消息时,对调用的顺序也有要求,不然可能会出现死锁或内存溢出等比较严重的问题。
简单案例
#include <mpi.h>
#include <iostream>
int main(int argc, char *argv[])
{
int myId;
int A[4];
int sum = 0;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myId);
switch (myId)
{
case 0:
for (size_t i = 0; i < 4; i++)
{
A[i] = 1;
}
break;
case 1:
for (size_t i = 0; i < 4; i++)
{
A[i] = 2;
}
break;
default:
break;
}
for (size_t i = 0; i < 4; i++)
{
sum += A[i];
}
int k;
MPI_Comm_rank(MPI_COMM_WORLD, &k);
printf("进程%d的sum为:%d\n",k,sum);
if (myId==0)
{
MPI_Send(&sum,1,MPI_INT,1,0,MPI_COMM_WORLD);
}
if (myId==1)
{
int k;
MPI_Status status;
MPI_Recv(&k,1,MPI_INT,0,0,MPI_COMM_WORLD,&status);
sum += k;
}
std::cout << "最终结果为:" << sum << std::endl;
MPI_Finalize();
return 0;
}
结果:
MPI计算圆周率
编写代码
#include <mpi.h>
#include <iostream>
#include <time.h>
double f(double x) {
return 4 / (1 + x*x);
}
int main(int argc, char *argv[])
{
clock_t start, end;
start = clock();
int myid,numprocs;
int N = 400000000;
double sum = 0;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
for (size_t i = myid + 1; i <= N; i+=numprocs)
{
sum += f((i-0.5)/N);
}
switch (myid)
{
case 0:
MPI_Send(&sum,1,MPI_DOUBLE,3,0,MPI_COMM_WORLD);
printf("process %d of %d sum = %.10f\n",myid,numprocs,sum);
break;
case 1:
MPI_Send(&sum,1,MPI_DOUBLE,3,1,MPI_COMM_WORLD);
printf("process %d of %d sum = %.10f\n",myid,numprocs,sum);
break;
case 2:
MPI_Send(&sum,1,MPI_DOUBLE,3,2,MPI_COMM_WORLD);
printf("process %d of %d sum = %.10f\n",myid,numprocs,sum);
break;
case 3:
printf("process %d of %d sum = %.10f\n",myid,numprocs,sum);
double sum0,sum1,sum2;
MPI_Recv(&sum0,1,MPI_DOUBLE,0,0,MPI_COMM_WORLD,MPI_STATUS_IGNORE);
MPI_Recv(&sum1,1,MPI_DOUBLE,1,1,MPI_COMM_WORLD,MPI_STATUS_IGNORE);
MPI_Recv(&sum2,1,MPI_DOUBLE,2,2,MPI_COMM_WORLD,MPI_STATUS_IGNORE);
printf("recv data from sum0 = %.10f\n",sum0);
sum = sum + sum0 + sum1 + sum2;
printf("最终结果为:%.20f\n", sum/N);
break;
default:
break;
}
MPI_Finalize();
end = clock();
printf("总计耗时:%ld ms\n",end-start);
return 0;
}
运行并计算时间:
而使用传统计算方法,使用的时间如下:
代码简化
使用MPI_Send与MPI_Recv虽然可以不错的完成此项任务,但不够简洁。
可以通过使用规约操作对代码进行简化:
MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
该函数可以理解为在通信域MPI_COMM_WORLD中,将各个进程的MPI_DOUBLE型变量起始地址为&mypi长度为1发送到缓冲区,并执行MPI_SUM操作,将结果返回至0进程的&pi地址中。用直白的话讲就是将域内每一个进程的mypi相加求和,并将所得的结果存入0进程中的pi变量。规约函数中有很多操作,比如求和、求积、取最大值、取最小值等等,具体可以查阅手册,这一函数会使得我们的并行程序编写变得很方便。
#include <mpi.h>
#include <iostream>
#include <time.h>
double f(double x) {
return 4 / (1 + x*x);
}
int main(int argc, char *argv[])
{
clock_t start, end;
start = clock();
int myid,numprocs;
int N = 400000000;
double sum = 0;
double res;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
for (size_t i = myid + 1; i <= N; i += numprocs)
{
sum += f((i-0.5)/N);
}
MPI_Reduce(&sum,&res,1,MPI_DOUBLE,MPI_SUM,0,MPI_COMM_WORLD);
MPI_Finalize();
end = clock();
if (myid == 0) {
printf("最终结果为:%.20f\n",res/N);
printf("总计耗时:%ld ms\n",end-start);
}
return 0;
}
openMP
安装
在Ubuntu上安装OpenMP可以先安装支持OpenMP的编译器(如GCC),然后通过编译时加上编译选项“-fopenmp”来开启OpenMP的支持。具体步骤如下:
- 安装GCC编译器
sudo apt-get install gcc
- 安装OpenMP库
sudo apt-get install libomp-dev
- 编译时加上编译选项“-fopenmp”来开启OpenMP的支持
gcc -fopenmp -o output input.c
openMP指令
OpenMP的指令有以下一些:(常用的已标黑)
- parallel,用在一个代码段之前,表示这段代码将被多个线程并行执行
- for,用于for循环之前,将循环分配到多个线程中并行执行,必须保证每次循环之间无相关性。
- parallel for, parallel 和 for语句的结合,也是用在一个for循环之前,表示for循环的代码将被多个线程并行执行。
- sections,用在可能会被并行执行的代码段之前
- parallel sections,parallel和sections两个语句的结合
- critical,用在一段代码临界区之前
- single,用在一段只被单个线程执行的代码段之前,表示后面的代码段将被单线程执行。
- flush,
- barrier,用于并行区内代码的线程同步,所有线程执行到barrier时要停止,直到所有线程都执行到barrier时才继续往下执行。
- atomic,用于指定一块内存区域被制动更新
- master,用于指定一段代码块由主线程执行
- ordered, 用于指定并行区域的循环按顺序执行
- threadprivate, 用于指定一个变量是线程私有的。
#include <iostream>
#include "omp.h"
using namespace std;
int main(int argc, char **argv) {
//设置线程数,一般设置的线程数不超过CPU核心数,这里开4个线程执行并行代码段
omp_set_num_threads(4);
#pragma omp parallel
{
cout << "Hello" << ", I am Thread " << omp_get_thread_num() << endl;
}
}
openMP常用库函数
omp_get_num_procs, 返回运行本线程的多处理机的处理器个数。
omp_get_num_threads, 返回当前并行区域中的活动线程个数。
omp_get_thread_num, 返回线程号
omp_set_num_threads, 设置并行执行代码时的线程个数
omp_init_lock, 初始化一个简单锁
omp_set_lock, 上锁操作
omp_unset_lock, 解锁操作,要和omp_set_lock函数配对使用。
omp_destroy_lock, omp_init_lock函数的配对操作函数,关闭一个锁
openMP子句
private, 指定每个线程都有它自己的变量私有副本。
firstprivate,指定每个线程都有它自己的变量私有副本,并且变量要被继承主线程中的初值。
lastprivate,主要是用来指定将线程中的私有变量的值在并行处理结束后复制回主线程中的对应变量。
reduce,用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的运算。
nowait,忽略指定中暗含的等待
num_threads,指定线程的个数
schedule,指定如何调度for循环迭代
shared,指定一个或多个变量为多个线程间的共享变量
ordered,用来指定for循环的执行要按顺序执行
copyprivate,用于single指令中的指定变量为多个线程的共享变量
copyin,用来指定一个threadprivate的变量的值要用主线程的值进行初始化。
default,用来指定并行处理区域内的变量的使用方式,缺省是shared
复杂矩阵的并行计算
传统矩阵计算方案
#include <iostream>
#include <time.h>
#include <chrono>
using namespace std::chrono;
#define N1 600
#define N2 500
int main(int argc, char const *argv[])
{
auto start = steady_clock::now();
long long int m1[N1][N2];
long long int m2[N2][N1];
long long int m3[N1][N1] = {0};
for (int i = 0; i < N1; i++) {
for (int j = 0; j < N2; j++) {
m1[i][j] = rand()%10 + 1;
m2[j][i] = rand()%10 + 1;
}
}
for (int i = 0; i < N1; i++) {
for (int j = 0; j < N1; j++) {
m3[i][j] = 0;
for (int k = 0; k < N2; k++) {
m3[i][j] += m1[i][k]*m2[k][j];
}
}
}
auto end = steady_clock::now();
auto t = duration_cast<milliseconds>(end-start);
printf("总共耗时:%ld ms\n", t.count());
return 0;
}
使用omp进行并行运算
#include <iostream>
#include <chrono>
#include <time.h>
#include <omp.h>
using namespace std::chrono;
#define N1 600
#define N2 500
int main(int argc, char const *argv[])
{
auto start = steady_clock::now();
omp_set_num_threads(4);
long long int m1[N1][N2];
long long int m2[N2][N1];
long long int m3[N1][N1] = {0};
for (int i = 0; i < N1; i++) {
for (int j = 0; j < N2; j++) {
m1[i][j] = rand()%10 + 1;
m2[j][i] = rand()%10 + 1;
}
}
#pragma omp parallel
{
int kt = omp_get_thread_num();
for (int i = kt - 1; i < N1; i+=8) {
for (int j = 0; j < N1; j++) {
m3[i][j] = 0;
for (int k = 0; k < N2; k++) {
m3[i][j] += m1[i][k]*m2[k][j];
}
}
}
}
auto end = steady_clock::now();
auto t = duration_cast<milliseconds>(end-start);
printf("总共耗时:%ld ms\n", t.count());
return 0;
}
使用MPI进行并行计算
#include <iostream>
#include <chrono>
#include <time.h>
#include <mpi.h>
using namespace std::chrono;
#define N1 600
#define N2 500
int main(int argc, char *argv[])
{
auto start = steady_clock::now();
MPI_Init(&argc, &argv);
long long int m1[N1][N2];
long long int m2[N2][N1];
long long int m3[N1][N1] = {0};
for (int i = 0; i < N1; i++) {
for (int j = 0; j < N2; j++) {
m1[i][j] = rand()%10 + 1;
m2[j][i] = rand()%10 + 1;
}
}
int myid, numprocs;
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
for (int i = myid - 1; i < N1; i += numprocs) {
for (int j = 0; j < N1; j++) {
m3[i][j] = 0;
for (int k = 0; k < N2; k++) {
m3[i][j] += m1[i][k]*m2[k][j];
}
}
}
auto end = steady_clock::now();
auto t = duration_cast<milliseconds>(end-start);
printf("总共耗时:%ld ms\n", t.count());
MPI_Finalize();
return 0;
}