I want to upload potentially large batches (possibly 100s) of files to FTP, using the SSH.NET library and the Renci.SshNet.Async extensions.  I need to limit the number of concurrent uploads to five, or whatever number I discover the FTP can handle.
This is my code before any limiting:
using (var sftp = new SftpClient(sftpHost, 22, sftpUser, sftpPass))
{
    var tasks = new List<Task>();
    try
    {
        sftp.Connect();
        foreach (var file in Directory.EnumerateFiles(localPath, "*.xml"))
        {
            tasks.Add(
                sftp.UploadAsync(
                    File.OpenRead(file),      // Stream input
                    Path.GetFileName(file),   // string path
                    true));                   // bool canOverride
        }
        await Task.WhenAll(tasks);
        sftp.Disconnect();
    }
    // trimmed catch
}
I've read about SemaphoreSlim, but I don't fully understand how it works and how it is used with TAP.  This is, based on the MSDN documentation, how I would implement it.
I'm unsure if using Task.Run is the correct way to go about this, as it's I/O bound, and from what I know, Task.Run is for CPU-bound work and async/await for I/O-bound work.  I also don't understand how these tasks enter (is that the correct terminology) the semaphore, as all they do is call .Release() on it.
using (var sftp = new SftpClient(sftpHost, 22, sftpUser, sftpPass))
{
    var tasks = new List<Task>();
    var semaphore = new SemaphoreSlim(5);
    try
    {
        sftp.Connect();
        foreach (var file in Directory.EnumerateFiles(localPath, "*.xml"))
        {
            tasks.Add(
                Task.Run(() =>
                {
                    sftp.UploadAsync(
                        File.OpenRead(file),      // Stream input
                        Path.GetFileName(file),   // string path
                        true));                   // bool canOverride
                    semaphore.Release();
                });
        }
        await Task.WhenAll(tasks);
        sftp.Disconnect();
    }
    // trimmed catch
}
 
    