C# 线程池示例

以下三个代码示例演示 QueueUserWorkItem 和 RegisterWaitForSingleObject 方法。

第一个示例使用 QueueUserWorkItem 方法将一个由 ThreadProc 方法表示的非常简单的任务排入队列。

using System;
using System.Threading;
public class Example {
    public static void Main() {
        // 将任务排队
        ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc));
       
        Console.WriteLine("Main thread does some work, then sleeps.");
        // 如果你注释掉这个Sleep, 而线程池使用后台线程,
         // 那么主线程将会停止运行,并在线程池任务运行之前退出
         // (这是一个简单的线程池运行环境例子)
        Thread.Sleep(1000);

        Console.WriteLine("Main thread exits.");
    }

    // 这是执行任务的线程子程序
    static void ThreadProc(Object stateInfo) {
        // 没有state对象被传递到QueueUserWorkItem, 所以
        // stateInfo是null.
        Console.WriteLine("Hello from the thread pool.");
    }
}

为 QueueUserWorkItem 提供任务数据
下面的代码示例使用 QueueUserWorkItem 方法将一个任务排队并为该任务提供数据。

using System;
using System.Threading;

// TaskInfo 类 为任务提供将在线程池线程中执行的 state 信息
public class TaskInfo {
    // 任务所需的State信息,这些类成员可以是任务需要的确定的只读属性,
    // 可读写属性,或者其它。
    public string Boilerplate;
    public int Value;

    // Public 构造函数提供一个非常简单途径用来接收任务所需要的信息
    public TaskInfo(string text, int number) {
        Boilerplate = text;
        Value = number;
    }
}

public class Example {
    public static void Main() {
        // 创建一个包含任务所需要信息的对象。
        TaskInfo ti = new TaskInfo("This report displays the number {0}.", 42);

        // 将任务排队并传入所需信息
        if (ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc), ti)) {   
            Console.WriteLine("Main thread does some work, then sleeps.");

            // 如果你注释掉这个Sleep, 而线程池使用后台线程,
              // 那么主线程将会停止运行,并在线程池任务有机会运行之前退出
              // (这是一个简单的线程池运行环境例子)
            Thread.Sleep(1000);

            Console.WriteLine("Main thread exits.");
        }
        else {
            Console.WriteLine("Unable to queue ThreadPool request.");
        }
    }

    // 这个线程子程序执行独立的任务,在这里转换并打印一个非常简单的报告
    static void ThreadProc(Object stateInfo) {
        TaskInfo ti = (TaskInfo) stateInfo;
        Console.WriteLine(ti.Boilerplate, ti.Value);
    }
}

RegisterWaitForSingleObject
下面的示例演示几种线程处理功能。

· 使用 RegisterWaitForSingleObject 方法将任务排队,以由 ThreadPool 线程执行。

· 使用 AutoResetEvent 发出信号,通知执行任务。请参见 EventWaitHandle、AutoResetEvent 和 ManualResetEvent。

· 使用 WaitOrTimerCallback 委托处理超时和信号。

· 使用 RegisteredWaitHandle 取消排入队列的任务。

 

using System;
using System.Threading;

// TaskInfo 包含将要被回调(callback)的数据
public class TaskInfo {
    public RegisteredWaitHandle Handle = null;
    public string OtherInfo = "default";
}

public class Example {
    public static void Main(string[] args) {
        // 主线程使用 AutoResetEvent 通知执行回调方法的 wait 句柄(handle)
        AutoResetEvent ev = new AutoResetEvent(false);

        TaskInfo ti = new TaskInfo();
        ti.OtherInfo = "First task";
        // TaskInfo对象包含了为任务返回RegisterWaitForSingleObject的注册等待句柄(handle)
        // 这允许当对象一旦被通知就终止等待操作。(见 WaitProc).
        ti.Handle = ThreadPool.RegisterWaitForSingleObject(
            ev,
            new WaitOrTimerCallback(WaitProc),
            ti,
            1000,
            false
        );

        // 主线程等待三秒钟,用以示范排队线程超时的情况,然后通知它。
        Thread.Sleep(3100);
        Console.WriteLine("Main thread signals.");
        ev.Set();

        // 主线程休眠(sleeps),应该能给回调(callback)方法足够的时候去执行它。
        // 如果你注释掉这行,程序通常会在线程池线程被执行前退出。
        Thread.Sleep(1000);
        // 如果你开始一个自己的线程,你可以调用 Thread.Join() 来等待它完成。
        // 这个选项在线程池线程里不可用。
    }
  
    // 当注册等待超时,或者当等待句柄(WaitHandle——在这个例子里是AutoResetEvent)被通知时,
    // 将会执行这个回调(callback)方法,
    // WaitProc 反注册等待句柄(WaitHandle) the first time the event is
    // signaled.
    public static void WaitProc(object state, bool timedOut) {
        // state 对象必须传入正确的类型,因为 WaitOrTimerCallback 委托(delegate)
        // 明确指出的对象的类型。
        TaskInfo ti = (TaskInfo) state;

        string cause = "TIMED OUT";
        if (!timedOut) {
            cause = "SIGNALED";
            // 如果这个回调(callback)方法被执行是因为等待句柄(WaitHandle)通知,
            // 停止执行接下来的回调(callback )方法, 并反注册等待句柄(WaitHandle)
            if (ti.Handle != null)
                ti.Handle.Unregister(null);
        }

        Console.WriteLine("WaitProc( {0} ) executes on thread {1}; cause = {2}.",
            ti.OtherInfo,
            Thread.CurrentThread.GetHashCode().ToString(),
            cause
        );
    }
}

 

---------------------------------------------------------------------------------------------------

c#.net多线程编程教学(3):线程同步

随着对多线程学习的深入,你可能觉得需要了解一些有关线程共享资源的问题. .NET framework提供了很多的类和数据类型来控制对共享资源的访问。

  考虑一种我们经常遇到的情况:有一些全局变量和共享的类变量,我们需要从不同的线程来更新它们,可以通过使用System.Threading.Interlocked类完成这样的任务,它提供了原子的,非模块化的整数更新操作。

  还有你可以使用System.Threading.Monitor类锁定对象的方法的一段代码,使其暂时不能被别的线程访问。

  System.Threading.WaitHandle类的实例可以用来封装等待对共享资源的独占访问权的操作系统特定的对象。尤其对于非受管代码的互操作问题。

  System.Threading.Mutex用于对多个复杂的线程同步的问题,它也允许单线程的访问。

  像ManualResetEvent和AutoResetEvent这样的同步事件类支持一个类通知其他事件的线程。

  不讨论线程的同步问题,等于对多线程编程知之甚少,但是我们要十分谨慎的使用多线程的同步。在使用线程同步时,我们事先就要要能够正确的确定是那个对象和方法有可能造成死锁(死锁就是所有的线程都停止了相应,都在等者对方释放资源)。还有赃数据的问题(指的是同一时间多个线程对数据作了操作而造成的不一致),这个不容易理解,这么说吧,有X和Y两个线程,线程X从文件读取数据并且写数据到数据结构,线程Y从这个数据结构读数据并将数据送到其他的计算机。假设在Y读数据的同时,X写入数据,那么显然Y读取的数据与实际存储的数据是不一致的。这种情况显然是我们应该避免发生的。少量的线程将使得刚才的问题发生的几率要少的多,对共享资源的访问也更好的同步。

  .NET Framework的CLR提供了三种方法来完成对共享资源 ,诸如全局变量域,特定的代码段,静态的和实例化的方法和域。

  (1) 代码域同步:使用Monitor类可以同步静态/实例化的方法的全部代码或者部分代码段。不支持静态域的同步。在实例化的方法中,this指针用于同步;而在静态的方法中,类用于同步,这在后面会讲到。

  (2) 手工同步:使用不同的同步类(诸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent 和Interlocked等)创建自己的同步机制。这种同步方式要求你自己手动的为不同的域和方法同步,这种同步方式也可以用于进程间的同步和对共享资源的等待而造成的死锁解除。

  (3) 上下文同步:使用SynchronizationAttribute为ContextBoundObject对象创建简单的,自动的同步。这种同步方式仅用于实例化的方法和域的同步。所有在同一个上下文域的对象共享同一个锁。
Monitor Class

  在给定的时间和指定的代码段只能被一个线程访问,Monitor 类非常适合于这种情况的线程同步。这个类中的方法都是静态的,所以不需要实例化这个类。下面一些静态的方法提供了一种机制用来同步对象的访问从而避免死锁和维护数据的一致性。

  Monitor.Enter 方法:在指定对象上获取排他锁。

  Monitor.TryEnter 方法:试图获取指定对象的排他锁。

  Monitor.Exit 方法:释放指定对象上的排他锁。

  Monitor.Wait 方法:释放对象上的锁并阻塞当前线程,直到它重新获取该锁。

  Monitor.Pulse 方法:通知等待队列中的线程锁定对象状态的更改。

  Monitor.PulseAll 方法:通知所有的等待线程对象状态的更改。

  通过对指定对象的加锁和解锁可以同步代码段的访问。Monitor.Enter, Monitor.TryEnter 和 Monitor.Exit用来对指定对象的加锁和解锁。一旦获取(调用了Monitor.Enter)指定对象(代码段)的锁,其他的线程都不能获取该锁。举个例子来说吧,线程X获得了一个对象锁,这个对象锁可以释放的(调用Monitor.Exit(object) or Monitor.Wait)。当这个对象锁被释放后,Monitor.Pulse方法和 Monitor.PulseAll方法通知就绪队列的下一个线程进行和其他所有就绪队列的线程将有机会获取排他锁。线程X释放了锁而线程Y获得了锁,同时调用Monitor.Wait的线程X进入等待队列。当从当前锁定对象的线程(线程Y)受到了Pulse或PulseAll,等待队列的线程就进入就绪队列。线程X重新得到对象锁时,Monitor.Wait才返回。如果拥有锁的线程(线程Y)不调用Pulse或PulseAll,方法可能被不确定的锁定。Pulse, PulseAll and Wait必须是被同步的代码段鄂被调用。对每一个同步的对象,你需要有当前拥有锁的线程的指针,就绪队列和等待队列(包含需要被通知锁定对象的状态变化的线程)的指针。

  你也许会问,当两个线程同时调用Monitor.Enter会发生什么事情?无论这两个线程地调用Monitor.Enter是多么地接近,实际上肯定有一个在前,一个在后,因此永远只会有一个获得对象锁。既然Monitor.Enter是原子操作,那么CPU是不可能偏好一个线程而不喜欢另外一个线程的。为了获取更好的性能,你应该延迟后一个线程的获取锁调用和立即释放前一个线程的对象锁。对于private和internal的对象,加锁是可行的,但是对于external对象有可能导致死锁,因为不相关的代码可能因为不同的目的而对同一个对象加锁。

  如果你要对一段代码加锁,最好的是在try语句里面加入设置锁的语句,而将Monitor.Exit放在finally语句里面。对于整个代码段的加锁,你可以使用MethodImplAttribute(在System.Runtime.CompilerServices命名空间)类在其构造器中设置同步值。这是一种可以替代的方法,当加锁的方法返回时,锁也就被释放了。如果需要要很快释放锁,你可以使用Monitor类和C# lock的声明代替上述的方法。

  让我们来看一段使用Monitor类的代码:
 

public void some_method()
{

int a=100;

int b=0;

Monitor.Enter(this);

//say we do something here.

int c=a/b;

Monitor.Exit(this);

}

  上面的代码运行会产生问题。当代码运行到int c=a/b; 的时候,会抛出一个异常,Monitor.Exit将不会返回。因此这段程序将挂起,其他的线程也将得不到锁。有两种方法可以解决上面的问题。第一个方法是:将代码放入try…finally内,在finally调用Monitor.Exit,这样的话最后一定会释放锁。第二种方法是:利用C#的lock()方法。调用这个方法和调用Monitoy.Enter的作用效果是一样的。但是这种方法一旦代码执行超出范围,释放锁将不会自动的发生。见下面的代码:
 

public void some_method()
{

int a=100;

int b=0;

lock(this);

//say we do something here.

int c=a/b;

}

  C# lock申明提供了与Monitoy.Enter和Monitoy.Exit同样的功能,这种方法用在你的代码段不能被其他独立的线程中断的情况。
WaitHandle Class

  WaitHandle类作为基类来使用的,它允许多个等待操作。这个类封装了win32的同步处理方法。WaitHandle对象通知其他的线程它需要对资源排他性的访问,其他的线程必须等待,直到WaitHandle不再使用资源和等待句柄没有被使用。下面是从它继承来的几个类:

  Mutex 类:同步基元也可用于进程间同步。

  AutoResetEvent:通知一个或多个正在等待的线程已发生事件。无法继承此类。

  ManualResetEvent:当通知一个或多个正在等待的线程事件已发生时出现。无法继承此类。

  这些类定义了一些信号机制使得对资源排他性访问的占有和释放。他们有两种状态:signaled 和 nonsignaled。Signaled状态的等待句柄不属于任何线程,除非是nonsignaled状态。拥有等待句柄的线程不再使用等待句柄时用set方法,其他的线程可以调用Reset方法来改变状态或者任意一个WaitHandle方法要求拥有等待句柄,这些方法见下面:

  WaitAll:等待指定数组中的所有元素收到信号。

  WaitAny:等待指定数组中的任一元素收到信号。

  WaitOne:当在派生类中重写时,阻塞当前线程,直到当前的 WaitHandle 收到信号。

  这些wait方法阻塞线程直到一个或者更多的同步对象收到信号。

  WaitHandle对象封装等待对共享资源的独占访问权的操作系统特定的对象无论是收管代码还是非受管代码都可以使用。但是它没有Monitor使用轻便,Monitor是完全的受管代码而且对操作系统资源的使用非常有效率。

Mutex Class

  Mutex是另外一种完成线程间和跨进程同步的方法,它同时也提供进程间的同步。它允许一个线程独占共享资源的同时阻止其他线程和进程的访问。Mutex的名字就很好的说明了它的所有者对资源的排他性的占有。一旦一个线程拥有了Mutex,想得到Mutex的其他线程都将挂起直到占有线程释放它。Mutex.ReleaseMutex方法用于释放Mutex,一个线程可以多次调用wait方法来请求同一个Mutex,但是在释放Mutex的时候必须调用同样次数的Mutex.ReleaseMutex。如果没有线程占有Mutex,那么Mutex的状态就变为signaled,否则为nosignaled。一旦Mutex的状态变为signaled,等待队列的下一个线程将会得到Mutex。Mutex类对应与win32的CreateMutex,创建Mutex对象的方法非常简单,常用的有下面几种方法:

  一个线程可以通过调用WaitHandle.WaitOne 或 WaitHandle.WaitAny 或 WaitHandle.WaitAll得到Mutex的拥有权。如果Mutex不属于任何线程,上述调用将使得线程拥有Mutex,而且WaitOne会立即返回。但是如果有其他的线程拥有Mutex,WaitOne将陷入无限期的等待直到获取Mutex。你可以在WaitOne方法中指定参数即等待的时间而避免无限期的等待Mutex。调用Close作用于Mutex将释放拥有。一旦Mutex被创建,你可以通过GetHandle方法获得Mutex的句柄而给WaitHandle.WaitAny 或 WaitHandle.WaitAll 方法使用。

  下面是一个示例:
 

public void some_method()
{

int a=100;

int b=20;

Mutex firstMutex = new Mutex(false);

FirstMutex.WaitOne();

//some kind of processing can be done here.

Int x=a/b;

FirstMutex.Close();

}

  在上面的例子中,线程创建了Mutex,但是开始并没有申明拥有它,通过调用WaitOne方法拥有Mutex。
Synchronization Events

  同步时间是一些等待句柄用来通知其他的线程发生了什么事情和资源是可用的。他们有两个状态:signaled and nonsignaled。AutoResetEvent 和 ManualResetEvent就是这种同步事件。

AutoResetEvent Class

  这个类可以通知一个或多个线程发生事件。当一个等待线程得到释放时,它将状态转换为signaled。用set方法使它的实例状态变为signaled。但是一旦等待的线程被通知时间变为signaled,它的转台将自动的变为nonsignaled。如果没有线程侦听事件,转台将保持为signaled。此类不能被继承。

ManualResetEvent Class

  这个类也用来通知一个或多个线程事件发生了。它的状态可以手动的被设置和重置。手动重置时间将保持signaled状态直到ManualResetEvent.Reset设置其状态为nonsignaled,或保持状态为nonsignaled直到ManualResetEvent.Set设置其状态为signaled。这个类不能被继承。

Interlocked Class

  它提供了在线程之间共享的变量访问的同步,它的操作时原子操作,且被线程共享.你可以通过Interlocked.Increment 或 Interlocked.Decrement来增加或减少共享变量.它的有点在于是原子操作,也就是说这些方法可以代一个整型的参数增量并且返回新的值,所有的操作就是一步.你也可以使用它来指定变量的值或者检查两个变量是否相等,如果相等,将用指定的值代替其中一个变量的值.

ReaderWriterLock class

  它定义了一种锁,提供唯一写/多读的机制,使得读写的同步.任意数目的线程都可以读数据,数据锁在有线程更新数据时将是需要的.读的线程可以获取锁,当且仅当这里没有写的线程.当没有读线程和其他的写线程时,写线程可以得到锁.因此,一旦writer-lock被请求,所有的读线程将不能读取数据直到写线程访问完毕.它支持暂停而避免死锁.它也支持嵌套的读/写锁.支持嵌套的读锁的方法是ReaderWriterLock.AcquireReaderLock,如果一个线程有写锁则该线程将暂停;

  支持嵌套的写锁的方法是ReaderWriterLock.AcquireWriterLock,如果一个线程有读锁则该线程暂停.如果有读锁将容易倒是死锁.安全的办法是使用ReaderWriterLock.UpgradeToWriterLock方法,这将使读者升级到写者.你可以用ReaderWriterLock.DowngradeFromWriterLock方法使写者降级为读者.调用ReaderWriterLock.ReleaseLock将释放锁, ReaderWriterLock.RestoreLock将重新装载锁的状态到调用ReaderWriterLock.ReleaseLock以前.

结论:

  这部分讲述了.NET平台上的线程同步的问题.造接下来的系列文章中我将给出一些例子来更进一步的说明这些使用的方法和技巧.虽然线程同步的使用会给我们的程序带来很大的价值,但是我们最好能够小心使用这些方法.否则带来的不是受益,而将倒是性能下降甚至程序崩溃.只有大量的联系和体会才能使你驾驭这些技巧.尽量少使用那些在同步代码块完成不了或者不确定的阻塞的东西,尤其是I/O操作;尽可能的使用局部变量来代替全局变量;同步用在那些部分代码被多个线程和进程访问和状态被不同的进程共享的地方;安排你的代码使得每一个数据在一个线程里得到精确的控制;不是共享在线程之间的代码是安全的;在下一篇文章中我们将学习线程池有关的知识.

 

----------------------------------------------------------------------------------------------------------

C# 多线程控制控件实例(例程简单,注释详细)

该实例功能为“多线程控制UI控件”,线程函数实现自动加1。界面如下:

 

 

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace treadTest
{   
    //定义委托
    public delegate void ListBoxDelegate();
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        //委托处理方法(关联与ListBoxDelegate)
        private void ListBox()
        {
            if (!listBox1.InvokeRequired)//如果在UI主线程操作ListBox,
            {
                listBox1.Items.Add(++CommonData.num);//则直接进行控件操作,“与UI主线程相关联”
                listBox1.SelectedItem = listBox1.Items[listBox1.Items.Count - 1];
            }
            else//如果是在另一线程操作ListBox,则启用委托
                listBox1.Invoke(new ListBoxDelegate(listShow));
        }
       
        //定义对UI主线程控件的操作,“与AddAuto相关联”。
        private void listShow()
        {
            listBox1.Items.Add(CommonData.num);
            listBox1.SelectedItem = listBox1.Items[listBox1.Items.Count - 1];
        }
        //定义线程函数
        private void AddAuto()
        {
            while (CommonData.Flag == 0)
            {
                CommonData.num++;
                Thread.Sleep(1000);
                ListBox();//不能直接控制UI上的控件,所以用该方法选择使用委托
            }
        }
        //在click事件中启动多线程
        private void btnStart_Click(object sender, EventArgs e)
        {
            //线程标志置0,表示开启线程
            CommonData.Flag = 0;
            //定义 ThreadStart的委托类型的参数,并使该委托指向线程函数
            ListBoxDelegate mycn = new ListBoxDelegate(AddAuto);
            //实例化线程
            Thread insertTxt = new Thread(new ThreadStart(mycn));
            //启动线程
            insertTxt.Start();     
        }

        private void btnAbort_Click(object sender, EventArgs e)
        {
            CommonData.Flag = 1;
        }
        private void btnCtrlMain_Click(object sender, EventArgs e)
        {
            ListBox();
        }
        private void btnReset_Click(object sender, EventArgs e)
        {
            CommonData.num = 0;
        }
        private void btnClear_Click(object sender, EventArgs e)
        {
            listBox1.Items.Clear();
        }
        private void btnQuit_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

    }
   
    //全局变量解决方案
    public class CommonData
    {
        private static int _Flag = 0;
        private static int _num = 0;
        public static int Flag
        {
            get { return _Flag; }
            set { _Flag = value; }
        }
        public static int num
        {
            get { return _num; }
            set { _num = value; }
        }
    }
}

 

总结:

        要使用多线程控制UI控件,必须用委托实现。调用控件的Invoke方法(Invoke方法的参数是一个委托类型的参数)。

实现步骤:

         1.声明委托。

          2.声明委托处理函数(判断是主线程控制UI控件,还是Invoke(多线程)控制UI控件)。

         3.声明一个线程实例,将线程函数的委托传入ThreadStart()。

         4.开启该线程。

         5.定义该线程函数,欲控制UI控件,则调用第2步的委托处理函数,他将自己判断选择用Invoke。

         6.定义Invoke需要调用的函数(如本例的listShow函数)

//*********************************************************************************************************************************

      在上例中,只是完成了多线程控制主线程控件的功能,如果能手动和自动同时访问全局变量时,就有可能出现线程不同步的问题。以下主要利用lock线程锁来修改解决方案,使线程同步,注意代码带动的地方。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace treadTest
{   
    //定义委托
    public delegate void ListBoxDelegate();
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        //委托处理方法(关联与ListBoxDelegate)
        private void ListBox()
        {
            if (!listBox1.InvokeRequired)//如果在UI主线程操作ListBox,
            {
                listBox1.Items.Add(CommonData.plus());//则直接进行控件操作,“与UI主线程相关联”
                listBox1.SelectedItem = listBox1.Items[listBox1.Items.Count - 1];
            }
            else//如果是在另一线程操作ListBox,则启用委托
                listBox1.Invoke(new ListBoxDelegate(listShow));
        }
       
        //定义对UI主线程控件的操作,“与AddAuto相关联”。
        private void listShow()
        {
            listBox1.Items.Add(CommonData.plus());
            listBox1.SelectedItem = listBox1.Items[listBox1.Items.Count - 1];
        }
        //定义线程函数
        private void AddAuto()
        {
            while (CommonData.Flag == 0)
            {
                Thread.Sleep(1000);
                ListBox();//不能直接控制UI上的控件,所以用该方法选择使用委托
            }
        }
        //在click事件中启动多线程
        private void btnStart_Click(object sender, EventArgs e)
        {
            //线程标志置0,表示开启线程
            CommonData.Flag = 0;
            //定义 ThreadStart的委托类型的参数,并使该委托指向线程函数
            ListBoxDelegate mycn = new ListBoxDelegate(AddAuto);
            //实例化线程
            Thread insertTxt = new Thread(new ThreadStart(mycn));
            //启动线程
            insertTxt.Start();     
        }

        private void btnAbort_Click(object sender, EventArgs e)
        {
            CommonData.Flag = 1;
        }
        private void btnCtrlMain_Click(object sender, EventArgs e)
        {
            ListBox();
        }
        private void btnReset_Click(object sender, EventArgs e)
        {
            CommonData.num = 0;
        }
        private void btnClear_Click(object sender, EventArgs e)
        {
            listBox1.Items.Clear();
        }
        private void btnQuit_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }
    }
   
    //全局变量解决方案
    public class CommonData
    {
        private static int _Flag = 0;
        private static int _num = 0;
        public static int plus()
        {
            lock (new object())
            {
                return _num++;
            }
        }
        public static int Flag
        {
            get { return _Flag; }
            set { _Flag = value; }
        }
        public static int num
        {
            get { return _num; }
            set { _num = value; }
        }
    }
}

点击查看原图
此条目发表在article分类目录,贴了, 标签。将固定链接加入收藏夹。