使用Gtk#的C#程序异步显示文本触发System.AccessViolationException的解决方法
解决Gtk#异步更新UI时的线程安全问题
这个问题的核心原因很明确:Gtk的所有UI操作必须在主线程(GUI线程)中执行,而你当前的代码在await Task.Delay(1000)之后,后续的textview.Buffer.Insert操作是在非GUI线程执行的,这直接违反了Gtk的线程模型,导致了内存访问错误(不管是BSD下的SIGABRT/SIGSEGV还是Windows的AccessViolationException,本质都是线程不安全的UI访问)。
和WinForms/WPF不同,Gtk并没有默认的SynchronizationContext来帮你把await后的代码切回GUI线程,所以直接用async/await操作UI控件必然会出问题。下面是两种可行的解决方案:
方案一:使用Gtk的GLib.Timeout实现定时更新(推荐)
Gtk提供了GLib.Timeout.Add方法,它会在GUI线程中定时触发回调函数,完美符合你的需求。我们可以用这个方法来逐行插入文本:
void DisplayText(string FileName) { // 读取所有行(如果文件很大,建议用StreamReader逐行读取,避免内存占用过高) var lines = File.ReadAllLines(FileName); var lineEnumerator = lines.GetEnumerator(); // 每1000ms触发一次回调 GLib.Timeout.Add(1000, () => { if (!lineEnumerator.MoveNext()) { // 所有行处理完毕,停止定时器 lineEnumerator.Dispose(); return false; } var currentLine = lineEnumerator.Current; var scoreBoard = currentLine.Split('\t'); // 所有UI操作都在GUI线程执行,安全可靠 TextIter ti = textview.Buffer.EndIter; // 改为从末尾插入,避免迭代器失效 textview.Buffer.Insert(ref ti, scoreBoard[0]); textview.Buffer.Insert(ref ti, " | "); textview.Buffer.Insert(ref ti, scoreBoard[1]); textview.Buffer.Insert(ref ti, "\t"); textview.Buffer.Insert(ref ti, scoreBoard[2]); textview.Buffer.Insert(ref ti, " | "); textview.Buffer.Insert(ref ti, scoreBoard[3]); textview.Buffer.Insert(ref ti, "\n"); // 返回true继续触发定时器,返回false停止 return true; }); }
方案二:手动将UI操作切回GUI线程
如果你坚持要用async/await,可以在await之后,用Gtk.Application.Invoke把UI操作包裹起来,强制在GUI线程执行:
async void DisplayText(string FileName) { string[] temp = File.ReadAllLines(FileName); foreach (string line in temp) { var scoreBoard = line.Split('\t'); await Task.Delay(1000); // 用Application.Invoke将UI操作调度到GUI线程 Gtk.Application.Invoke((sender, e) => { TextIter ti = textview.Buffer.EndIter; // 每次都获取最新的末尾迭代器 textview.Buffer.Insert(ref ti, scoreBoard[0]); textview.Buffer.Insert(ref ti, " | "); textview.Buffer.Insert(ref ti, scoreBoard[1]); textview.Buffer.Insert(ref ti, "\t"); textview.Buffer.Insert(ref ti, scoreBoard[2]); textview.Buffer.Insert(ref ti, " | "); textview.Buffer.Insert(ref ti, scoreBoard[3]); textview.Buffer.Insert(ref ti, "\n"); }); } }
这里要注意:原来的TextIter是和文本缓冲区的状态绑定的,在await期间缓冲区可能已经被修改,所以直接复用原来的迭代器会导致插入位置错误,每次在UI操作前重新获取EndIter是更稳妥的做法。
额外建议
- 如果你的文本文件很大,建议不要一次性用
File.ReadAllLines读取所有内容,而是用StreamReader逐行读取,避免占用过多内存。 - 始终记住Gtk的线程规则:任何UI控件的创建、修改、访问都必须在GUI线程执行,所有后台操作的结果都要通过
Gtk.Application.Invoke或者GLib.Timeout/GLib.Idle调度到主线程处理。
内容的提问来源于stack exchange,提问作者Jihoo Byeon
相关产品推荐
相关产品推荐

