The statement SELECT ... FOR UPDATE works for you exactly as it says. It
*selects* and locks it *for update*. Because the row doesn't exist,
SELECT actually selects nothing. So it locks nothing for update.
When the row exists, the first transaction returns a row and the second
waits on SELECT.
Gap locks in InnoDB are “purely inhibitive”, which means they only stop other transactions from inserting to the gap. They do not prevent different transactions from taking gap locks on the same gap. Thus, a gap X-lock has the same effect as a gap S-lock.
Your goal is to "check if a row exists with a particular value
and if not, generate the data for that row and insert it". Normally
uniqueness checks are done inside a database. Make your KEY (col) a
UNIQUE key, then when you insert the database will check whether a row
exists and will insert the value if not.
If you insist on doing it in the application, you can try user-level
locks, GET_LOCK/RELEASE_LOCK functions. Like
SELECT GET_LOCK(4, 10);
-- If the result is 1
SELECT * FROM test WHERE col = 4;
-- If no results, generate data and insert
INSERT INTO test SET col = 4, data = 'generated data goes here';
SELECT RELEASE_LOCK(4);