一、MPI 知识点

1.MPI是什么

MPI是一个跨平台的通信协议,用于编写并行计算机,支持点对点和广播。MPI是一个信息传递应用程序接口,包括协议和语义说明,他们指明其如何在各种实现中发挥其特性。MPI的目标是高性能,大规模性和可移植性。MPI在今天仍为高性能计算的主要模型。

2.MPI 基本框架函数
  • MPI_Init

    调用MPI_Init 是为了告知系统进行所必要的初始化设置。参数argc_p个argv_p是指向参数argc和argv的指针。当不需要的时候设置为NULL。
int MPI_Init(
int * argc_p /*in/out*/,
char ** argv_p /*in/out*/
);
  • MPI_Finalize

    调用MPI_Finalize是为了告知MPI系统MPI已经使用完毕。为MPI分配的任何资源都可以释放了。
  • MPI程序的基本框架
#include <mpi.h>
int main(int argc, char * argv[]) {
MPI_Init(&argc, &argv);
...
MPI_Finalize();
return 0;
}
3.MPI通信
  • 通信子

    通信子(communicator)指的是一组可以互相发送信息的进程集合。MPI_Init的其中一个目的,是在用户启动程序时,定义有用户启动的所有进程组成的通信子。称为MPI_COMM_WORLD。
int MPI_Comm_size(
MPI_Comm comm ,
int * comm_sz_p
);
int MPI_Comm_rank(
MPI_Comm comm,
int * my_rank_p
);

第一个参数是一个通信子,它所属的类型是MPI的通信子定义的特殊类型:MPI_Comm.MPI_Comm_size 函数在它的第二个参数返回通信子的进程数。MPI_Comm_rank函数在它的第二个参数返回正在调用进程的通信子中的进程号。

  • MPI_Send
int MPI_Send(
void * msg_buf_p,
int msg_size,
MPI_Datatype msg_type,
int dest,
int tag,
MPI_Comm commmunicator
);

第一个参数:msg_buf_p是一个指向消息内容的内存块的指针。

第二个参数:msg_size是指定了要发送的数据量。

第三个参数:msg_type 是指数据类型,MPI数据类型如下表所示。

MPI数据类型 c语言数据类型
MPI_CHAR signed char
MPI_SHORT signed short int
MPI_INT signed int
MPI_LONG signed long int
MPI_LONG_LONG signed long long int
MPI_UNSIGNED_CHAR unsigned short int
MPI_UNSIGNED unsigned int
MPI_UNSIGNED_LONG unsigned long int
MPI_FLOAT float
MPI_DOUBLE double
MPI_LONG_DOUBLE long double
MPI_BYTE
MPI_PACKED

第四个参数:dest指定了要接收消息的进程的进程号。

第五个参数:(MPI_ANY_TAG为-1)tag是个非负int型,用于区分看上去完全一样的消息。

最后一个参数:是一个通信子,用于指定通信范围。通信子指的是一组互相发送消息的进程的集合。一个通信子的进程所发送的消息不能被另一个通信子的进程所接收。

  • MPI_Recv
int MPI_Recv(
void * msg_buf_p,
int buf_size,
MPI_Datatype buf_type,
int source,
int tag,
MPI_Comm communicator,
MPI_Status* status_p
);

参数source用来指定了接收消息应该从哪个进程发送来的,参数tag要与发送消息的参数tag相匹配。参数communicator必须与发送进程所用的通信子匹配。

MPI类型MPI_Status是一个有至少三个成员的结构,MPI_SOURCE,MPI_TAG和MPI_ERROR。将&status作为最后一个参数传递给MPI_Recv函数并调用它后,可以通过检查以下两个成员来确定发送者和标签。

  • 消息匹配

    假定q号进程调用MPI_Send()函数
MPI_Send(send_buf_p, send_buf_sz, send_type, dest, send_tag, send_comm)

假定r号进程调用了MPI_Recv()函数

MPI_Recv(recv_buf_p, recv_buf_sz, recv_type, src, recv_tag, recv_comm,

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

(1)recv_comm = send_comm
(2)recv_tag = send_tag
(3)dest=r & src =q

在多数的情况下,满足下面的规则就可以了:

如果recv_type = send_type 同时recv_buf_sz ≥≥ send_buf_sz

那么由q号进程发送的消息就可以被r号进程成功的接收。

一个进程可以接收多个进程发送的消息,接收进程并不知道其他进程执行发送消息的顺序。MPI提供了一个特殊的常量MPI_ANY_SOURCE,就可以传递给MPI_Recv。

类似的,一个进程有可能接收多条来自另一个进程的有着不同的标签的消息,并且接收进程不知道消息发送的顺序。使用通配符(wildcard)参数时,需要注意的几点:

只要接收者可以调用通配符参数,发送者必须指定一个进程号与另一个非负整数标签。此外,MPI使用的是所谓的推(push)通信机制,而不是拉(pull)通信机制。
通信子参数没有通配符,发送者和接收者必须指定通信子。
  • MPI_Send和MPI_Recv

    发送进程可以缓冲消息,也可以阻塞。

    如果是缓冲消息,则MPI系统将会把消息放置在自己内部存储器里,并返回MPI_Send的调用。如果是系统发生阻塞,那么它将一直等待,知道可以开始发送消息,并不立即返回MPI_Send的调用。

    MPI_Send 的精确行为可以由MPI实现所决定的,但是,典型的实现方法有一个默认的消息截止大小,如果一条消息的大小小于截止大小,就会被缓冲,如果大于截止大小,就会被阻塞。

    MPI_Recv函数总是阻塞是,直到接收到一条匹配消息,当MPI_Recv函数调用返回时,就知道一条消息已经存储在接收缓冲区中,接收消息函数同样可以替代,系统检查是否有一条匹配的消息并返回。

    MPI要求消息是不可超越的(nonvertaking),如果q号进程发送两条消息给r号进程,那么q号发送的第一条消息必须在第二条消息之前可用,但是如果消息来自不同的进程的消息的到达顺序是没有限制的。
  • MPI_Sendrecv

    利用mpi求解微分方程时,经常会遇到不同进程的通讯,特别是如下形式的通讯:

    进程0->进程1->进程2->进程3...->进程n->进程0

这时,若单纯的利用MPI_Send, MPI_Recv函数进行通讯的话,容易造成死锁,下面介绍MPI_Sendrecv的来解决这个问题。顾名思义,MPI_Sendrecv表示的作用是将本进程的信息发送出去,并接收其他进程的信息,其调用方式如下:

MPI_Sendrecv(
void *sendbuf, //initial address of send buffer
int sendcount, //number of entries to send
MPI_Datatype sendtype, //type of entries in send buffer
int dest, //rank of destination
int sendtag, //send tag,99
void *recvbuf, //initial address of receive buffer
int recvcount, //max number of entries to receive
MPI_Datatype recvtype,
//type of entries in receive buffer (这里数目是按实数的数目,若数据类型为 MPI_COMPLEX时,传递的数目要乘以2)        
int source, //rank of source       
int recvtag, //receive tag       
MPI_Comm comm, //group communicator      
MPI_Status status //return status;
)

例子:

#include "mpi.h"
#include <stdio.h> int main(int argc, char *argv[])
{
int myid, num, left, right;
int sendBuffer[10], getBuffer[10];
MPI_Status status;
MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD, &num);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
right = (myid + 1) % num;
left = myid - 1;
if (left < 0)
left = numprocs - 1;
MPI_Sendrecv(sendBuffer, 10, MPI_INT, left, 123, getBuffer, 10, MPI_INT, right, 123, MPI_COMM_WORLD, &status);
printf("rank:%d,from %d to %d",myid,left,right);
MPI_Finalize();
return 0;
}
  • MPI_Sendrecv_replace

    发送和接收只使用一个buffer。
int MPI_Sendrecv_replace(
void *buf, int count, MPI_Datatype datatype,
int dest, int sendtag, int source, int recvtag,
MPI_Comm comm, MPI_Status *status)
  • MPI_Probe和MPI_Iprobe

    \(qquad\)MPI_Probe()和MPI_Probe()函数探测接收消息的内容,但不影响实际接收到的消息。我们可以根据探测到的消息内容决定如何接收这些消息,比如根据消息大小分配缓冲区等等。需要说明的是,这两个函数第一个是阻塞方式,即只有探测到匹配的消息才返回;第二个是非阻塞方式,即无论探测到与否都立即返回。
int MPI_Probe (int source /* in */,
int tag /* in */,
MPI_Comm comm /* in */,
MPI_Status* status /*out*/)

以上是阻塞型探测,直到有一个符合条件的消息到达,返回MPI_ANY_SOURCE和 MPI_ANY_TAG

int MPI_Iprobe (int source /* in */,
int tag /* in */,
MPI_Comm comm /* in */,
int * flag /*out*/,
MPI_Status* status /*out*/)

以上是非阻塞型探测,无论是否有一个符合条件的消息到达,立即返回。有flag=true;否则flag=false。

例子:

#include "mpi.h"
#include <stdio.h> #define MAX_BUF_SIZE_LG 22
#define NUM_MSGS_PER_BUF_SIZE 5
char buf[1 << MAX_BUF_SIZE_LG]; int main(int argc, char **argv)
{
int p_size;
int p_rank;
int msg_size_lg;
int errs = 0;
int mpi_errno;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &p_size);
MPI_Comm_rank(MPI_COMM_WORLD, &p_rank);
for (msg_size_lg = 0; msg_size_lg <= MAX_BUF_SIZE_LG; msg_size_lg++)
{
const int msg_size = 1 << msg_size_lg;
int msg_cnt;
printf( "testing messages of size %d\n", msg_size );fflush(stdout);
for (msg_cnt = 0; msg_cnt < NUM_MSGS_PER_BUF_SIZE; msg_cnt++)
{
MPI_Status status;
const int tag = msg_size_lg * NUM_MSGS_PER_BUF_SIZE + msg_cnt;
printf( "Message count %d\n", msg_cnt );fflush(stdout);
if (p_rank == 0)
{
int p;
for (p = 1; p < p_size; p ++)
{
/* Wait for synchronization message */
MPI_Recv(NULL, 0, MPI_BYTE, MPI_ANY_SOURCE, tag, MPI_COMM_WORLD, &status);
if (status.MPI_TAG != tag)
{
printf("ERROR: unexpected message tag from MPI_Recv(): lp=0, rp=%d, expected=%d, actual=%d, count=%d\n", status.MPI_SOURCE, status.MPI_TAG, tag, msg_cnt);fflush(stdout);
}
/* Send unexpected message which hopefully MPI_Probe() is already waiting for at the remote process */
MPI_Send (buf, msg_size, MPI_BYTE, status.MPI_SOURCE, status.MPI_TAG, MPI_COMM_WORLD);
}
}
else
{
int incoming_msg_size;
/* Send synchronization message */
MPI_Send(NULL, 0, MPI_BYTE, 0, tag, MPI_COMM_WORLD);
/* Perform probe, hopefully before the master process can send its reply */
MPI_Probe(MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
MPI_Get_count(&status, MPI_BYTE, &incoming_msg_size);
if (status.MPI_SOURCE != 0)
{
printf("ERROR: unexpected message source from MPI_Probe(): p=%d, expected=0, actual=%d, count=%d\n", p_rank, status.MPI_SOURCE, msg_cnt);fflush(stdout);
}
if (status.MPI_TAG != tag)
{
printf("ERROR: unexpected message tag from MPI_Probe(): p=%d, expected=%d, actual=%d, count=%d\n", p_rank, tag, status.MPI_TAG, msg_cnt);fflush(stdout);
}
if (incoming_msg_size != msg_size)
{
printf("ERROR: unexpected message size from MPI_Probe(): p=%d, expected=%d, actual=%d, count=%d\n", p_rank, msg_size, incoming_msg_size, msg_cnt);fflush(stdout);
} /* Receive the probed message from the master process */
MPI_Recv(buf, msg_size, MPI_BYTE, 0, tag, MPI_COMM_WORLD, &status);
MPI_Get_count(&status, MPI_BYTE, &incoming_msg_size);
if (status.MPI_SOURCE != 0)
{
printf("ERROR: unexpected message source from MPI_Recv(): p=%d, expected=0, actual=%d, count=%d\n", p_rank, status.MPI_SOURCE, msg_cnt);fflush(stdout);
}
if (status.MPI_TAG != tag)
{
printf("ERROR: unexpected message tag from MPI_Recv(): p=%d, expected=%d, actual=%d, count=%d\n", p_rank, tag, status.MPI_TAG, msg_cnt);fflush(stdout);
}
if (incoming_msg_size != msg_size)
{
printf("ERROR: unexpected message size from MPI_Recv(): p=%d, expected=%d, actual=%d, count=%d\n", p_rank, msg_size, incoming_msg_size, msg_cnt);fflush(stdout);
}
}
}
}
MPI_Finalize();
return 0;
}
  • MPI_Get_count

    根据 status 和 datatype,查询实际接受到了数据个数保存在 *count 中。

int MPI_Get_count(
MPI_Status *status,
MPI_Datatype datatype,
int *count
);
  • MPI_Isend和MPI_Irecv

    与MPI_Send与MPI_Recv不同,MPI_Isend与MPI_Irecv均为非阻塞式通信。

int MPI_Isend(
void *buf,
int count,
MPI_Datatype datatype,
int dest,
int tag,
MPI_Comm comm,
MPI_Request *request
);
int MPI_Irecv(
void *buf,
int count,
MPI_Datatype datatype,
int source,
int tag,
MPI_Comm comm,
MPI_Request *request
);

例子:

#include<stdio.h>
#include "mpi.h"
int main( int argc, char* argv[] ){
int rank, nproc;
int isbuf, irbuf, count;
MPI_Request request;
MPI_Status status;
int TAG = 100; MPI_Init( &argc, &argv );
MPI_Comm_size( MPI_COMM_WORLD, &nproc );
MPI_Comm_rank( MPI_COMM_WORLD, &rank ); if(rank == 0) {
isbuf = 9;
MPI_Isend( &isbuf, 1, MPI_INT, 1, TAG, MPI_COMM_WORLD, &request );
} else if(rank == 1) {
MPI_Irecv( &irbuf, 1, MPI_INT, 0, TAG, MPI_COMM_WORLD, &request);
MPI_Wait(&request, &status);
MPI_Get_count(&status, MPI_INT, &count);
printf( "irbuf = %d source = %d tag = %d count = %d\n",
irbuf, status.MPI_SOURCE, status.MPI_TAG, count);
}
MPI_Finalize();
return 0;
}
  • MPI_Wait,MPI_Waitany,MPI_Waitall和MPI_Waitsome
int MPI_Wait(
MPI_Request *request,
MPI_Status *status
);

​ 当程序运行到时MPI_Wait,会堵塞直到MPI_Wait传入的句柄参数对应的函数被完成。以非阻塞通信对象作为参数,一直等到相应的非阻塞通信完成后才成功返回,将相关信息放入status中,并释放这一非阻塞通信对象。

int MPI_Waitall(
int count,
MPI_Request array_of_requests[],
MPI_Status array_of_statuses[]
);

​ 所有通信操作完成之后才返回,否则将一直等待。当所有的非阻塞通信完成时才成功返回,第i个非阻塞通信对象对应的通信完成信息存放在array_of_statuses[i]中,并释放非阻塞完成对象数组。

int MPI_Waitany(
int count,
MPI_Request array_of_requests[],
int *index,
MPI_Status *status
);

​ 当所有请求句柄中至少有一个已经完成通信操作,就返回,如果有多于一个请求句柄已经完成,MPI_waitany将随机选择其中的一个并立即返回。当存在多个非阻塞通信对象时,我们用MPI_Request request[count] 来定义非阻塞完成对象数组。MPI_Wait用于等待非阻塞通信对象数组中的任意一个非阻塞通信对象的完成,一旦有一个非阻塞通信完成后,就返回该非阻塞通信所对应非阻塞通信对象在上述数组中的标签index,并释放该非阻塞通信对象。同时,把该通信的相关信息存放在status中返回。

int MPI_Waitsome(
int incount,
MPI_Request array_of_requests[],
int *outcount,
int array_of_indices[],
MPI_Status array_of_statuses[]
);

​ MPI_Waitsome与其余完成调用最大的不同之处在于增加了下标数组array_of_indices,当任意数目的非阻塞通信完成时,MPI_Wait便返回,完成非阻塞通信的数目记录在outcount中,相应的非阻塞通信对象的下标存放在下标数组中,对应通信的相关信息存放在array_of_statuses。

  • MPI_Test,MPI_Testany, MPI_Testsome,MPI_Testall和MPI_Test_cancelled
int MPI_Test(
MPI_Request *request,
int *flag,
MPI_Status *status
);

与MPI_Wait不同,MPI_Test在调用后会立刻返回,若相应非阻塞通信已完成,则完成标志flag=true。反之,完成标志flag=false。

int MPI_Testany(
int count,
MPI_Request array_of_requests[],
int *index,
int *flag,
MPI_Status *status
);

用于测试非阻塞通信数组中是否有任何一个对象已经完成,若有对象完成(若有多个,任取一个),令flag=true,并释放该对象。

int MPI_Testsome(
int incount,
MPI_Request array_of_requests[],
int *outcount,
int array_of_indices[],
MPI_Status array_of_statuses[]
);

立即返回,有几个非阻塞通信已经完成,就令outcount等于几,且将完成对象的下标记录在下标数组中。若没有非阻塞通信完成,则返回outcount=0。(并不立flag - -)

int MPI_Testall(
int count,
MPI_Request array_of_requests[],
int *flag,
MPI_Status array_of_statuses[]
);

当非阻塞通信数组中有任意一个非阻塞通信对象对应的非阻塞通信没有完成时,令flag=false并立即返回。当所有通信都已经完成时,令flag=true并返回。

int MPI_Test_cancelled(
MPI_Status *status,
int *flag
);

如果与状态对象关联的通信已成功取消,则返回flag = true。否则 返回flag = false。

  • MPI_Send_init和MPI_Recv_init
int MPI_Send_init(
void *buf,
int count,
MPI_Datatype datatype,
int dest,
int tag,
MPI_Comm comm,
MPI_Request *request
);
int MPI_Recv_init(
void *buf,
int count,
MPI_Datatype datatype,
int source,
int tag,
MPI_Comm comm,
MPI_Request *request
);

如果一个通信在一个并行计算的内部循环中不断地以同样的参数被执行。在这种情况下,该通信可以优化:把这些通信参数一次性捆绑到一个坚持式通信请求,然后不断用该请求初始化和完成消息。

  • MPI_Start和MPI_Startall
int MPI_Start(
MPI_Request *request
);

处于非活动状态的请求发出调用后,请求将变为活动状态。

int MPI_Startall(
int count,
MPI_Request array_of_requests[]
);

MPI_Startall的调用的效果与MPI_Start 以某种任意顺序为 i=0 ,..., count-1 执行的调用的效果相同。

  • MPI_Request_free
int MPI_Request_free(
MPI_Request *request
);

此例程通常用于释放使用MPI_Recv_init或MPI_Send_init创建的非活动持久请求。也可以释放活动请求。但是,一旦释放,请求不能再用于wait或test例程。

  • MPI_Cancel
int MPI_Cancel(
MPI_Request *request
);

MPI_Cancel操作允许取消挂起的通信。这是清理所必需的。发布发送或接收会占用用户资源(发送或接收缓冲区),可能需要取消以正常释放这些资源。MPI_Cancel可用于取消使用持久请求的通信,就像用于非持久请求一样。成功取消将取消活动通信,但不会取消请求本身。在呼叫MPI_Cancel以及随后对MPI_Wait或MPI_Test的呼叫后,请求将变为非活动状态,可以激活以进行新的通信。

MPI基础知识的更多相关文章

  1. [源码解析] 深度学习分布式训练框架 Horovod (1) --- 基础知识

    [源码解析] 深度学习分布式训练框架 Horovod --- (1) 基础知识 目录 [源码解析] 深度学习分布式训练框架 Horovod --- (1) 基础知识 0x00 摘要 0x01 分布式并 ...

  2. .NET面试题系列[1] - .NET框架基础知识(1)

    很明显,CLS是CTS的一个子集,而且是最小的子集. - 张子阳 .NET框架基础知识(1) 参考资料: http://www.tracefact.net/CLR-and-Framework/DotN ...

  3. RabbitMQ基础知识

    RabbitMQ基础知识 一.背景 RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue )的开源实现.AMQP 的出现其实也是应了广大人民群众的需求,虽然 ...

  4. Java基础知识(壹)

    写在前面的话 这篇博客,是很早之前自己的学习Java基础知识的,所记录的内容,仅仅是当时学习的一个总结随笔.现在分享出来,希望能帮助大家,如有不足的,希望大家支出. 后续会继续分享基础知识手记.希望能 ...

  5. selenium自动化基础知识

    什么是自动化测试? 自动化测试分为:功能自动化和性能自动化 功能自动化即使用计算机通过编码的方式来替代手工测试,完成一些重复性比较高的测试,解放测试人员的测试压力.同时,如果系统有不份模块更改后,只要 ...

  6. [SQL] SQL 基础知识梳理(一)- 数据库与 SQL

    SQL 基础知识梳理(一)- 数据库与 SQL [博主]反骨仔 [原文地址]http://www.cnblogs.com/liqingwen/p/5902856.html 目录 What's 数据库 ...

  7. [SQL] SQL 基础知识梳理(二) - 查询基础

    SQL 基础知识梳理(二) - 查询基础 [博主]反骨仔 [原文]http://www.cnblogs.com/liqingwen/p/5904824.html 序 这是<SQL 基础知识梳理( ...

  8. [SQL] SQL 基础知识梳理(三) - 聚合和排序

    SQL 基础知识梳理(三) - 聚合和排序 [博主]反骨仔 [原文]http://www.cnblogs.com/liqingwen/p/5926689.html 序 这是<SQL 基础知识梳理 ...

  9. [SQL] SQL 基础知识梳理(四) - 数据更新

    SQL 基础知识梳理(四) - 数据更新 [博主]反骨仔 [原文]http://www.cnblogs.com/liqingwen/p/5929786.html 序 这是<SQL 基础知识梳理( ...

随机推荐

  1. PHP stripos() 函数

    实例 查找 "php" 在字符串中第一次出现的位置: <?php高佣联盟 www.cgewang.comecho stripos("I love php, I lo ...

  2. PHP ord() 函数

    实例 返回 "h" 的 ASCII值: <?php高佣联盟 www.cgewang.comecho ord("h")."<br>&q ...

  3. darkbzoj #3759. Hungergame 博弈论 线性基 NIM

    LINK:Hungergame 放上一道简单题 复习一下. 考虑每次可以打开任意多个盒子 如果全打开了 那么就是一个NIM游戏了. 如果发现局面是异或为0的时候此时先手必胜了. 考虑局面不全体异或为0 ...

  4. 2019 HL SC day4

    自闭场本来 以为 顶多一些不太会 结果发现 一堆不太会 . 树状数组  感觉 好久没看 了有点遗忘 不过还好 现在我来了.莅临之神将会消灭一切知识点哦. 今天说点不一样东西 树状数组 hh 很有用的东 ...

  5. Spring Joinpoint

    如果用maven管理 则需要 <artifactId> aopalliance </artifactId> <artifactId> spring-aspects ...

  6. LeetCode 164. Maximum Gap[翻译]

    164. Maximum Gap 164. 最大间隔 Given an unsorted array, find the maximum difference between the successi ...

  7. Gradient Centralization: 简单的梯度中心化,一行代码加速训练并提升泛化能力 | ECCV 2020 Oral

    梯度中心化GC对权值梯度进行零均值化,能够使得网络的训练更加稳定,并且能提高网络的泛化能力,算法思路简单,论文的理论分析十分充分,能够很好地解释GC的作用原理   来源:晓飞的算法工程笔记 公众号 论 ...

  8. 解决 IntelliJ IDEA占用C盘过大空间问题

    原文地址:https://blog.csdn.net/weixin_44449518/article/details/103334235 问题描述: 在保证其他软件缓存不影响C盘可用空间的基础上,当我 ...

  9. Python爬取招聘网站数据,给学习、求职一点参考

    1.项目背景 随着科技的飞速发展,数据呈现爆发式的增长,任何人都摆脱不了与数据打交道,社会对于“数据”方面的人才需求也在不断增大.因此了解当下企业究竟需要招聘什么样的人才?需要什么样的技能?不管是对于 ...

  10. 17、Observer 观察者模式

    以一个实例给大家引入观察者,大家多多少少都写过html或者java中的swing.我们定义一个按钮,给他增加一个点击事件,那么这个方法是怎么被触发到呢,对了,就是利用了观察者设计模式 观察者模式 当对 ...