2014-12-23 17 views
-2

所以我对多线程很新,我参加了一些youtube课程,阅读并且只是制作试用应用程序。WinForms中MultitThreading与监视器,不一致的顺序

我想这个'问题'是一个很好的例子(最后,在一些很好的答案之后),如何处理刚刚进入多线程的人的多线程。我会更新这篇文章,以便为更多读者提供一个很好的参考。

更新1:改变了锁定的方式,我认为Monitor是'更好',但这取决于你想要什么。代码根据@hatchat的回答进行编辑。我的第一条路仍然可以在LoadAllData()看到,而新的方式是在SaveData(),还更新了问题的答案和更多的猜测。

所以我做了一些测试,我想有一个对象,明显地命名为Databases来执行数据库命令。我的目标是在没有用户看到任何性能问题的情况下继续工作的同时保存数据。一些要求:

  • 我必须能够发送数据集到需要在数据库中更新的类。
  • 我需要能够从数据库检索数据并将其作为数据集取回。
  • 我必须能够调用与customerId关联的不同表格。

所以我有2种形式,一种接收所有数据和一种形式,只显示一个表(当客户从form1中选择时)。如果您不介意,我只是向您展示所有代码,在Form1中最重要的按钮是btnLoadAll,而在表格2中,最重要的代码位于btnSave事件中。

Form1中:

public delegate void ReturnDelegateDataSet(DataSet data); //used to return data to the main thread, used in form1 and form2 

public partial class MultiThread_w_database : Form 
{ 
    DataSet allCustomersData; 
    private int currentSelectedCustomer = -1; 

    public MultiThread_w_database() 
    { 
     InitializeComponent(); 
     btnLoadSelected.Enabled = false; 
     allCustomersData = new DataSet(); 
    } 

    private void btnLoadAll_Click(object sender, EventArgs e) 
    {//normally this would happen on formLoad 
     //we want to return a dataset, so we have to use a delegate. 
     ReturnDelegateDataSet delegateReturn = new ReturnDelegateDataSet(returnMethod1); 

     ThreadDatabase data = new ThreadDatabase(delegateReturn); 
     listBox1.Items.Add("creating Thread.." + DateTime.Now.ToString("hh:mm:ss:ms")); 
     Thread loadThread = new Thread(new ThreadStart(data.LoadAllData)); 
     listBox1.Items.Add("Starting Thread.." + DateTime.Now.ToString("hh:mm:ss:ms")); 
     loadThread.Start(); 
     listBox1.Items.Add("Thread running, waiting for join.." + DateTime.Now.ToString("hh:mm:ss:ms")); 
     loadThread.Join(60000);//so we are sure it should take longer then 5 seconds 
     //now .Join still makes the app freeze, but we try to make sure that we don't load data 
     //if we are still saving data. 
     if(allCustomersData != null) 
     { 
      dgvCustomers.DataSource = allCustomersData.Tables["allCustomers"]; 
      dgvSecondTable.DataSource = allCustomersData.Tables["secondTable"]; 
      listBox1.Items.Add("thread is back and datasource is set" + DateTime.Now.ToString("hh:mm:ss:ms")); 
     } 
     listBox1.Items.Add("end of buttonEvent"); 
    } 

    //method is used to return the dataset 
    public void returnMethod1(DataSet data) 
    { 
     allCustomersData = data; 
    } 

有2种以上的方法,用于检查被选择的内容的行,因此什么CustomerID和一个用于打开穿过所述客户ID的其它形式(如果不是-1)

现在我的窗体2:

public partial class MultiThread_w_Database2 : Form 
{ 
    DataSet currentCustomer; 
    private int currentSelectedCustomer = -1; 

    //I have this as global so I could recieve the reportString from that object 
    //we can't do console.writeline in here and you can't access form controls from 
    //another thread, not easy at least. 
    ThreadDatabase dataSave; 

    public MultiThread_w_Database2(int customerId) 
    { 
     InitializeComponent(); 
     currentSelectedCustomer = customerId; 

     //just giving datasource to datagridview, same like before. 

     //Like I said, global so I could receive the report strings from every action 
     dataSave = new ThreadDatabase(currentCustomer, currentSelectedCustomer); 
    } 

    private void btnSave_Click(object sender, EventArgs e) 
    { //magic happening here, btw for some good reason it really saves the data 
     //even if we give the dataset already in the constructor of this form :) 
     if(currentSelectedCustomer != -1 && currentCustomer != null) 
     { 
      Thread saveThread = new Thread(new ThreadStart(dataSave.SaveData)); 
      saveThread.Start(); 
     } 

    } 

有2种方法,一个定时器,显示记录和一个回调方法,只能在形式的开机使用。

现在的数据库类,最重要的部分是LoadAllData()LoadCertainData()SaveData()

static object locker = new object(); 
    private DataSet currentData; 
    private SqlDataAdapter adapt; 
    private int customerId = -1; //if none, -1, might be useful for testing before calling function 
    private string connectionString = ConfigurationManager.ConnectionStrings["somestring"].ConnectionString; 
    private ReturnDelegateDataSet callbackData; 
    private string reportString = ""; //using for reporting back 
    //CONSTRUCTORS 
    //constructor for multiThread load ALL data 
    public ThreadDatabase(ReturnDelegateDataSet callback) 
    { 
     currentData = new DataSet(); 
     adapt = new SqlDataAdapter(); 
     callbackData = callback; 

    } 

    //constructor for getting data associated to a customer. 
    public ThreadDatabase(int customerId, ReturnDelegateDataSet callback) 
    { 
     currentData = new DataSet(); 
     adapt = new SqlDataAdapter(); 
     this.customerId = customerId; 
     callbackData = callback; 
    } 

    //constructor for multithread Send Data 
    public ThreadDatabase(DataSet dataNededToUpload, int customerId) 
    { 
     currentData = dataNededToUpload; 
     adapt = new SqlDataAdapter(); 

    } 

    //here is a property for returning reportString 

    //METHODS 
    //just load all data 
    public void LoadAllData() //must be void, since we will return dataset via delegate 
    { 
     Monitor.Enter(locker); 
     try 
     { 
      using (SqlConnection conn = new SqlConnection(connectionString)) 
      { 
       //doo some stuff to get data, fill in currentData Dataset, 2 tables. 
      } 
     } 
     catch (Exception e) { throw e;} 
     finally 
     { 

      Monitor.Exit(locker); 

     } 
     if(callbackData !=null) //'return' dataset 
     { 
      callbackData(currentData); 
     } 

    } 

    //load data for one customer 
    public void LoadCertainData() 
    { 

     if (Monitor.TryEnter(locker, 10000)) //with timeout 
     { 
      try 
      { 
       using (SqlConnection conn = new SqlConnection(connectionString)) 
       { 
        conn.Open(); 

        currentData.Tables.Clear(); //here we load another table, assosiated with the customer 
        SqlCommand comm = new SqlCommand("SELECT * FROM SecondTable WHERE customerId = @0", conn); 
        comm.Parameters.AddWithValue("@0", customerId); 
        currentData.Tables.Add("OneCustomers"); 
        adapt.SelectCommand = comm; 
        adapt.Fill(currentData.Tables["OneCustomers"]); 
       } 
      } 
      finally 
      { 
       Monitor.Exit(locker); 
      } 
      if (callbackData != null) //'return' dataset 
      { 
       callbackData(currentData); 
      } 
     } 
     else 
     { 
      //do other stuff 
     } 


    } 

    //save edited data 
    public void SaveData()//use dataset and customerId from constructor. 
    { 
     reportString += Thread.CurrentThread.Name + "trying to Aquire Lock " + DateTime.Now.ToString("hh:mm:ss:ms") + "\n"; 
     lock (locker) 
     { 
      reportString += Thread.CurrentThread.Name + "lock aquired, updateing database" + DateTime.Now.ToString("hh:mm:ss:ms") + "\n"; 
      Thread.Sleep(5000); 
      using (SqlConnection connect = new SqlConnection(connectionString)) 
      { 
       connect.Open(); 
       SqlCommandBuilder commbuilder = new SqlCommandBuilder(adapt); 
       adapt.SelectCommand = new SqlCommand("SELECT * FROM SecondTable WHERE customerId = " + customerId, connect); 
       adapt.Update(currentData.Tables["OneCustomers"]); 
      } 
      reportString += Thread.CurrentThread.Name + "closing lock" + DateTime.Now.ToString("hh:mm:ss:ms") + "\n"; 
     } 


    } 

测试结果:

testResults

对于较大:click this link

疑问,你是寻找?

  1. 为什么Form1上(在图像中左侧)能破线槽锁,一个时间它等待锁被再次打开,但大多不会,即使它被调用的另一种方法,它应该使用相同的锁。我对结果感到满意,首先它不会使我的应用程序崩溃,其次它不会同时保存,但只能从相同的表单中保存,然后我猜。
  2. 我已经尝试使用Monitor.TryEnter与间隔,这导致有时离开锁锁我做了,就像我在那里调用方法:LoadCertainData()在数据库类... 现在修复此问题,正确的方法显示在代码
  3. 我认为这台显示器能够获取锁,以便他们进来,在我已经编号了什么顺序点击(datagridview)和它执行的顺序的图像。它没有按照正确的顺序进行,看起来最后一个出现在第一位。我不知道如何this queuing works 事实证明它没有。
  4. 最后但并非最不重要的是,这被认为是ThreadSafe?我正朝着好的方向前进吗? 认为没有第一个,仍然必须修复正确的方法返回对象(数据集)我想这不是一个好主意,使用相同的类都为非多线程方法和multitheading功能?

对于1.我想这是因为我们创建数据库对象的另一个实例,在我看来静止目标应该可以解决这个问题,因为它在窗口2显然不。 编辑:根据@Hatchet,我说得对,但这让我想:我创建了6个新的form2实例,因此又创建了6个更多的ThreadDatabase实例,那么为什么那些6在同步中,这意味着他们不同时获得一个锁。而form1可以同时获得一个锁。我注意到:当另一个线程(在form2中)已经获得了锁的时候,form1会一直等到它被打开。但是,一旦该锁打开,form1和form2都可以获取该锁。

我知道,在图像中,我可以很容易地编辑ID的表的,它只是一个测试,我知道我的周围

+0

你不应该使用多线程。教程是可怕的过时。使用单线程异步方法。 – Aron

+1

我认为这个问题会更适合于http://codereview.stackexchange.com/ – hatchet

+0

我会检查一下Aron,谢谢,但我认为这是围绕多线程的全部魔法,让GUI在运行时保持平稳运行在后台的东西。 – CularBytes

回答

2

方式我不是在多线程方面的专家,但它看起来像你的代码中有一些问题。其中一个是,尽管Monitor可用于.Net,并且可以使用,但我认为大多数人可能不会在特定情况下使用它。所以,你的代码最终看起来老式。在c#中有些东西可以在多任务代码中看到更加正常。一个很好的例子就是lock声明。这:

lock(lockObj) { 
    // do stuff 
} 

相当于

bool lockWasTaken = false; 
var temp = lockObj; 
try { 
    Monitor.Enter(temp, ref lockWasTaken); 
    { // do stuff } 
} finally { 
    if (lockWasTaken) Monitor.Exit(temp); 
} 

lock说法更容易,更清洁,更常见的,而且比你在做什么更稳健。

您使用Monitor.TryEnter是错误的。你试图获得一个锁,并且如果你在10秒后没有获得锁,你只需要提前充电,然后做你声称试图与锁同步的东西。我不明白你为什么使用TryEnter。如果您想阻止,则应该使用Enter,直到您可以获得锁定。如果你想尝试获得锁,你应该使用TryEnter,如果你不能,那么就不要做任何你用锁保护的东西。例如:

if (Monitor.TryEnter(lockObj, 10000)) { 
    try { 
     // do stuff that required a lock 
    } finally { 
     Monitor.Exit(lockObj); 
    } 
    } else { 
    // do something else when you couldn't get a lock 
    } 

我想知道您是否在概念上正确地思考锁的目的。不要把它看作是保护一些代码。它用于通过同步对这些资源的访问来保护共享资源的完整性。要知道在哪里锁定,您需要知道需要保护的内容。有各种各样的问题可以通过共享资源和锁定而出现。你必须在需要的地方使用锁,但是也就是说,你也应该只在需要的地方使用它们,并且在必要的范围内使用它们。很容易出错,当你这样做时,做错的问题和症状可能非常微妙,看起来很奇怪。

问题#1我认为部分覆盖上面。我还会补充一点,你是正确的,因为你创建了两个ThreadDatabase实例,这两个实例之间没有同步。这是因为每个实例都有自己的锁定对象。锁定对象不会阻止尝试锁定不同的对象。

问题#2覆盖上述

关于问题#3,你真的不 '执行' 的锁。你获得/获得一个锁。我认为你不应该考虑秩序。一旦你开始了一些线程,当它们以什么顺序到达代码中的某个特定点时,它就是你的手中的一种。没有一个“正确的”顺序。

问题#4:我很确定它不是线程安全的,如果没有别的,因为TryEnter的使用不正确。但我想也可能有其他问题。例如,ThreadDatabase中的方法返回一个私有实例成员currentData,并且一旦这样分发,对其的访问就不受任何同步的保护。

如果你想要一个非常全面的C#多线程资源,它是相当新的,我推荐Albahari's Threading in C#。他涵盖了可用的功能,并讨论了隐藏的许多隐藏陷阱。

+0

感谢您的链接,看起来不错。我更新了我的帖子 – CularBytes

相关问题