WinForms执行同步操作时如何避免阻塞UI线程?
我正在为高校教师开发一款简易WinForms应用,教师可通过网络向30-35台学生机的网络共享发送文件/文件夹。目前应用功能可用,但复制包含25个子文件夹、311个文件(总计7.22 MB)的文件夹时,UI会出现冻结,拖拽窗口会进入“未响应”状态,完成操作后UI恢复响应。
我尝试了异步封装同步操作:为所有30台电脑创建List<Task>,并通过await Task.WhenAll(tasks)等待完成。
目录复制代码
private async Task CopyDirectoryAsync(string sourceDirectory, string destinationDirectory, string[] directories, string[] files, string host, bool isCollectOperation) { try { if (isCollectOperation) { directories = Directory.GetDirectories(sourceDirectory, "*", _defaultEnumerationOptions); files = Directory.GetFiles(sourceDirectory, "*.*", _defaultEnumerationOptions); } await CreateDirectoryAsync(destinationDirectory); for (var i = 0; i < directories.Length; i++) { await CreateDirectoryAsync(@$"{destinationDirectory}{directories[i].Replace(sourceDirectory, "")}"); } for (var i = 0; i < files.Length; i++) { await CopyFileAsync(files[i], $@"{files[i].Replace(sourceDirectory, destinationDirectory)}", null, false); } SetHostLabelColor(host, Color.Green); } catch (Exception ex) { SetHostLabelColor(host, Color.Red); await Log(ex); } }
文件复制代码
private async Task CopyFileAsync(string sourceFile, string destinationFile, string host, bool isFileCopy) { try { await using var sourceStream = new FileStream( sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read, 8192, FileOptions.Asynchronous | FileOptions.SequentialScan); await using var destinationStream = new FileStream( destinationFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 8192, FileOptions.Asynchronous | FileOptions.SequentialScan); await sourceStream.CopyToAsync(destinationStream); if (isFileCopy) { SetHostLabelColor(host, Color.Green); } } catch (Exception ex) { if (!isFileCopy) throw; SetHostLabelColor(host, Color.Red); await Log(ex); } }
封装的目录创建方法
private static async Task CreateDirectoryAsync(string path) { await Task.Run(() => { Directory.CreateDirectory(path); }); }
我的推测是:尽管用Task.Run()将Directory.CreateDirectory()封装成异步方法,但它本质是严格同步的,仍会阻塞WinForms应用的消息队列,Directory.GetDirectories()和Directory.GetFiles()也存在同样问题。
问题
在WinForms应用中,是否有办法卸载同步操作以避免阻塞UI线程,且最好不使用Control.BeginInvoke()?
我理解复制目录操作耗时久可能是SMB/NTFS/WIN32等的限制,但疑惑为何UI线程会被阻塞。
更新:调试发现的阻塞点
在调试器中点击“全部中断”时,断点几乎总是停在以下代码处:
[LibraryImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "7.0.8.42427")] private unsafe static SafeFileHandle CreateFilePrivate(string lpFileName, int dwDesiredAccess, FileShare dwShareMode, SECURITY_ATTRIBUTES* lpSecurityAttributes, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile) { bool flag = false; nint handle = 0; SafeFileHandle safeFileHandle = new SafeFileHandle(); int lastSystemError; try { try { fixed (char* ptr = &Utf16StringMarshaller.GetPinnableReference(lpFileName)) { void* lpFileName2 = ptr; Marshal.SetLastSystemError(0); /* Always here --> */ handle = __PInvoke((ushort*)lpFileName2, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); lastSystemError = Marshal.GetLastSystemError(); } } finally { } flag = true; } finally { if (flag) { Marshal.InitHandle(safeFileHandle, handle); } } Marshal.SetLastPInvokeError(lastSystemError); return safeFileHandle; [DllImport("kernel32.dll", EntryPoint = "CreateFileW", ExactSpelling = true)] static extern unsafe IntPtr __PInvoke(ushort* lpFileName, int dwDesiredAccess, FileShare dwShareMode, SECURITY_ATTRIBUTES* lpSecurityAttributes, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile); }
解决方案建议
将所有同步IO操作移至后台线程
目前仅封装了Directory.CreateDirectory,但Directory.GetDirectories和Directory.GetFiles仍在UI线程执行,遍历目录时会直接阻塞UI。需把整个目录遍历逻辑用Task.Run包裹,确保UI线程不处理任何同步IO:private async Task CopyDirectoryAsync(string sourceDirectory, string destinationDirectory, string host, bool isCollectOperation) { try { string[] directories = null; string[] files = null; // 目录遍历移至后台线程 await Task.Run(() => { if (isCollectOperation) { directories = Directory.GetDirectories(sourceDirectory, "*", _defaultEnumerationOptions); files = Directory.GetFiles(sourceDirectory, "*.*", _defaultEnumerationOptions); } }); await CreateDirectoryAsync(destinationDirectory); // 并行创建子目录,提升效率 var dirCreateTasks = directories.Select(dir => CreateDirectoryAsync($@"{destinationDirectory}{dir.Replace(sourceDirectory, "")}")); await Task.WhenAll(dirCreateTasks); // 并行复制文件,注意控制并发数避免网络过载 var fileTasks = files.Select(file => CopyFileAsync(file, file.Replace(sourceDirectory, destinationDirectory), null, false)); await Task.WhenAll(fileTasks); // await后自动回到UI线程,直接更新UI即可 SetHostLabelColor(host, Color.Green); } catch (Exception ex) { SetHostLabelColor(host, Color.Red); await Log(ex); } }关于
CreateFileW阻塞的说明
网络共享的IO操作本身存在同步等待逻辑,即使使用FileOptions.Asynchronous,SMB在部分场景下仍会等待网络响应。但只要把触发这些操作的逻辑移至后台线程,UI线程就不会被阻塞——FileStream的异步操作基于IOCP完成端口,不会占用UI线程资源。UI上下文处理
WinForms中await会自动捕获当前同步上下文,所以SetHostLabelColor这类UI操作可直接调用,无需Control.BeginInvoke,因为await后会自动回到UI线程。
内容的提问来源于stack exchange,提问作者v.doro2

