高性能方案设计--并行并发

1. 概念

并行和并发的概念,网上也有很多解释,我从任务的角度来说一下我的理解。
一个任务,包含多个流程,比如写数据到文件当作一个任务,则其包括的流程是,打开文件、写入数据、关闭文件,对应的C语言函数是,fopen, fwrite, fclose 这些。
并行,如果这个任务的所有流程都可以同时执行,就称为并行,而这个任务就是可并行。当然,写数据到文件这个任务,3个流程不可以同时执行。举个更通俗的例子,把搬家作为一个任务,流程是搬凳子、搬椅子、搬桌子等,这些流程是可以并行的,有多个人搬就行。
并发,多个任务可以同时进行。相对于并行是针对单个任务的,而并发是针对多个任务的。比如把搬家作为一个任务,并发就是,有多个人需要搬家,这种是可以同时执行。
如果一个任务有多个可以同时执行的子任务,而这些子任务的流程也可以同时执行,则这个任务就支持并发并行。比如,把多个人搬家作为一个任务,每个人要搬的所有行李作为子任务,而每个人的行李都可以同时搬。
并行和并发的概念,根据实际情况,可以互换。如果把多个人搬家作为一个任务,每个人要搬的行李作为流程,这就是并行的概念。而把每个人要搬的行李作为子任务,这是并发的概念。
但是,在编程里面,并行的研究是,如何把一个串行任务变成并行任务。并发研究的是,如何让多个串行任务同时执行。由于串行任务同时执行,在多核CPU系统中,很容易实现,而要在一个串行任务中发现并行,这个比较难。所以,并发用的比较多。下面的代码是一个并行的例子:
Int a = 5;
Int b = 7;
Int c = a+b;
这3行代码,就这么看是串行的,先给a赋值,再给b赋值,然后a和b相加的值赋给c。由于给a和给b赋值这两个流程是可以独立进行的,不是先给a赋值,再给b赋值,可以把这两个流程并行执行,这就是串行变并行。而a+b这个流程要等上面两个流程执行完才可以,到这里是串行。

2. 高性能中的应用

并行和并发,都需要多个执行者参与,由于参与的执行者多了,自然而然,性能就提高了。
并行是通过减少一个任务的耗时来提高性能,而并发是通过多个任务同时执行来提高性能。在网络编程上面,并行就是通过减少单次任务的延时来提高吞吐量,并发是在延时没有减少的情况下,同时执行多个任务来提高吞吐量。
在C语言里面,并行和并发常用的是多进程或者多线程,这两种方法,网上很多资料。

2.1 并行
并行的例子实在比较难找,我就用快速排序来分析吧。
快速排序的大概流程是,找到一个中位数,把待排序的数据分成两段,左边的小于等于这个中位数,右边的大于这个中位数,然后对这两段的数据各自再找到中位数,再分成多段。伪代码如下:

quickSoft(data)//快速排序函数
{
  middle; //找到中位数,并把数据分成left和right两段
  quickSoft(left);//找到中位数后,分成两段,左边的小于等于中位数
  quickSoft(right);//右边的大于中位数
}

从这里可以看出,quickSoft(left);
和 quickSoft(right); 是可以并行的,我们可以把快速排序修改为并行模式,伪代码如下:

quickSoft(data)//快速排序函数
{
  middle; //找到中位数,并把数据分成left和right两段
  createPthread(&leftId, &quickSoft,
&left);//建立线程来排序左段的数据
  createPthread(&RightId, &quickSoft,
&Right);//建立线程来排序右段的数据
  waitForPthread(&leftId);//等待左段线程结束
  waitForPthread(&righted);//等待右段线程结束
}

虽然快速排序是可以并行的,但实际上,并行不一定能减少排序的时间,要考虑到建立线程的耗时、线程切换的耗时、CPU的数量、数据的量和数据是否有很大的随机性。理论和实际相结合才是选择方案的要素。

单任务的快速排序代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
void swap(int *iaTmp1, int *iaTmp2);
int quickSort( int iaArray[], int iaStartIndex, int iaEndIndex );
void printArray();
#define ARRAY_SIZE 1024
int *pigArray;
int igArraySize;
int main( int argc, char *argv[] )
{
  int *piArray;
  int iTmp;
  if (argc == 1)//如果参数只有程序本身
  {
    igArraySize = ARRAY_SIZE;//设置数据为默认数量
  }
  else
  {
    igArraySize = atoi(argv[1]);//从参数中获取数据的数量
  }
  pigArray = malloc(igArraySize*sizeof(int));//分配内存,用来装下待排序的数据
  if (pigArray == NULL)
  {
    fprintf( stderr, "get ram error\n" );
    fflush(stderr);
    return 0;
  }
  memset(pigArray, 0x00, igArraySize * sizeof(int));
  for (iTmp = 0; iTmp < igArraySize; iTmp++)//生成随机数来排序
  {
    *(pigArray + iTmp) = rand()&0x0000FFFF;
  }
  printArray( );//打印出生成的随机数,以空格分开
  quickSort( pigArray, 0, igArraySize - 1 ); //开始快速排序
  fprintf(stderr, "after sort\n");fflush(stderr);
  printArray();//打印排序后的随机数
}
int quickSort( int *iaArray, int iaStartIndex, int iaEndIndex )
{
  int iMid;
  int iStartIndex;
  int iEndIndex;
  int iMidIndex;
  iStartIndex = iaStartIndex;
  iEndIndex = iaEndIndex;
  /*如果只有一个数,不处理*/
  if (iaStartIndex == iaEndIndex)
  {
    return 0;
  }
  /*如果是有两个数*/
  if (iEndIndex - iStartIndex == 1)
  {
    /*如果第一个数比第二个数大,互换*/
    if (*(iaArray + iStartIndex) > *(iaArray + iEndIndex))
    {
      swap(iaArray + iStartIndex, iaArray + iEndIndex);
    }
    return 0;
  }
  /*用第一个数作为参考值*/
  iMid = *(iaArray+iStartIndex);
  iStartIndex++;
  /*循环处理,找出中值,左边小于等于参考值,右边大于参考值*/
  while (1)
  {
    /*从左开始,找出第一个大于等于参考值的*/
    while (iStartIndex < iEndIndex && *(iaArray+iStartIndex) <= iMid)
    {
      iStartIndex++;
    }
    /*如果没有比参考值大的*/
    if (iStartIndex == iEndIndex)
    {
      iMidIndex = iStartIndex;
      break;
    }
    /*有比参考值大的*/
    /*从右开始,找出第一个小于等于参考值的*/
    while (iEndIndex > iStartIndex && *(iaArray+iEndIndex) > iMid)
    {
      iEndIndex--;
    }
    /*如果没有比参考值小的*/
    if (iStartIndex == iEndIndex)
    {
      iMidIndex = iStartIndex - 1;
      break;
    }
    /*互换数据*/
    swap(iaArray+iStartIndex, iaArray+iEndIndex);
    /*如果是相邻的两个数,左边的比参考值小,右边的大于等于参考值*/
    if (iStartIndex + 1 == iEndIndex)
    {
      iMidIndex = iStartIndex;
      break;
    }
    /*指向下一个数*/
    iStartIndex++;
    iEndIndex--;
    /*如果之间只隔了一个数*/
    if (iStartIndex == iEndIndex)
    {
      /*如果这个数比参考值大*/
      if (*(iaArray+iStartIndex) > iMid)
      {
        iMidIndex = iStartIndex -1;
      }
      else
      {
        iMidIndex = iStartIndex;
      }
      break;
    }
  }
  /*如果都小于等于参考值的*/
  if (iMidIndex == iaEndIndex)
  {
    /*互换数据*/
    swap(iaArray + iaStartIndex, iaArray + iaEndIndex);
    iMidIndex--;
  }
  quickSort(iaArray, iaStartIndex, iMidIndex);
  quickSort(iaArray, iMidIndex + 1, iaEndIndex);
  return 0;
}
//打印数据
void printArray()
{
  int iTmp;
  fprintf( stderr, "\n" );
  for (iTmp = 0; iTmp < igArraySize; iTmp++)
  {
    fprintf( stderr, "%d ", pigArray[iTmp] );
  }
  fprintf(stderr, "\n" );
  fflush(stderr);
}
//交换数据
void swap(int *iaTmp1, int *iaTmp2)
{
  int iTmp;
  iTmp = *iaTmp1;
  *iaTmp1 = *iaTmp2;
  *iaTmp2 = iTmp;
  return;
}

编译是 gcc –o quickSort quickSort.c
执行是 quickSort 随机数数量

并行排序的代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>
#define ARRAY_SIZE 1024
int *pigArray;
int igArraySize;
struct quickStruct
{
  int *piArray;
  int iStartIndex;
  int iEndIndex;
  int iDeep;
};
void swap(int *iaTmp1, int *iaTmp2);
int quickSort(struct quickStruct *saqs);
void printArray();
int main(int argc, char *argv[])
{
  struct quickStruct sqs;
  int *piArray;
  int iTmp;
  if (argc == 1) //如果参数只有程序本身
  {
    igArraySize = ARRAY_SIZE; //设置数据为默认数量
  }
  else
  {
    igArraySize = atoi(argv[1]); //从参数中获取数据的数量
  }
  pigArray = malloc(igArraySize * sizeof(int)); //分配内存,用来装下待排序的数据
  if (pigArray == NULL)
  {
    fprintf(stderr, "get ram error\n");
    fflush(stderr);
    return 0;
  }
  memset(pigArray, 0x00, igArraySize * sizeof(int));
  for (iTmp = 0; iTmp < igArraySize; iTmp++) //生成随机数来排序
  {
    *(pigArray + iTmp) = rand() & 0x0000FFFF;
  }
  printArray();                    //打印出生成的随机数,以空格分开
  memset(&sqs, 0x00, sizeof(sqs)); //初始化结构体
  sqs.iDeep = 0;
  sqs.piArray = pigArray;
  sqs.iStartIndex = 0;
  sqs.iEndIndex = igArraySize - 1;
  //开始快速排序
  quickSort(&sqs);
  fprintf(stderr, "after sort\n");
  fflush(stderr);
  printArray(); //打印排序后的随机数
}
int quickSort(struct quickStruct *saqs)
{
  int iMid;
  int iStartIndex;
  int iEndIndex;
  int iMidIndex;
  int *iArray;
  iArray = saqs->piArray;
  iStartIndex = saqs->iStartIndex;
  iEndIndex = saqs->iEndIndex;
  /*如果只有一个数,不处理*/
  if (saqs->iStartIndex == saqs->iEndIndex)
  {
    return 0;
  }
  /*如果是有两个数*/
  if (iEndIndex - iStartIndex == 1)
  {
    /*如果第一个数比第二个数大,互换*/
    if (*(iArray + iStartIndex) > *(iArray + iEndIndex))
    {
      swap(iArray + iStartIndex, iArray + iEndIndex);
    }
    return 0;
  }
  /*用第一个数作为参考值*/
  iMid = *(iArray + iStartIndex);
  iStartIndex++;
  /*循环处理,找出中值,左边小于等于参考值,右边大于参考值*/
  while (1)
  {
    /*从左开始,找出第一个大于等于参考值的*/
    while (iStartIndex < iEndIndex && *(iArray + iStartIndex) <= iMid)
    {
      iStartIndex++;
    }
    /*如果没有比参考值大的*/
    if (iStartIndex == iEndIndex)
    {
      iMidIndex = iStartIndex;
      break;
    }
    /*有比参考值大的*/
    /*从右开始,找出第一个小于等于参考值的*/
    while (iEndIndex > iStartIndex && *(iArray + iEndIndex) > iMid)
    {
      iEndIndex--;
    }
    /*如果没有比参考值小的*/
    if (iStartIndex == iEndIndex)
    {
      iMidIndex = iStartIndex - 1;
      break;
    }
    /*互换数据*/
    swap(iArray + iStartIndex, iArray + iEndIndex);
    /*如果是相邻的两个数,左边的比参考值小,右边的大于等于参考值*/
    if (iStartIndex + 1 == iEndIndex)
    {
      iMidIndex = iStartIndex;
      break;
    }
    /*指向下一个数*/
    iStartIndex++;
    iEndIndex--;
    /*如果之间只隔了一个数*/
    if (iStartIndex == iEndIndex)
    {
      /*如果这个数比参考值大*/
      if (*(iArray + iStartIndex) > iMid)
      {
        iMidIndex = iStartIndex - 1;
      }
      else
      {
        iMidIndex = iStartIndex;
      }
      break;
    }
  }
  /*如果都小于等于参考值的*/
  if (iMidIndex == saqs->iEndIndex)
  {
    /*互换数据*/
    swap(iArray + saqs->iStartIndex, iArray + saqs->iEndIndex);
    iMidIndex--;
  }
  int iPthreadLeft;
  int iPthreadRight;
  struct quickStruct sqsLeft;
  struct quickStruct sqsRight;
  //初始化左结构体的数据,并设置
  memset(&sqsLeft, 0x00, sizeof(sqsLeft));
  sqsLeft.piArray = iArray;
  sqsLeft.iDeep = saqs->iDeep + 1;         //设置深度+1
  sqsLeft.iStartIndex = saqs->iStartIndex; //设置分段后的数据的开始位置
  sqsLeft.iEndIndex = iMidIndex;           //设置分段后的数据的结束位置
  //初始化右结构体的数据,并设置
  memset(&sqsRight, 0x00, sizeof(sqsLeft));
  sqsRight.piArray = iArray;
  sqsRight.iDeep = saqs->iDeep + 1;     //设置深度+1
  sqsRight.iStartIndex = iMidIndex + 1; //设置分段后的数据的开始位置
  sqsRight.iEndIndex = saqs->iEndIndex; //设置分段后的数据的结束位置
  //建立线程
  if (pthread_create(&iPthreadLeft, NULL, (void *)(&quickSort), &sqsLeft) != 0)
  {
    fprintf(stderr, "start left pthread error[%d][%s]\n", errno, strerror(errno));
    fflush(stderr);
    exit(1);
  }
  if (pthread_create(&iPthreadRight, NULL, (void *)(&quickSort), &sqsRight) != 0)
  {
    fprintf(stderr, "start right pthread error[%d][%s]\n", errno, strerror(errno));
    fflush(stderr);
    exit(1);
  }
  //等待线程结束
  void *status;
  pthread_join(iPthreadLeft, &status);  //left 线程
  pthread_join(iPthreadRight, &status); //right 线程
  return 0;
}
//打印数据
void printArray()
{
  int iTmp;
  fprintf(stderr, "\n");
  for (iTmp = 0; iTmp < igArraySize; iTmp++)
  {
    fprintf(stderr, "%d ", pigArray[iTmp]);
  }
  fprintf(stderr, "\n");
  fflush(stderr);
}
//交换数据
void swap(int *iaTmp1, int *iaTmp2)
{
  int iTmp;
  iTmp = *iaTmp1;
  *iaTmp1 = *iaTmp2;
  *iaTmp2 = iTmp;
  return;
}

编译是 gcc –o quickPthreadSort quickPthreadSort.c –lpthread
执行是 quickPthreadSort 随机数数量

快排的速度很快,即使是在超级本上执行,对1000万个0~65535的随机数进行排序,单任务的耗时是53.249秒,并行的耗时是24.432秒。由于速度很快,对分段后的少量数据排序的耗时,甚至比创建线程的时间都要少,所以,即使在并行的情况下,也没快多少。

2.2 并发
并发是针对任务的,和并行一样,找到并发点就可以了,例子比较多,我原来的文章 https://blog.csdn.net/highhandli/article/details/105229750就是关于tcp epoll多线程并发的,在accept后,开启线程处理剩下的流程。我这里用文件数据同步来说明吧。
有这样一个任务,取得上游数据文件,和本地的数据库同步。数据文件中的每一行数据对于数据库的每一条数据,有主键,根据主键,如果数据库存在,就更新数据库的数据,否则,将文本的数据插入到数据库。原来的伪代码如下:

connectDb;//连接数据库
openFile;//打开文件
while (1)
{
  if (readData == NULL)//从文件读取数据,如果没有数据了,结束
  {
     break;
  }
  if (insertData == ERROR)//如果插入数据失败,表示主键重复
  {
     updateData;//更新数据库
  }
  commit;//提交数据
}
closeDb;//关闭数据库
closeFile;//关闭文件

这个代码是读取一条数据就同步一条数据,并且是一个任务在执行,当时800万的数据,同步到数据库耗时7个小时,离投产时间2个月,离下次性能测试2个星期,让我来优化,剩余时间也不算多了。
这里,用一个数据库连接,每次只处理一条数据,其实,可以建立多个数据库连接,也就是用并发,每个连接都处理一条数据,这样就相当于同时处理多条数据了,可以提高总体性能,修改后的伪代码如下:

openFile;//打开文件
while (1)
{
  if (read5000Data == NULL)//从文件读取5000条数据,如果没有数据或者数据不足5000条了,结束
  {
     break;
  }
  fork;//建立子进程处理数据
  connectDb;//连接数据库
  if (insertData == ERROR)//如果插入数据失败,表示主键重复
  {
     updateData;//更新数据库
  }
  commit;//提交数据
  closeDb;//关闭数据库
  exit; //子进程退出
}
//处理上面不足5000条的数据
fork;//建立子进程处理数据
connectDb;//连接数据库
if (insertData == ERROR)//如果插入数据失败,表示主键重复
{
   updateData;//更新数据库
}
commit;//提交数据
closeDb;//关闭数据库
closeFile;//关闭文件

经过并发后,性能基本和并发量成正比,当然也要数据库的性能跟得上。最后我启动了128个进程,6分钟就同步完所有数据了。
伪代码里没显示进程限制和进程回收的情况,比如限制在最多启动30个线程,就要用到waitPid函数来等待子进程退出了。
上面的并发我之所以用多进程而不是多线程,主要考虑的是时间和改动量的问题。的确,我一开始是考虑用多线程,每个线程连接一个数据库,同步数据在每个线程里实现。但是,由于改成多线程,对原来的代码的改动量大很多,甚至改动了主要结构,只有2周的时间,未必能赶得上,而且当时是稳定优先的,如果改动多了,就不允许上线了,所以,我才用多进程,加个fork()就可以新建一个子进程了,而且还是在主要流程上面fork的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值