Loading data async with queue. Proper write only locking and scheduling
Instead of keeping track of what's being loaded, you can represent that as a state by wrapping IDataSet into a class:

private sealed class DataSetResult
{
    public volatile IDataSet Result;
}


When an instance of this class is created, start loading the data. You can then just return the value of the Result field - if it is null, the data is still being loaded:

private readonly ConcurrentDictionary&lt;string, Lazy&lt;DataSetResult&gt;&gt; _data;

public IDataSet GetData(string dataId)
{
    var dataSet = _data.GetOrAdd(
        dataId, 
        _ =&gt; new Lazy&lt;DataSetResult&gt;(
            () =&gt;
            {
                var result = new DataSetResult();
                dataIoAsync.LoadDataAsync(dataParams);
                return result;
            }
        )
    );

    return dataSet.Value.Result;
}

private void DataIoAsync_DataFileLoaded(object sender, DataFileLoadedAsyncEventArgs e)
{
    _data[e.DataId].Value.Result = e.DataSet;
    OnDataFileLoaded(e.DataId);
}


The Lazy&lt;DataSetResult&gt; above ensures that data is retrieved at most once for a given id.

Personally, however, I would prefer to return Task&lt;IDataSet&gt; from this method, as it is really up to the caller to decide whether they want to wait for the result to become available or not. Using Task also allows you to report any exceptions to the client:

private readonly ConcurrentDictionary&lt;string, Lazy&lt;TaskCompletionSource&lt;IDataSet&gt;&gt;&gt; _data;

public Task&lt;IDataSet&gt; GetDataAsync(string dataId)
{
    var dataSet = _data.GetOrAdd(
        dataId, 
        _ =&gt; new Lazy&lt;TaskCompletionSource&lt;IDataSet&gt;&gt;(
            () =&gt;
            {
                var result = new TaskCompletionSource&lt;IDataSet&gt;();
                dataIoAsync.LoadDataAsync(dataParams);
                return result;
            }
        )
    );

    return dataSet.Value.Task;
}

private void DataIoAsync_DataFileLoaded(object sender, DataFileLoadedAsyncEventArgs e)
{
    _data[e.DataId].Value.TrySetResult(e.DataSet);
    OnDataFileLoaded(e.DataId);
}