线程内调用的方法以意外的顺序执行 (C# .net)

Method called within thread executes in unexpected order (C# .net)

寻求一般帮助以理解此程序流程: 在 windows 表单应用程序中,为什么当我在新线程中调用方法时,该线程不等待方法完成就继续执行?

线程内调用的方法默认是异步执行的吗? (我希望程序在方法完成之前阻塞,而不必使用下面的 Thread.Sleep 行)。下面 "Thread.Sleep" 行的评论可能有助于进一步澄清我的问题。 - 谢谢!

    private void button1_Click(object sender, EventArgs e)
    {
        //Call doStuff in new thread:
        System.Threading.Thread myThread;
        myThread = new System.Threading.Thread(new System.Threading.ThreadStart(doStuff));
        myThread.Start();
    }

    private void doStuff()
    {
        //Instantiate external class used for threadSafe interaction with UI objects:
        ThreadSafeCalls tsc = new ThreadSafeCalls();
        int indx_firstColumn = 0;

        //Loop 3 times, add a row, and add value of "lastRowAdded" to column1 cell.
        for (int a = 0; a < 3; a += 1)
        {
            tsc.AddGridRow(dataGridView1); //Call method to add a row to gridview:
            Thread.Sleep(1000); // Why does the execution of the line above go all crazy if I don't pause here? It's executing on the same thread, shouldn't it be synchronous? 
            tsc.AddGridCellData(dataGridView1,indx_firstColumn, tsc.lastRowAdded,tsc.lastRowAdded.ToString()); //Add value of "lastRowAdded" (for visual debugging)
        }

"ThreadSafeCalls"class的内容:

        public int lastRowAdded = -999;

    public void AddGridRow(DataGridView gv)
    {
        if (gv.InvokeRequired)
        {
            gv.BeginInvoke((MethodInvoker)delegate () 
            {
                lastRowAdded = gv.Rows.Add();
            });
        }
        else
        {
            lastRowAdded = gv.Rows.Add();
        }
    }

    public void AddGridCellData(DataGridView gv, int column, int rowID, string value)
    {
        //Thread safe:
            if (gv.InvokeRequired)
            {
                gv.BeginInvoke((MethodInvoker)delegate () { gv.Rows[rowID].Cells[column].Value = value + " "; });
            }
            else
            {
                gv.Rows[rowID].Cells[column].Value = value;
            }
    }

来自搜索结果:

Control.BeginInvoke Method (System.Windows.Forms)

在创建控件基础句柄的线程上异步执行委托。在线程上异步执行指定委托

来源:Control.BeginInvoke Method 在 msdn 上。

编辑: 你说的 "go all crazy" 的意思不是很清楚,但在盯着代码看了很长一段时间后,我意识到有一场比赛 window 在不使用睡眠时在循环中表现出来。我运行这个剥离版多次确认。为冗长的编辑道歉,但没有细节很难理解这个问题。

//Loop 3 times, add a row, and add value of "lastRowAdded" to column1 cell.
for (int a = 0; a < 3; a += 1)
{
    tsc.AddGridRow(dataGridView1); //Call method to add a row to gridview:
    Thread.Sleep(1000); // Why does the execution of the line above go all crazy if I don't pause here? It's executing on the same thread, shouldn't it be synchronous? 
    tsc.AddGridCellData(dataGridView1,indx_firstColumn, tsc.lastRowAdded,tsc.lastRowAdded.ToString()); //Add value of "lastRowAdded" (for visual debugging)
}

分解:

  1. 安排在 UI 线程上添加一行并更新 lastRowAdded
  2. 休眠 1 秒。遗漏它会导致种族显现。
  3. 传递 lastRowAdded 的值和等效字符串,因为 myThread 记得它,安排要在 UI 线程上更新的单元格。
  4. 重复 3 次。

您遇到这种情况的原因是 caching。本质上,当您忽略睡眠 myThread 时,会看到 lastRowAdded 的陈旧版本,然后将陈旧的副本传递给 AddGridCellData。此陈旧值传播到 UI 线程,通常会导致 -999 或其他一些不正确的行索引。有时您应该会收到 IndexOutOfRangeException,但并非总是如此。休眠正好给 UI 线程足够的时间将其缓存的值写回主内存,然后 myThread 读取更新的值。根据 OS 在哪些内核上调度线程,它似乎在某些运行中正常运行而在其他运行中不正确。

要解决此问题,您需要移除睡眠并同步访问 ThreadSafeCalls 中的所有可变数据。最简单的方法是使用锁。

循环实现:

for (int a = 0; a < 3; a += 1)
{
    tsc.AddGridRow(dataGridView1);
    tsc.AddGridCellData(dataGridView1, indx_firstColumn);
}

TSC 实施:

class ThreadSafeCalls
{
    private object syncObject = new object();
    private int lastRowAdded = -999;

    public int LastRowAdded
    {
        get {
            lock (syncObject) {
                return lastRowAdded;
            }
        }
        set {
            lock (syncObject) {
                lastRowAdded = value;
            }
        }
    }

    public void AddGridRow(DataGridView gv)
    {
        if (gv.InvokeRequired) {
            gv.BeginInvoke((MethodInvoker)delegate () {
                LastRowAdded = gv.Rows.Add();
            });
        }
        else {
            LastRowAdded = gv.Rows.Add();
        }
    }
    public void AddGridCellData(DataGridView gv, int column)
    {
        if (gv.InvokeRequired) {
            gv.BeginInvoke((MethodInvoker)delegate () {
                var lastRow = LastRowAdded;
                gv.Rows[lastRow].Cells[column].Value = lastRow + " "; 
            });
        } else {
            var lastRow = LastRowAdded;
            gv.Rows[lastRow].Cells[column].Value = lastRow + " ";
        }
    }
}

原答案:

Are methods called within a thread executed asynchronously by default?

Yes 指的是针对创建线程执行 ThreadStart 目标方法;这就是线程的意义所在。您可以使用额外的线程来执行后台加载或额外处理等操作,而不会阻塞主线程。

(I would expect the program to block until the method is complete without having to use the Thread.Sleep line below).

BeginInvoke 向 UI 线程调度程序添加一个方法,以便稍后在 UI 线程 上 调用之前正在做(即处理 OS 事件,执行在您添加的方法之前安排的方法)。这是延迟执行,returns 一旦被调度,这就是它不阻塞的原因。执行被推迟到 UI 线程的原因是大多数 UI 框架不是线程安全的。修改 UI 的多个线程会导致数据竞争,从而产生各种问题。

附带说明一下,您真的应该避免创建线程来响应用户输入。线程是昂贵的资源,应该尽快创建(最好是在初始化时)。在您的示例中,每次单击按钮时都会创建一个线程,这非常慢。