Is this table-backed-queue implementation safe?
Check out the locking that occurs in your stored procedure - the following is a simple example, you can adapt it to test your query and others.  In case it needs to be clear, go to a quiet server for this, like a development enivironment or better yet, a local install.  The last thing you want to be doing is holding transactions open for giggles in a production environment.

USE tempdb;
GO

IF ( OBJECT_ID( 'dbo.TestTable', 'U' ) IS NULL )
BEGIN
    --DROP TABLE dbo.TestTable;
    CREATE TABLE dbo.TestTable
    (
        Foo                 INTEGER,
        BAR                 INTEGER
    );

    INSERT  INTO dbo.TestTable( Foo, Bar )
    SELECT  TOP 10 1, 1
    FROM    sys.objects;
END;
GO

IF ( OBJECT_ID( 'dbo.TestProcess', 'P' ) IS NULL )
BEGIN
    --DROP PROCEDURE dbo.TestProcess;
    EXEC( 'CREATE PROCEDURE dbo.TestProcess AS SET NOCOUNT OFF;' );
END;
GO

ALTER PROCEDURE dbo.TestProcess
AS BEGIN
    SET NOCOUNT ON;

    --SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    BEGIN TRANSACTION;

    SELECT  isolation_level = CASE transaction_isolation_level
                WHEN 0 THEN 'Unspecified'
                WHEN 1 THEN 'Read Uncommitted'
                WHEN 2 THEN 'Read Committed'
                WHEN 3 THEN 'Repeatable'
                WHEN 4 THEN 'Serializable'
                WHEN 5 THEN 'Snapshot'
                ELSE 'UNKNOWN ( ' + CONVERT( NVARCHAR( 8 ), 
                    transaction_isolation_level ) + ' )'
            END
    FROM    sys.dm_exec_requests
    WHERE   session_id = @@SPID;

    SELECT  ObjectName = OBJECT_NAME( resource_associated_entity_id ), 
            request_session_id, resource_type, 
            request_mode, request_type, request_status
    FROM    sys.dm_tran_locks
    WHERE   request_session_id = @@SPID
        AND resource_type = 'OBJECT';

    UPDATE  dbo.TestTable
        SET Foo = 1
    OUTPUT  INSERTED.Foo, INSERTED.Bar
    WHERE   Bar = 1;

    SELECT  ObjectName = OBJECT_NAME( resource_associated_entity_id ), 
            request_session_id, resource_type, 
            request_mode, request_type, request_status
    FROM    sys.dm_tran_locks
    WHERE   request_session_id = @@SPID
        AND resource_type = 'OBJECT';

    IF ( @@ERROR &lt;&gt; 0 )
    BEGIN
        ROLLBACK TRANSACTION;
        RAISERROR( 'Rolled Back Update', 16, 1 ) WITH NOWAIT;
    END ELSE BEGIN
        COMMIT TRANSACTION;
    END;

    SELECT  ObjectName = OBJECT_NAME( resource_associated_entity_id ), 
            request_session_id, resource_type, 
            request_mode, request_type, request_status
    FROM    sys.dm_tran_locks
    WHERE   request_session_id = @@SPID
        AND resource_type = 'OBJECT';

    SET NOCOUNT OFF;

    SELECT  isolation_level = CASE transaction_isolation_level
                WHEN 0 THEN 'Unspecified'
                WHEN 1 THEN 'Read Uncommitted'
                WHEN 2 THEN 'Read Committed'
                WHEN 3 THEN 'Repeatable'
                WHEN 4 THEN 'Serializable'
                WHEN 5 THEN 'Snapshot'
                ELSE 'UNKNOWN ( ' + CONVERT( NVARCHAR( 8 ), 
                    transaction_isolation_level ) + ' )'
            END
    FROM    sys.dm_exec_requests
    WHERE   session_id = @@SPID;
END;
GO


With that set up, open a new session, which opens a transaction and runs the "guts" of the procedure:

USE tempdb;
GO

BEGIN TRANSACTION;

UPDATE  dbo.TestTable
    SET Foo = 1
WHERE   Bar = 1;

--COMMIT TRANSACTION;


Open another session and do the same thing, closing the transaction this time:

USE tempdb;
GO

BEGIN TRANSACTION;

UPDATE  dbo.TestTable
    SET Foo = 1
WHERE   Bar = 1;

COMMIT TRANSACTION;


This example will hang, as update locks are being held on the test table.  In yet another session, run the following:

USE tempdb;
GO

SELECT  ObjectName = OBJECT_NAME( resource_associated_entity_id ), 
        request_session_id, resource_type, request_mode, 
        request_type, request_status
FROM    sys.dm_tran_locks
WHERE   resource_database_id = DB_ID()
    AND resource_type = 'OBJECT';

SELECT  session_id, wait_type, resource_description
FROM    sys.dm_os_waiting_tasks
WHERE   session_id IN ( SELECT  request_session_id  
                        FROM    sys.dm_tran_locks
                        WHERE   resource_database_id = DB_ID()
                            AND resource_type = 'OBJECT' );


Here you will see that when the "guts" are run, transaction locks are granted to both the uncommitted session and the hanging session, but the hanging session is waiting for the modify update lock to be granted ( LCK_M_U ) while it waits for the original session to finish.  Go back to the first session and commit the transaction, then go ahead and run the procedure.

USE tempdb;
GO

EXECUTE dbo.TestProcess;


You should see that same intent-exclusive ( IX ) lock having been granted while the procedure runs.  This will lead to the same scenario as outlined above, where further transactions against the affected objects will "hang" while the active transactions complete / the procedure runs.

This is all using the default SET TRANSACTION ISOLATION LEVEL READ COMMITTED, well, transaction isolation level.  With the TOPs and CROSS APPLYs, it's potentially possible that a READ UNCOMMITTED or even a READ COMMITTED isolation level would allow concurrent transactions to effectively SELECT the keys about to be operated on before granting the LCK_M_Us, but in a single statement such as in your procedure, I would be somewhat surprised if that were the case.  If so, then a SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; may be the best option to ensure asynchronous access to the objects.