OpenCL 主机提醒事件、命令同步事件
1.在OpenCL中,事件是一个和事件发生(occurrence)相对应的数据结构。一次事件监视的可能是数据传输操作的结束,也可能监视的是内核的执行。可以通过三种主要的方式来使用事件:
主机提醒(host notification)--用来提醒主机,设备已经执行完一条命令的事件;
命令同步--用来强迫延迟执行,直到另一个事件发生的事件;
性能分析--用来监视执行一条命令所耗时长的事件。
性能分析是高性能应用程序开发过程中所要用到的十分重要的工具,它可以评估计算硬件和编程方法的性能,有了性能分析,就可以在不同的设备、内核以及数据划分策略间进行比较。
一般而言,当为内核生成相对应的工作项之后,他们之间的执行将是无序而且不稳定的。当然,如果工作项访问的内存空间互不相关,程序的运行也就并无大碍,但如果各个工作项需要对同一块数据做处理,事情就麻烦了。OpenCL提供了各种函数来对工作项的处理进行排序。
下面这个例子是展示主机提醒事件的工作过程。假设,需要带调用函数clEnqueueReadBuffer,将大量的数据从设备传输到主机上。整个过程可能会非常耗时,所以,你可以通过将函数的第三个参数设置为CL_FALSE,来将函数定义为非阻塞型。这样,函数clEnqueueReadBuffer便会立即返回,而主机则能在数据传输的过程中,进行其他任务的处理。
当数据传输结束后,可能还会需要主机应用程序来处理这些数据。这时,就需要声明cl_event结构,并进行两项相关性配置。其一是配置事件和数据传输命令的相关性;其二是配置事件和主机条用函数的相关性,这个函数将在传输命令执行完之后,被主机调用,因而这种函数也被称为回调函数。
将事件和命令关联:很多函数都是将命令发送到命令队列中,他们的名字开头一般都形如clEnqueue,而且最后一个参数都是指向cl_event结构的指针。
将事件和回调函数做关联
主机提醒的例子:代码创建、配置两个回调事件,Kernel_event和read_event,第一个事件和内核执行命令相关,第二条命令和主机读取数据的命令相关。这两个事件同时也和函数kernel_complete,函数read_complete分别相关。
2.命令同步事件
命令队列处理命令的默认顺序就是他们入列的顺序。但如果命令被发送到同一上下文的不同命令队列中时,就很难区分他们各自之间的执行顺序。但在事件的帮助之下,设置命令的执行顺序又变成了可能。只需要强迫命令延时,等待相应的事件集执行完成。这些延时事件集也被称为等待列表。如果事件和命令执行相关联,这个事件便被称为命令事件。如果事件和主机应用程序中的事件发生相关联,这个事件就被称为用户事件。
2.1等待命令和命令事件
cl_uint num_events--命令的等待列表中cl_event对象的数量;
const cl_event*wait_list--指向等待列表中cl_event对象的指针;
2.2等待列表和用户事件
命令事件和用户事件也有个明显的区别,命令事件对应的是设备上所执行的命令,而用户事件是由主机应用程序所生成的。有了用户事件,你可以通过主机,而非设备,来暂缓命令的执行。
函数clCreateUserEvent的目的就是创建用户事件:
cl_event clCreateUserEvent(cl_context context, cl_int *err)
函数返回的是cl_event对象,可以被放于任何一条命令的等待列表之中。并不特别指定某个命令队列,用户事件可以用到多个设备上。
如果想将用户事件加到命令的等待列表中,命令的执行将会延迟到主机应用程序完成对事件的更新之后。整个过程需要调用函数clSetUserEventStatus,其函数签名:
cl_int clSetUserEventStatus(cl_event event,cl_int status)
参数status的取值范围只能是CL_COMPLETE(其值为0)或一个负值。如果status设为CL_COMPLETE,所有等待这个用户事件的命令都将允许执行。如果status设为一个负值,所有等待这个用户事件的命令都将被终止执行。
主机应用程序将两条命令发送到设备上:一个用来执行内核,一个用来读取内核的输出数据。相互之间的关系是:读命令在内核执行命令完成之后开始执行,而内核执行命令又由用户事件来触发执行。因此,一旦用户按键,用户事件被触发,那么两条命令都会相继被执行。当读命令完成之后,他会调用回调函数read_complete.
clEnqueueReadBuffer函数被称为非阻塞型(CL_FALSE),如果阻塞参数变为CL_TRUE,函数将会等到读操作运行完后,才算完成。但是读命令暂缓的是内核执行命令,而它要等到用户事件完成之后才能执行完。如果clEnqueueReadBuffer处于等待状态,这个函数后面的代码京不会得到运行,整个应用程序也将处于暂停状态。
2.3额外的命令同步函数
通过发送三种新的类型的命令,来进行这种同步化操作,这三种新命令就是标记命令、等待命令以及障碍命令。
标记命令:
clEnqueueMarker入列一条名为标记命令的命令。将事件和之前的所有命令的执行关联起来。函数的签名:
cl_int clEnqueueMarker(cl_command_queue command_queue,cl_event *event)
这个函数被用来关联event和标记命令,event可以被放在等待列表中,来提醒主机。
等待命令:
由clEnqueueWaitForEvents函数所生成的命令也被称为等待命令,他会暂停命令队列的执行,直到它的等待列表中的命令全部执行完成。其函数签名如下:
cl_int clEnqueueWaitForEvents(cl_command_queue queue,cl_uint num_events,const cl_event *wait_list)
最后两个参数用来创建一个等待列表,这个列表和之前介绍的入列函数的等待列表很相似。但与后者只暂缓一条命令的执行不同,这个列表中的事件会暂缓列表中每条命令的执行。等待命令告诉命令队列不要在等待列表中的事件完成之前,处理复制命令和写命令。
这里,clEnqueueWaitForEvents函数发送的命令可以暂缓队列中命令的执行。在调用clSetUserEventStatus函数,设定用户为完成状态,后面的命令就可以被正常的执行。
障碍命令:
clEnqueueBarrier函数并不能接受或配置事件,但函数的目的却和clEnqueueWaieForEvents函数差不多,因此也可以放到这里来讨论。两个函数都是暂缓队列中命令的执行,但clEnqueueBarrier使用的却不是等待列表,他是通过入列一个名为障碍命令(barrier command)的命令来实现的。这条命令会在前面的命令执行结束前,一直阻止后面的命令的执行,函数签名:
cl_int clEnqueueBarrier(cl_command_queue queue)
障碍命令适用于,两条命令集合之间有执行上的先后关系。有了障碍命令,就可以回避对事件和回调函数的使用。障碍命令会自动地调整命令之间的先后执行顺序。
2.4获取和事件关联的数据
回调函数可以访问函数的data参数,而非函数的event参数。但在调用clGetEventInfo函数之前,返回大量和事件有关的信息,例如事件的上下文,命令队列以及和事件关联的命令的类型。函数签名:
cl_int clGetEventInfo(cl_event event, cl_event_info param_name,
size_t param_value_size,void*param_value, size_t*param_size)
大多数的取值都很简单,但CL_EVENT_COMMAND_EXECUTION_STATUS和CL_EVENT_COMMAND_TYPE这两个取值却值得提一下,到目前为止,我们所碰到过的表示名利给状态的常数都是CL_COMPLETE,它表示的是命令执行完之后的状态。但是一条命令的完整生命周期共包括以下四个阶段:
入列--命令被入列到命令队列中,用CL_QUEUED来指明;
提交--命令被提交给设备,用CL_SUBMITTED来指明;
运行--命令正在设备上执行,用CL_RUNNING来指明;
完成--命令完成执行,用CL_COMPLETE来指明。
只能调用clSetEventCallback和clSetUserEventStatus这两个函数来访问CL_COMPLETE状态。
如果信息参数被设为CL_EVENT_COMMAND_TYPE,返回值表示的是和事件相关的命令类型。返回结果是一个枚举型cl_command_type结构。
CL_COMMAND_READ_BUFFER类型对应的是clEnqueueReadBuffer函数入列的命令。CL_COMMAND_USER,没有相对应的入列命令的函数,它表明cl_event对象是由clCreateUserEvent函数所创建的用户事件。
推荐阅读