我花了一整天的时间试图让我的应用程序使用线程,但没有运气。我已经阅读了很多关于它的文档,我仍然会遇到很多错误,所以我希望你能帮助我。
我有一个耗时的方法,它调用数据库并更新GUI。这必须一直发生(或大约每30秒)。
public class UpdateController
{
private UserController _userController;
public UpdateController(LoginController loginController, UserController userController)
{
_userController = userController;
loginController.LoginEvent += Update;
}
public void Update()
{
BackgroundWorker backgroundWorker = new BackgroundWorker();
while(true)
{
backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
backgroundWorker.RunWorkerAsync();
}
}
public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
_userController.UpdateUsersOnMap();
}
}
使用这种方法我得到一个例外,因为后台工作者不是和STA线程(但我可以理解这是我应该使用的)。我尝试过使用STA线程并且出现了其他错误。
我认为问题是因为我在进行数据库调用时尝试更新GUI(在后台线程中)。我应该只进行数据库调用,然后以某种方式它应该切换回主线程。在主线程执行后,它应该返回后台线程,依此类推。但是我看不出怎么做。
应用程序应在数据库调用后立即更新GUI。 Firering事件似乎不起作用。背景线程只是输入它们。
修改
一些非常好的答案:)这是新代码:
public class UpdateController{
private UserController _userController;
private BackgroundWorker _backgroundWorker;
public UpdateController(LoginController loginController, UserController userController)
{
_userController = userController;
loginController.LoginEvent += Update;
_backgroundWorker = new BackgroundWorker();
_backgroundWorker.DoWork += backgroundWorker_DoWork;
_backgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted;
}
public void _backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
_userController.UpdateUsersOnMap();
}
public void Update()
{
_backgroundWorker.RunWorkerAsync();
}
void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
//UI update
System.Threading.Thread.Sleep(10000);
Update();
}
public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
// Big database task
}
}
但是我怎么能每10秒运行一次呢? System.Threading.Thread.Sleep(10000)只会使我的GUI冻结,而在Update()中的while(true)循环会出现异常(Thread太忙)。
答案 0 :(得分:45)
您需要声明并配置一次BackgroundWorker - 然后在循环中调用RunWorkerAsync方法...
public class UpdateController
{
private UserController _userController;
private BackgroundWorker _backgroundWorker;
public UpdateController(LoginController loginController, UserController userController)
{
_userController = userController;
loginController.LoginEvent += Update;
_backgroundWorker = new BackgroundWorker();
_backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
_backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
_backgroundWorker.WorkerReportsProgress= true;
}
public void Update()
{
_backgroundWorker.RunWorkerAsync();
}
public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
while (true)
{
// Do the long-duration work here, and optionally
// send the update back to the UI thread...
int p = 0;// set your progress if appropriate
object param = "something"; // use this to pass any additional parameter back to the UI
_backgroundWorker.ReportProgress(p, param);
}
}
// This event handler updates the UI
private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// Update the UI here
// _userController.UpdateUsersOnMap();
}
}
答案 1 :(得分:10)
您必须使用 Control.InvokeRequired 属性来确定您是否在后台线程上。然后,您需要通过 Control.Invoke 方法调用修改UI的逻辑,以强制在主线程上进行UI操作。您可以通过创建委托并将其传递给 Control.Invoke 方法来完成此操作。这里的问题是你需要一些从控制派生的对象来调用这些方法。
修改:正如另一位用户发布的那样,如果您可以等到 BackgroundWorker.Completed 事件来更新您的用户界面,那么您可以订阅该事件并调用您的用户界面代码直接。在主应用程序线程上调用BackgroundWorker_Completed。我的代码假设您希望在操作期间进行更新。我的方法的一个替代方法是订阅 BwackgroundWorker.ProgressChanged 事件,但我相信在这种情况下您仍需要调用调用来更新您的UI。
例如
public class UpdateController
{
private UserController _userController;
BackgroundWorker backgroundWorker = new BackgroundWorker();
public UpdateController(LoginController loginController, UserController userController)
{
_userController = userController;
loginController.LoginEvent += Update;
}
public void Update()
{
// The while loop was unecessary here
backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
backgroundWorker.RunWorkerAsync();
}
public delegate void DoUIWorkHandler();
public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
// You must check here if your are executing on a background thread.
// UI operations are only allowed on the main application thread
if (someControlOnMyForm.InvokeRequired)
{
// This is how you force your logic to be called on the main
// application thread
someControlOnMyForm.Invoke(new
DoUIWorkHandler(_userController.UpdateUsersOnMap);
}
else
{
_userController.UpdateUsersOnMap()
}
}
}
答案 2 :(得分:5)
你应该删除while(true),你正在添加无限的事件处理程序并无限次地调用它们。
答案 3 :(得分:4)
您可以使用backgroundWorker类上的RunWorkerCompleted事件来定义后台任务完成时应执行的操作。所以你应该在DoWork处理程序中进行数据库调用,然后在RunWorkerCompleted处理程序中更新接口,如下所示:
BackgroundWorker bgw = new BackgroundWorker();
bgw.DoWork += (o, e) => { longRunningTask(); }
bgw.RunWorkerCompleted += (o, e) => {
if(e.Error == null && !e.Cancelled)
{
_userController.UpdateUsersOnMap();
}
}
bgw.RunWorkerAsync();
答案 4 :(得分:3)
除了之前的评论之外,请查看www.albahari.com/threading - 您将找到的有关线程的最佳文档。它将教您如何正确使用BackgroundWorker。
当BackgroundWorker触发Completed事件(在UI线程上调用以便于您使用时,您应该更新GUI,这样您就不必自己进行Control.Invoke了。)
答案 5 :(得分:2)
@ Lee的答案中的if语句应如下所示:
bgw.RunWorkerCompleted += (o, e) => {
if(e.Error == null && !e.Cancelled)
{
_userController.UpdateUsersOnMap();
}
}
...如果你想在没有错误的情况下调用UpdateUsersOnMap();
并且BgWorker没有被取消。
答案 6 :(得分:1)
这是您可以使用的源代码模式。在这个例子中,我正在重定向Console,然后我使用它来让后台工作程序在处理时将一些消息写入文本框。
这是辅助类TextBoxStreamWriter,用于重定向控制台输出:
public class TextBoxStreamWriter : TextWriter
{
TextBox _output = null;
public TextBoxStreamWriter(TextBox output)
{
_output = output;
}
public override void WriteLine(string value)
{
// When character data is written, append it to the text box.
// using Invoke so it works in a different thread as well
_output.Invoke((Action)(() => _output.AppendText(value+"\r\n")));
}
}
您需要在表单加载事件中使用它,如下所示:
private void Form1_Load(object sender, EventArgs e)
{
// Instantiate the writer and redirect the console out
var _writer = new TextBoxStreamWriter(txtResult);
Console.SetOut(_writer);
}
表单上还有一个按钮,它启动后台工作程序,它传递一个路径:
private void btnStart_Click(object sender, EventArgs e)
{
backgroundWorker1.RunWorkerAsync(txtPath.Text);
}
这是后台工作程序的工作负载,请注意它如何使用控制台将消息输出到文本框:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
var selectedPath = e.Argument as string;
Console.Out.WriteLine("Processing Path:"+selectedPath);
// ...
}
如果您之后需要重置某些控件,请按以下方式执行:
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
progressBar1.Invoke((Action) (() =>
{
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Style = ProgressBarStyle.Continuous;
}));
}
在此示例中,完成后,正在重置进度条。
重要事项:每当您访问GUI控件时,请像我在上面的示例中一样使用Invoke。 正如您在代码中看到的那样,使用Lambda可以轻松实现。