## Asynchronous Execution
## Learning Objectives
* Learn about how commands are enqueued asynchronously
* Learn about the different reasons for synchronization
* Learn about the different ways to perform synchronization
#### Asynchronous execution
* All command submitted to a `queue` are done so asynchronously.
* The functions return immediately and the command is run in a background thread.
* This includes individual commands like `memcpy` and collections of commands derived from a command group.
* This means you have to synchronize with those commands.
#### Synchronization
There are a number of reasons why you need to synchronize with commands
* Await completion of a kernel function.
* Await the results of a computation.
* Await error conditions which come from a failure to execute any of the commands.
#### Synchronization with kernel functions
There are two ways ways to synchronize with kernel functions.
* Calling `wait` on an `event` object returned from enqueuing a kernel function command, either via a command group or a shortcut function.
* Calling `wait` or `wait_and_throw` on the `queue` itself.
#### Synchronizing with kernel functions
#include<sycl/sycl.hpp>
#include<vector>
using namespace sycl;
int main(){
std::vector<int> hostA = { 1, 5, 66, 14, 55, 11, 12 };
queue gpuQueue(gpu_selector_v);
int* devA = malloc_device<int>(hostA.size(), gpuQueue);
auto e = gpuQueue.memcpy(devA, hostA.data(), hostA.size());
e.wait();
gpuQueue.parallel_for(hostA.size(), [=](id<1> idx){
devA[idx]++;
}).wait();
}
* Calling `wait` on an `event` object returned from enqueuing a command group will wait for the commands from that command group to complete.
* This is how we have synchronized in our examples so far.
* This effectively creates a blocking operations that will complete in place by immediately synchronizing.
#### Synchronizing with kernel functions
#include<sycl/sycl.hpp>
#include<vector>
using namespace sycl;
int main(){
std::vector<int> hostA = { 1, 5, 66, 14, 55, 11, 12 };
queue gpuQueue(gpu_selector_v);
int* devA = malloc_device<int>(hostA.size(), gpuQueue));
gpuQueue.memcpy(devA, hostA.data(), hostA.size());
gpuQueue.wait();
gpuQueue.parallel_for(hostA.size(), [=](id<1> idx){
devA[idx]++;
});
gpuQueue.wait();
}
* Again calling `wait` or `wait_and_throw` on a `queue` will wait for all commands enqueued to it to complete.
* Note you generally don't want to call `wait` on the `queue` after every command, instead you want to create dependencies between commands, which we cover in the next lecture.
#### Synchronizing with data
There are multiple ways ways to synchronize with data, but it differs depending on the data management model you are using.
* you can synchronize the same way you would for kernel functions, calling `wait` on an `event` or the `queue`.
#### Synchronizing with data
gpuQueue.memcpy(data, devicePtr, sizeof(int)).wait();
gpuQueue.memcpy(data, devicePtr, sizeof(int));
gpuQueue.wait();
* Simply call `wait` on the `event` returned from `memcpy`.
* Alternatively call `wait` on the `queue`.
#### Synchronizing with errors
* Errors are handled by a `queue` and any asynchronous errors can be produced during any of the synchronization methods we've looked at.
* The best way to ensure all errors are caught is to synchronize by calling `wait` or `wait_and_throw` on the the `queue`.
#### Exercise
Code_Exercises/Asynchronous_Execution/source
Try out the different methods of synchronizing with a kernel function and the resulting data from the computation.