You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用C# Parallel.ForEach+Renci SSH.NET下载SFTP文件出现0KB问题

解决Renci SSH.NET并行下载文件时出现0KB的问题

兄弟,你踩了SSH.NET一个很常见的坑——SftpClient实例是非线程安全的!你用Parallel.ForEach让多个线程同时操作同一个客户端对象,会导致内部的网络流、操作状态互相冲突,最终文件根本没被正确写入,所以大小都是0KB。

问题根源

Renci SSH.NET的SftpClient设计上不支持多线程并发调用它的方法(比如DownloadFile)。当多个线程同时操作同一个客户端时,会出现数据流混乱、操作被中断的情况,导致本地文件只创建了空文件,没有写入任何内容。

解决方案:每个并行任务使用独立的SftpClient实例

最直接有效的办法是,在并行循环的每个任务里创建一个独立的SftpClient连接,这样每个线程的操作完全隔离,不会互相干扰。

示例代码

先看你可能的错误写法(共用一个客户端):

// 错误写法:多个线程共用同一个SftpClient
var client = new SftpClient("your-host", "username", "password");
client.Connect();

var remoteFiles = client.ListDirectory("/remote-folder").ToList();

Parallel.ForEach(remoteFiles, file =>
{
    var localFilePath = Path.Combine("local-folder", file.Name);
    using (var fs = File.Create(localFilePath))
    {
        client.DownloadFile(file.FullName, fs); // 多线程同时调用同一个client,导致冲突
    }
});

client.Disconnect();

修改后的正确写法:

// 先获取远程文件列表(避免在并行循环里共享客户端)
List<SftpFile> remoteFiles;
using (var tempClient = new SftpClient("your-host", "username", "password"))
{
    tempClient.Connect();
    remoteFiles = tempClient.ListDirectory("/remote-folder").ToList();
    tempClient.Disconnect();
}

// 并行下载,每个任务创建独立的客户端
Parallel.ForEach(remoteFiles, new ParallelOptions { MaxDegreeOfParallelism = 4 }, file =>
{
    var localFilePath = Path.Combine("local-folder", file.Name);
    
    // 每个线程创建自己的SftpClient实例
    using (var client = new SftpClient("your-host", "username", "password"))
    {
        try
        {
            client.Connect();
            using (var fs = File.Create(localFilePath))
            {
                client.DownloadFile(file.FullName, fs);
            }
        }
        catch (Exception ex)
        {
            // 处理下载异常,比如记录日志、标记失败文件
            Console.WriteLine($"下载文件 {file.Name} 失败:{ex.Message}");
        }
        finally
        {
            if (client.IsConnected)
            {
                client.Disconnect();
            }
        }
    }
});

关键细节说明

  • 控制并发数:通过ParallelOptions.MaxDegreeOfParallelism限制同时运行的任务数(比如设为4、8),避免短时间内创建大量SSH连接导致服务器拒绝或性能下降,具体数值可以根据服务器的连接限制和你的网络情况调整。
  • 提前获取文件列表:先在单线程里用一个临时客户端获取所有要下载的文件列表,避免在并行循环里共享客户端对象。
  • 异常处理:并行循环里的异常不会直接中断所有任务,所以要在每个任务内部添加try-catch,处理单个文件的下载失败情况,避免影响其他文件。

进阶优化:使用连接池(可选)

如果要下载的文件数量非常多,频繁创建和销毁SSH连接会有一定性能开销。这时可以用对象池来复用SftpClient实例,减少连接建立的时间。

比如用Microsoft.Extensions.ObjectPool(需要安装对应NuGet包)来实现:

// 初始化连接池
var pool = new DefaultObjectPool<SftpClient>(new PooledPolicy());

// 并行下载时从池里获取客户端
Parallel.ForEach(remoteFiles, file =>
{
    var client = pool.Get();
    try
    {
        if (!client.IsConnected)
        {
            client.Connect();
        }
        // 执行下载操作...
        var localFilePath = Path.Combine("local-folder", file.Name);
        using (var fs = File.Create(localFilePath))
        {
            client.DownloadFile(file.FullName, fs);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"下载文件 {file.Name} 失败:{ex.Message}");
    }
    finally
    {
        // 将客户端放回池里
        pool.Return(client);
    }
});

// 连接池策略类
public class PooledPolicy : IPooledPolicy<SftpClient>
{
    public SftpClient Create()
    {
        return new SftpClient("your-host", "username", "password");
    }

    public bool Return(SftpClient client)
    {
        if (client.IsConnected)
        {
            try
            {
                client.Disconnect();
            }
            catch
            {
                // 忽略断开连接的异常
            }
        }
        return true;
    }
}

这个方案适合超大量文件的场景,普通几千个文件的话,直接每个任务创建独立客户端已经足够好用了。

内容的提问来源于stack exchange,提问作者user1591668

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 08:41:12