偶尔会出现 C# 中调用的问题



我不时使用调用所需的方法有问题,如果有人知道这个问题或者可以向我解释,我想 aks。我有一些代码:

splashScreen splashObj = splashScreen.GetInstance();
if (thrd == null)
{
thrd = new Thread(new ThreadStart(loadingScreenStart));
thrd.IsBackground = true;
thrd.Start();
}
else
{
if (splashObj.InvokeRequired)
{
splashObj.Invoke((MethodInvoker)delegate()
{
splashObj.Show();
}
);
}
else
{
splashObj.Show();
}
}
if (splashObj.loadLabel.InvokeRequired)
{
splashObj.loadLabel.Invoke((MethodInvoker)delegate()
{
splashObj.loadLabel.Text = "Checking Username...";
}
);
}
else
{
splashObj.loadLabel.Text = "Checking Username...";
}

public void loadingScreenStart()
{
splashScreen splashObj = splashScreen.GetInstance();
Application.Run(splashScreen.GetInstance());
}

启动画面代码:

public partial class splashScreen : Form
{
public Form1 form1;
public splashScreen()
{
InitializeComponent();
//form1 = frm1;
}
public splashScreen(Form1 frm1)
{
form1 = frm1;
}

private void splashScreen_Load(object sender, EventArgs e)
{
}
private static splashScreen m_instance = null;
private static object m_instanceLock = new object();
public static splashScreen GetInstance()
{
lock (m_instanceLock)
{
if (m_instance == null)
{
m_instance = new splashScreen();
}
}
return m_instance;
}
}

因此,我显示一个启动画面,而其余代码在后台加载。消息显示如"正在检查用户名...","检查密码...","正在加载..."等等。 这段代码工作正常,但有时似乎代码执行比线程快,然后我收到一个错误,指出该方法是从创建它的另一个线程调用的。为什么会这样?有没有解决这种问题的方法?这种情况可能每二十次处决中发生一次。

多线程很难,所以你应该尝试让它尽可能简单。第一,没有理由在启动画面类本身之外使用任何启动画面代码。其次,你应该始终知道你在做什么,所以InvokeRequired只是一个代码气味,说"我不知道谁会调用这种方法"。

void Main()
{
SplashScreen.ShowText("Loading 1");
Thread.Sleep(1000);
SplashScreen.ShowText("Loading 2");
Thread.Sleep(2000);
SplashScreen.Done();
Thread.Sleep(2000);
SplashScreen.ShowText("Loading 3");
}
// Define other methods and classes here
public partial class SplashScreen : Form
{
private static SplashScreen instance;
private static readonly ManualResetEvent initEvent = new ManualResetEvent(false);
Label loadLabel;
private SplashScreen()
{
// InitializeComponent();
loadLabel = new Label();
Controls.Add(loadLabel);
Load += (s, e) => initEvent.Set();
Closing += (s, e) => initEvent.Reset();
}
private static object syncObject = new object();
private static void InitializeIfRequired()
{
// If not set, we'll have to init the message loop
if (!initEvent.WaitOne(0))
{
lock (syncObject)
{
// Someone initialized it before us
if (initEvent.WaitOne(0)) return;
// Recreate the form if it was closed
instance = new SplashScreen();
var thread = new Thread(() => { Application.Run(instance); });
thread.Start();
// Wait until the form is ready
initEvent.WaitOne();
}
}
}
public static void ShowText(string text)
{
InitializeIfRequired();
instance.Invoke((Action)(() => 
{ 
if (!instance.IsDisposed) instance.loadLabel.Text = text; 
}
));
}
public static void Done()
{
// Is it closed already?
if (!initEvent.WaitOne(0)) return;
lock (syncObject)
{
// Someone closed it before us
if (!initEvent.WaitOne(0)) return;
instance.Invoke((Action)(() => { instance.Close(); }));
}
}
}

这是 LINQPad 的一个代码段,您需要删除对InitializeComponent的注释以及loadLabel控件(应改为在设计器上)。

这样,初始屏幕的逻辑就与应用程序的其余部分完全隔离。要显示初始屏幕文本,只需调用SplashScreen.ShowText。要使初始屏幕消失,请拨打SplashScreen.Done

请注意,无需使用InvokeRequired- 任何合法的SplashScreen.ShowText调用(或Done)都不可能不需要编组到初始屏幕的 UI 线程。

现在,这并不完美。这是我在大约 10 分钟内写好的东西。但它(可能:))是线程安全的,比原始的更好地遵循最佳实践,并且更易于使用。

此外,在某些情况下,我会使用更高级别的结构,例如Lazy<T>Task- 但由于这在这里并没有真正的帮助(启动新的消息循环,如果它被关闭,必须重新创建表单......),我选择了更简单的解决方案。

请注意,我使用的是Invoke而不是BeginInvoke或类似 - 这非常重要,因为否则可能会排队关闭,然后是处理已处理表单的ShowText。如果您打算从多个线程调用ShowText,则锁定整个ShowText身体会更安全。鉴于初始屏幕的通常用例,有一些线程安全是不必要的,但是......

好的,所以这个问题仍然存在,当你的线程在显示表单之前调用表单时,可能会发生这种情况;InvokeRequiredBeginInvoke都可能失败。

解决方案是在表单事件上启动线程如下所示

/// <summary>
/// Testing form
/// </summary>
public partial class MyForm : Form
{
/// <summary>
/// Work work
/// </summary>
private Thread _MyThread;
/// <summary>
/// New test form
/// </summary>
public MyForm()
{
// Initialize components
this.InitializeComponent();
}
/// <summary>
/// First time form is show
/// </summary>
private void MyForm_Shown(object sender, EventArgs e)
{
// Create and start thread
this._MyThread = new Thread(this.MyThreadWork);
this._MyThread.Start();
}
/// <summary>
/// Doing some work here
/// </summary>
private void MyThreadWork()
{
// Counting time
int count = 0;
// Forever and ever
while (true)
{
this.PrintInfo((count++).ToString());
Thread.Sleep(1000);
}
}
/// <summary>
/// Printing info into form title
/// </summary>
private void PrintInfo(string info)
{
// Needs to sync?
if (this.InvokeRequired)
{
// Sync me
this.BeginInvoke(new Action<string>(this.PrintInfo), info);
}
else
{
// Prints info
this.Text = info;
}
}
/// <summary>
/// Good bye form, don't miss the work work
/// </summary>
private void MyForm_FormClosed(object sender, FormClosedEventArgs e)
{
// Kill worker
this._MyThread.Abort();
}
}

有些人只会在第一个 PrintInfo 之前添加一个 Thread.Sleep,但很常见,你会使用什么睡眠时间?1、10、100?取决于表单的显示速度。

最新更新