I use a mutex in the driver, that takes the mutex as part of the initial processing of the I2C transaction, and then the driver doesn’t return to the task until the ISR signals the operation is done, and then the driver can give the mutex back.
If you want to use an asynchronous operation where the driver copies the data to an internal buffer to let the task continue, then you can have the driver take the mutex, and if the driver is busy, wait for it to finish, and then queue up the next operations, start it, and then release the mutex.
So, for either method there is a mutex that lets only one task at a time use the driver, and a semaphore (or the like) to signal from the ISR level part of the driver to the task level of the driver that the operation was completed.