使用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
相关产品推荐
相关产品推荐

