并行处理及分布式系统

article/2025/9/13 14:42:26

并行处理及分布式系统

1 为什么要并行计算

1.1 为什么需要不断提升性能

  • 随着计算能力的增加,我们所面临的计算问题和需求也在增加
  • 随着技术的进步,我们从未想过的技术得到了解决,比如:人类基因解码、更准确的医疗成像、更快速准确的网络搜索…
  • 更复杂的问题仍有待解决,比如:气候模拟、蛋白质折叠、药物发现、能源研究、数据分析…

1.2 为什么要构建并行系统

单处理器性能大幅度提升的主要方法是增加集成电路晶体管密度。但是随着晶体管尺寸的减小,晶体管的传递速度增快,它们的能耗也在增加,大多数的能量是以热能的形式消耗,当一块集成电路变得太热的时候,就会变得不可靠。在 21 世纪的第一个 10 年中,使用空气冷却集成电路的散热能力已达到极限。也就是说,只通过增加集成电路的速度来提升处理器的性能的方法不可取。在这种条件下,集成电路制造商提出多核处理器的思路。

1.3 为什么要编写并行程序

大多数为传统单核系统编写的程序无法利用多核处理器,为了使程序能够更快地运行,有更加逼真的图像,就需要将串行程序改写为并行程序。

1.4 如何编写并行程序

想要解决某个问题,需要编写其对应的并行程序,首先需要对任务进行划分,确定其属于任务并行还是数据并行。

  • 任务并行:当许多任务或函数可以独立地、大规模地并行执行时,这就是任务并行。任务并行的重点在于利用多核系统对任务进行分配,将待解决的问题所需要的执行的各个任务分配到各个核上执行。
  • 数据并行:当可以同时处理许多数据时,这就是数据并行。数据并行的重点在于利用多核系统对数据进行分配,每个核在分配到的数据集上执行大致相似的数据操作

1.5 接下来做什么

首先我会参考《并行程序设计导论》所讲的 C 语言的三种拓展:消息传递接口(Message-Passing Interface,MPI)、POSIX 线程(POSIX threads,Pthreads) 和 OpenMP 来编写基本的并行程序。

选择并行程序实现框架的时候应该根据计算机的架构来选择,即根据硬件选择软件。Pthreads 和 OpenMP 是为 共享内存系统 的编程而设计的,它们提过访问共享内存的机制;而 MPI 是为 分布式内存系统 的编程而设计的,它提供发送消息的机制。

图中(a)表示共享内存系统,(b)表示分布式内存系统。

1.6 并发、并行、分布式

  • 并发计算:一个程序的多个任务再同一个时段内可以同时执行
  • 并行计算:一个程序通过多个任务紧密协作来解决某个问题
  • 分布式计算:一个程序需要与其他程序写作来解决某个问题

2 并行硬件和并行软件

2.1 背景知识

并行计算涉及到两个不同的技术领域:

  • 计算机架构(硬件)
  • 并行程序设计(软件)

硬件主要的目标就是为软件提供更快的计算速度,更低的性能功耗比,硬件结构上支持更快的并行。
软件的主要目的是使用当前的硬件压榨出最高的性能,给应用提供更稳定快速的计算结果。

2.1.1 冯 · 洛依曼结构

经典的冯 · 洛依曼结构包括存储器、运算器、控制器、输入设备、输出设备,其中运算器和控制器都在 CPU 之中,CPU 和主存通过总线连接。当数据或指令从主存传送到 CPU 时,称为数据或指令从内存中取出或者读出;当数据或指令从 CPU 传送到主存时,称为数据或指令写入或者存入内存中。这样主存和 CPU 之间的分离称为 冯 · 洛依曼瓶颈。

2.1.2 进程、多任务及线程

  • 进程:正在运行的程序的一个实例。

  • 多任务:通过时间片轮转的方式使人产生多个任务同时执行的错觉。

  • 线程:线程包含在进程中,同一个进程内的线程共享内存和 I/O 设备,利用线程程序员可以将程序划分为多个大致独立的任务。

2.2 对冯 · 洛依曼模型的改进

《并行程序设计导论》介绍了三种对冯 · 洛依曼模型的改进措施:缓存、虚拟内存、低层次并行

2.2.1 缓存(Cache)

基于访存局部性而设计的 CPU 缓存(CPU Cache)有着比其他存储器更小的访问开销,CPU Cache 通常和 CPU 位于同一块芯片上,访存开销比普通内存小很多。

局部性原理

  • 访问一个位置之后,接着访问其附近的位置
  • 空间局部性:访问临近的位置
  • 时间局部性:最近访问的位置,在不久的将来还会访问

CPU Cache 一般分为不同的层(level),第一层(L1)最小但是访问速度最快,更高层的 Cache (L2, L3, …)更大但访问速度较慢。大多数系统采用 3 层 Cache,每层 Cache 中的数据不重合,且都会存放在主存中。

当 CPU 需要访问数据或指令时,它会沿着 L1 Cache -> L2 Cache -> L3 Cache -> 主存 这条路径查询,若从 Cache 中查询到数据或指令时,则称为 Cache 命中 或 命中;若没有从 Cache 中查询到则称为 Cache 缺失 或 缺失。

Cache 命中 Cache 缺失

当 CPU 向 Cache 写数据时,会出现 Cache 中的值与主存中的值不一致的情况,为了解决这个问题,数中介绍了两种方法:

  • 写直达:当 CPU 向 Cache 写数据时,更新主存中的数据。
  • 写回:将 Cache 中的数据标记为 脏数据,当 Cache line 被主存中的新的 Cache line 替换时,脏的 Cache line 会被写入主存。

在 Cache 设计中,另一个问题是 Cache line 应该存储在什么位置。当从主存中取出一个 Cache line 时,应该把这个 Cache line 放到 Cache 中的什么位置,不同的系统采用不同的方式,这些方式分别为:

  • 全相联:一个新的 Cache line 可以放在 Cache 中的任意位置
  • 直接映射:每一个 Cache line 在 Cache 中都有唯一的位置
  • n路组相联:每个 Cache line 可以被放在 n 个不同的位置中的一个

当主存中的行能被映射到不同到 Cache 中的不同位置时,需要决定替换或者驱逐 Cache 中的某一行。常用的方案是最近最少使用

2.2.2 虚拟内存

如果一个大型的程序或者程序需要访问大型数据集,那么所有的指令或者数据可能在主存中放不下。采用 虚拟内存,使得主存可以作为辅存的缓存。利用时空局部性的原理,只把正在运行程序的活动部分保存在主存中。

2.2.3 低层次并行

指令级并行

指令级并行通过让多个处理器部件或者功能单元同时执行指令来提高处理器的性能。有两种主要方法来实现指令级并行:

  • 流水线:将功能单元分阶段安排。
  • 多发射:让多条指令同时启动。
    • 静态多发射:功能单元在编译时调度
    • 动态多发射:功能单元在运行时调度

硬件多线程

硬件多线程为系统提供了一种机制,使得当前执行的任务被阻塞时,系统能够继续其他有用的工作。

  • 细粒度多线程:处理器在每条指令执行完后切换线程,从而跳过被阻塞的线程。
  • 粗粒度多线程:只切换那些需要等待较长时间才能完成操作而被阻塞的线程。
  • 同步多线程:通过允许多个县城同时使用多个功能单元来利用超标量处理器的性能。

2.3 并行硬件

利用 Flynn 分类法 对计算机体系结构进行划分:

分别以数据和指令进行分析:

  • 单指令单数据 SISD (传统串行计算机,386)
  • 单指令多数据 SIMD (并行架构,比如向量机,所有核心指令唯一,但是数据不同,现在 CPU 基本都有这类的向量指令)
  • 多指令单数据 MISD (少见,多个指令围殴一个数据)
  • 多指令多数据 MIMD (并行架构,多核心,多指令,异步处理多个数据流,从而实现空间上的并行,MIMD 多数情况下包含 SIMD,就是 MIMD 有很多计算核,计算核支持 SIMD)

注:GPU 属于 SPMD,但是其可以使用 SIMD 并行

计算机架构也可以 根据内存划分:

  • 共享内存系统
    共享内存系统
  • 分布式内存系统
    分布式内存系统

2.4 并行软件

负载均衡:将任务在线程或进程之间分配,并使得每个进程或线程获得大致相等的工作量。

将串行程序或者算法转换为并行程序的过程称为 并行化。某些程序,如果能够通过简单地将会任务分配给进程或线程来实现并行化,我们称该程序是 易并行的

动态线程

  • 主线程等待工作请求,当一个请求到达时,它会派生出一个新线程来执行该请求。当新线程完成任务后,就会终止执行再合并到主线程中。
  • 资源的有效使用,但是线程的创建和终止非常耗时。

静态线程

  • 创建线程池并分配任务,但线程不被终止直到被清理。

  • 性能更好,但可能会浪费系统资源。

非确定性:在任何一个 MIMD 系统中,如果处理器异步执行,那么很可能会引发 非确定性。给定的输入能产生不同的输出,这种计算称为非确定性。

临界区:一次只能被一个线程执行的代码块。访问临界区的行为应该是 互斥的

保证互斥执行的最常用机制是 互斥锁 或者 互斥量

2.5 输入和输出

为了解决多个进程或线程直接输入或输出的矛盾,需要遵循以下规则:

  • 在分布式内存程序中,只有进程 0 能够访问 stdin。在共享内存程序中,只有主线程或线程 0 能够访问 stdin。
  • 在分布式内存和共享内存系统中,所有进程或线程都能够访问 stdout 和 stderr。
  • 但是,因为输出到 stdout 的非确定性顺序,大多数情况下,只有一个进程或线程会将结果输出到 stdout。但输出调试程序的结果是一个里外,在这种情况下,允许多个进程或线程写 stdout
  • 只有一个进程或线程会尝试访问一个除 stdin、stdout 或者 stderr 外的文件。
  • 调试程序输出在生成输出结果时,应该包括进程或线程的序号或者进程标识符。

2.6 性能

线性加速比 T 并 行 = T 串 行 / p T_{并行}=T_{串行}/p T=T/p,其中 p p p 表示程序运行所运行的系统的核数。

加速比
S = T 并 行 T 串 行 S=\cfrac{T_{并行}}{T_{串行}} S=TT
并行程序的效率
E = S P = T 串 行 p ⋅ T 并 行 E=\frac{S}{P}=\cfrac{T_{串行}}{p\cdot{T_{并行}}} E=PS=pTT
并行开销的影响
T 并 行 = T 串 行 / p + T 开 销 T_{并行}=T_{串行}/p\ +\ T_{开销} T=T/p + T

阿姆达尔定律

除非一个串行程序的执行几乎全都并行化,否则,不论有多少可以利用的核,通过并行化所产生的加速比都会是受限的。

假设原串行程序所需时间为 T 串 行 T_{串行} T,其中的 α \alpha α 的比例能够并行化,假设并行化的这一部分使用 p p p 个核,则程序中可以并行化的部分的加速比为 p p p。当 p p p 趋于无穷大时,该程序的加速比为:
S ∞ = 1 1 − α S_{\infty} = \frac{1}{1-\alpha} S=1α1
假设该程序可并行化部分并行化之后,其整体的加速比为:
S = T 串 行 α × T 串 行 / p + ( 1 − α ) × T 串 行 = 1 α / p + ( 1 − α ) S=\frac{T_{串行}}{\alpha\times{T_{串行}}/p\ +\ (1-\alpha)\times{T_{串行}}}=\frac{1}{\alpha/p\ +\ (1-\alpha)} S=α×T/p + (1α)×TT=α/p + (1α)1
所以可得:
S ≤ 1 1 − α S\le \frac{1}{1-\alpha} S1α1

可扩展性

假设我们运行一个拥有固定进程或线程数目的并行程序,并且它的输入规模也是固定的,那么我们可以得到一个效率值 E E E。现在,我们增加该程序所用的进程或线程数,如果在输入规模也以相应增长率增加的情况下,该程序的效率一直都是 E E E,那么我们就称该程序是 可拓展的

如果在增加进程或线程的个数时,可以维持固定的效率,却不增加问题的规模,那么程序称为 强可拓展性的

如果在增加进程或线程个数的同时,只有以相同倍率增加问题的规模才能使效率值保持不变,那么程序就称为 弱可拓展的

2.7 并行程序设计

Foster 方法

  • 划分 问题并识别任务:将要执行的指令和数据按照计算拆分为多个小任务。这一步的关键在于识别出可以并行执行的任务。
  • 在任务中识别要执行的 通信:确定前一步所识别出来的任务之间需要执行哪些通信。
  • 凝聚聚合 任务使之变成较大的组任务:将第一步中确定的任务和通信合并成更大的任务。
  • 将聚合任务 分配 给进程或线程:将聚合好的任务分配给进程或线程,还要使通信最小化,使得每个进程或线程得到的工作量大致均衡(负载均衡)。

3 MPI编程

分布式内存系统

MPI 是为 分布式内存系统 的编程而设计的,它提供发送消息的机制。本节将会介绍怎么利用 MPI 使用 消息传递 来对分布式内存系统进行编程。

3.1 预备知识

头文件

#include <mpi.h>

编译

mpicc -g -Wall -o outputname.out filename.c

执行

mpiexec -n <number of processes> <executable>

例如:

mpiexec -n 4 ./mpi_hello.out

运行 mpi_hello.out 文件并开启 4 个进程。

程序框架

//...
#include <mpi.h>
//...
int main(int argc, char *argv[])
{//...MPI_Init(&argc, &argv);//...MPI_Finalize();//...return 0;
}

通信子

int my_rank;//进程编号
int comm_sz;//进程数量
MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
MPI_Comm_size(MPI_COMM_WORLD, &Comm_sz);

其中 MPI 创建,由所有进程组成的通信子,称为 MPI_COMM_WORLD。

SPMD 程序

  • 单程序多数据流
  • 只编译一个程序
  • 进程 0 做一些不同的事情
  • if-else 结构使程序是 SPMD

通信

MPI_Send(send_buf_p, send_buf_sz, send_type, dest, send_tag, send_comm);//q 号进程调用
MPI_Recv(recv_buf_p, recv_buf_sz, recv_type, src, recv_tag, recv_comm, &status);//r 号进程调用

则 q 号进程调用 MPI_Send 函数所发送的消息可以被 r 号进程调用 MPI_Recv 函数接受,如果:

  • recv_comm = send_comm
  • recv_tag = send_tag
  • dest = r
  • src = q
MPI_Recv(result, result_sz, result_type, MPI_ANY_SOURCE, result_tag, comm, MPI_STATUS_IGNORE);

当设置 src 为 MPI_ANY_SOURCE 时,该进程接收所有进程发送给他的信息。

集合通信

集合通信 不同于只涉及两个进程的 MPI_Send 和 MPI_Recv,它涉及到一个通信子汇总的所有进程。为了区分这两种通信方式,MPI_Send 和 MPI_Recv 常常称为 点对点通信

4 Pthreads编程

共享内存系统

4.1 预备知识

头文件

#include <pthread.h>

编译

gcc -g -Wall -o outputname.out filename.c -lpthread 

执行

./filename.out <number of threads>

例如,运行 4 个线程的程序:

./filename.out 4

4.2 竞争条件

当多个线程同时执行时,多个线程执行语句的顺序通常是非确定的。当多个线程试图访问同一个共享资源时,并且其中至少有一个访问是更新操作,这样的访问可能会导致错误,导致结果的不确定性,我们称这种现象为 竞争条件。编写共享内存程序时最重要的任务之一就是识别和更正竞争条件。

4.3 临界区

临界区 是一个代码块,在这个代码块中,任意时刻还有一个线程能够更新共享资源,因此临界区中的代码执行应该作为串行代码执行。因此在设计程序时,应该尽可能少地使用临界区,并且使用的临界区应该尽可能短。

有三种避免对临界区竞争访问的基本方法:忙等待、互斥量和信号量。

4.4 忙等待

在忙等待中,线程不停地测试某个条件,但实际上,直到某个条件满足之前,不会执行任何有用的工作。

但是要注意编译器的优化会影响忙等待的正确执行。

4.5 互斥锁

互斥量 是互斥锁的简称,它是一个特殊类型的变量,通过某些特殊类型的函数,互斥量可以用来限制每次只有一个线程能进入临界区。互斥量保证了一个线程独享临界区,其他线程在有线程已经进入该临界区的情况下,不能同时进入。

4.6 信号量

产生原因:

  • 忙等待强制顺序线程访问临界区,造成资源浪费。

  • 使用互斥量,顺序由系统自己决定。

  • 在一些应用程序中,我们需要控制线程访问临界区的顺序。

5 OpenMP编程

共享内存系统

5.1 预备知识

预处理器指令:#pragma

编译

gcc -g -Wall -fopenmp -o outputname.out filename.c

执行

./filename.out <number of processes>

例如:

./omp_hello.out 4

程序

最基本的 parallel 指令可以以如下简单的形式表示:

#pragma omp parallel

子句

许多 OpenMP 指令可以被 子句 修改。使用最频繁的是 num_threads 子句,当时用 OpenMP 指令启动线程组时,可以通过修改 num_threads 子句来启动需要数目的线程。

6 并行程序开发

7 代码附录

梯形积分法 MPI 程序

矩阵向量乘

通过数学求和公式计算π

蒙特卡洛方法计算π

奇偶交换排序


http://chatgpt.dhexx.cn/article/tdI09Nof.shtml

相关文章

并行计算之MPI(一)

MPI学习 1. 了解并行计算 为什么要采用并行计算&#xff1f; &#xff08;1&#xff09;这是因为它可以加快速度即在更短的时间内解决相同的问题或在相同的时间内解决更多更复杂的问题特别是对一些新出现的巨大挑战问题&#xff1b; &#xff08;2&#xff09;节省投入并行计…

并行网关

1、并行网关 假设现在我们想在旁边放一份沙拉。无论如何&#xff0c;如果你想要沙拉&#xff0c;你可以像我们在图1.1中所做的那样建模。 图1.1:准备沙拉和主菜。 在这里&#xff0c;我们介绍了另一个符号&#xff1a;(文本)注释&#xff1b;这是一个您可以与任何流对象(在本…

并行计算:循环程序并行化的一般方法

一、数据划分和处理器指派 1. 带状划分方法 又叫做行列划分&#xff0c;就是将矩阵的整行或整列分成若干组&#xff0c;各组指派给一个处理器。 例如&#xff1a;设矩阵A由n行和m列&#xff0c;对其串行处理的程序段如下&#xff1a; for i1 to n dofor j1 to m doProcess(a[…

并行计算之MPI(三)

了解MPI 什么是MPI &#xff08;1&#xff09;MPI是一个库而不是一门语言&#xff0c;许多人认为MPI就是一种并行语言&#xff0c;这是不准确的。但是按照并行语言的分类可以把FORTRANMPI或CMPI。看作是一种在原来串行语言基础之上扩展后得到的并行语言&#xff0c;MPI库可以被…

并行计算的一些思考与总结

弗林分类法 根据弗林分类法&#xff0c;计算机结构主要分为 SIMD----单指令、多数据MIMD---多指令、多数据SISD----单指令、单数据MISD---多指令、单数据 一般的串行程序中为SISD&#xff0c;即在单核CPU下任何时间和地点只有一个指令处理一个数据&#xff0c;其所谓的多线程…

并行计算之MPI(二)

1.并行编程模型 目前两种最重要的并行编程模型是数据并行和消息传递数据并行编程模型的编程级别比较高编程相对简单但它仅适用于数据并行问题消息传递编程模型的编程级别相对较低但消息传递编程模型可以有更广泛的应用范围。 数据并行即将相同的操作同时作用于不同的数据因此…

Matlab 并行

Matlab 并行 1. 检查是否有并行附加功能2. 创建和删除并行2.1 创建默认的并行池2.2 在本地创建2.3 在集群创建2.4 删除 3. Parallel pool 包含的一些函数3.1 parfor3.2 parfeval 初学&#xff0c;肯定有理解不够的地方。看官方文件更靠谱。 1. 检查是否有并行附加功能 如果没有…

并行处理及分布式系统期末总结笔记

并行处理及分布式系统期末总结笔记 1、任务并行、数据并行的应用2、冯诺依曼体系结构的瓶颈及改进&#xff0c;Flynn分类法涉及的几种模型及其特点3、Cache的特点&#xff0c;Cache缺失、Cache命中、Cache一致性及解决方法、伪共享、流水线、多发射4、加速比、效率、阿姆达尔定…

并行程序设计导论期末复习

任务并行、数据并行的应用 任务并行 将待解决问题所需要执行的各个任务分配到各个核上执行。 数据并行 将待解决问题所需要处理的数据分配给各个核&#xff0c;每个核在分配到的数据集上执行大致相似的操作。 冯诺依曼体系结构的瓶颈及改进&#xff0c;Flynn分类法涉及的几…

并行程序设计导论 概念总结

Parallel Programing caiyi 2021/6/17 第一章 1.为什么要构建并行系统? 电路晶体管密度过大会使处理器能耗增加&#xff0c;散热的问题使通过继续增快集成电路密度提高处理器性能不再现实&#xff0c;因此集成电路商决定构建多核处理器。 2.为什么要编写并行程序&#xf…

cuda 并行计算

1 简介 2006年&#xff0c;NVIDIA公司发布了CUDA&#xff0c;CUDA是建立在NVIDIA的GPU上的一个通用并行计算平台和编程模型&#xff0c;基于CUDA编程可以利用GPU的并行计算引擎来更加高效地解决比较复杂的计算难题。CUDA是NVIDIA公司所开发的GPU编程模型&#xff0c;它提供了GP…

数据 并行

first 含义是计算机内包含一组处理单元&#xff08;PE&#xff09;&#xff0c;每一个处理单元存储一个&#xff08;或多个&#xff09;数据元素。当机器执行顺序程序时&#xff0c;可对应于全部或部分的内部处理单元所存的数据同时操作。 将并行处理技术引入信息检索领域 把数…

并行的常见问题和注意事项

关于Oracle中的并行&#xff0c;可以说是一把双刃剑&#xff0c;用得好&#xff0c;可以充分利用系统资源&#xff0c;提升数据库的处理能力&#xff0c;用得不好&#xff0c;可能会适得其反。 并行的基本使用方法&#xff0c;对于大部分SQL开发者和DBA来说&#xff0c;并行的一…

并发和串行、并行的概念

先抛开语言不管&#xff0c;只聊概念&#xff0c;说起并发&#xff0c;就很容易想到它和串行、并行的区别。 串行&#xff1a;一次只能取得一个任务并执行这个任务&#xff0c;这个任务执行完后面的任务才能继续&#xff1b; 并发&#xff1a;指的是在同一个时间段内&#xf…

牛腩新闻发布--过程或函数 'news_selectByCaId' 需要参数 '@caid',但未提供该参数(二)

发现问题 之前有一篇博客是因为存储过程中没有添加相应的函数&#xff0c;导致出现了“过程或函数 ‘news_selectByCaId’ 需要参数 ‘caid’&#xff0c;但未提供该参数”&#xff0c;这次继续出现了这样一个问题&#xff0c;但是出现的错误就不再过程函数中了&#xff0c;而…

牛腩新闻发布--过程或函数 'news_selectByCaId' 需要参数 '@caid',但未提供该参数(三)

发现问题 这篇博客是建立在“牛腩新闻发布–过程或函数 ‘news_selectByCaId’ 需要参数 ‘caid’&#xff0c;但未提供该参数&#xff08;二&#xff09;”&#xff0c;因为在那篇博客中说出了我当时遇到的“过程或函数 ‘news_selectByCaId’ 需要参数 ‘caid’&#xff0c;…

【牛腩】-'T_news_selectByCaId' 需要参数 '@caid',但未提供该参数。”

问题截图 解决方案&#xff1a; 改动存储过程 BEGINselect n.id,n.title,n.createTime,c.[name],n.caId from T_news ninner join T_category c on n.caIdc.id and n.caIdcaidorder by n.createTime desc END检查传参是或否正确如果以上都没有错误&#xff0c;那就看一下是否…

【重要补充】关于第三方潜在SDK导致的5.1.2Data use sharing

接上一篇《关于IDFA、CAID和「5. 1.2 - Data use & sharing」》后&#xff0c;我们发现&#xff0c;苹果在14.5出来前&#xff0c;对于IDFA替代方案之数据收集的审核打击力度越来越大。 因5.1.2条款被拒&#xff0c;目前可以确认的原因有以下两大&#xff1a; 一、如果你…

spring笔记⑬——spring事务

事务的四个特征 CAID是事务的四个特征&#xff0c;所有事务都必须满足以下特性。 原子性&#xff08;Atomicity&#xff09;&#xff1a;一个事务要么全部执行&#xff0c;要么不执行一致性(Consistency)&#xff1a;事务的运行并不改变数据库中数据的一致性隔离性&#xff0…

SQL查询语句(内联,as,in,通配符)

最近在学习牛腩新闻发布系统&#xff0c;正如牛老师所说&#xff0c;作为一个优秀的.NET开发人员&#xff0c;对SQL语句不熟怎么能行呢&#xff0c;接下来就总结下牛老师写的存储过程中SQL语句&#xff0c;挺经典&#xff0c;举一反三 首先先展示出来适用于系统的三张表 新闻类…