C#多线程(3)

一、线程同步

所谓同步:是指在某一时刻只有一个线程可以访问变量。
如果不能确保对变量的访问是同步的,就会产生错误。
C#为同步访问变量提供了一个非常简单的方式,即使用c#语言的关键字Lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在c#中,关键字Lock定义如下:
Lock(expression)
{
   statement_block
}

expression代表你希望跟踪的对象:
           如果你想保护一个类的实例,一般地,你可以使用this;
           如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了

而statement_block就算互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。

以书店卖书为例

using System;
using System.Threading;

namespace StudyThread
{
    class Program
    {
        static void Main(string[] args)
        {
            BookShop book = new BookShop();
            //创建两个线程同时访问Sale方法
            Thread t1 = new Thread(new ThreadStart(book.sale));
            Thread t2 = new Thread(new ThreadStart(book.sale));

            //启动线程
            t1.Start();
            t2.Start();

            Console.ReadKey();
        }


    }//Class_end


    class BookShop
    {
        //剩余图书数量
        public int numbers = 1;
        public void sale()
        {
            int tmp = numbers;
            //进行判断是否还有书,有则可以继续卖
            if (tmp > 0)
            {
                Thread.Sleep(1000);
                numbers -= 1;
                Console.WriteLine("售出一本图书,还剩余{0}本", numbers);
            }
            else
            {
                Console.WriteLine("该书已经售罄,请等待补货!");
            }
        }


    }
}

 

从运行结果可以看出,两个线程同步访问共享资源,没有考虑同步的问题,结果不正确

考虑线程同步,改进后的代码:

using System;
using System.Threading;

namespace StudyThread
{
    class Program
    {
        static void Main(string[] args)
        {
            BookShop book = new BookShop();
            //创建两个线程同时访问Sale方法
            Thread t1 = new Thread(new ThreadStart(book.sale));
            Thread t2 = new Thread(new ThreadStart(book.sale));

            //启动线程
            t1.Start();
            t2.Start();

            Console.ReadKey();
        }


    }//Class_end


    class BookShop
    {
        //剩余图书数量
        public int numbers = 1;
        public void sale()
        {
            //使用Lock关键字解决线程同步问题
            lock (this)
            {
                int tmp = numbers;
                //进行判断是否还有书,有则可以继续卖
                if (tmp > 0)
                {
                    Thread.Sleep(1000);
                    numbers -= 1;
                    Console.WriteLine("售出一本图书,还剩余{0}本", numbers);
                }
                else
                {
                    Console.WriteLine("该书已经售罄,请等待补货!");
                }
            }

            
        }

    }//Class_end
}

运行结果如下所示:

 

二、跨线程访问 

点击“测试”,创建一个线程,从0循环到10000给文本框赋值,代码如下: 

using System;
using System.Threading;
using System.Windows.Forms;

namespace CrossThreadDemo
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        //测试按钮
        private void Btn_Test_Click(object sender, EventArgs e)
        {
            //创建一个线程去执行这个方法(创建的线程默认是前台线程)
            Thread thread = new Thread(new ThreadStart(Test));
            //Start方法标记这个线程已经准备就绪,可以随时执行,具体什么时候执行这个程序由CPU决定
            //将线程设置为后台线程
            thread.IsBackground = true;
            thread.Start();
        }

        //测试方法
        private void Test()
        {
            for (int i = 0; i < 100; i++)
            {
                this.TextBox_ShowInfo.Text += i.ToString() + "\n\t";
            }
        }

    }//Class_end
}

运行结果如下所示:

 

产生错误的原因TextBox_ShowInfo是由主线程创建的,thread线程是另外创建的一个线程,在.NET上执行的是托管代码,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。

解决方案:

①在窗体的加载事件中,将C#内置控件(Control)类的CheckForIllegalCrossThreadCalls属性设置为false,屏蔽掉C#编译器对跨线程调用的检查。

 private void Form1_Load(object sender, EventArgs e)
 {
        //取消跨线程的访问
        Control.CheckForIllegalCrossThreadCalls = false;
 }

使用上述的方法虽然可以保证程序正常运行并实现应用的功能,但是在实际的软件开发中,做如此设置是不安全的(不符合.NET的安全规范),在产品软件的开发中,此类情况是不允许的。如果要在遵守.NET安全标准的前提下,实现从一个线程成功地访问另一个线程创建的空间,要使用C#的方法回调机制。

②使用回调函数

回调实现的一般过程:

 C#的方法回调机制,也是建立在委托基础上的,下面给出它的典型实现过程。

(1)、定义、声明回调。

        //定义回调
        private delegate void DisplayInfo(Type info);
        //声明回调
        DisplayInfo displayInfo;

 可以看出,这里定义声明的“回调”(DisplayInfo)其实就是一个委托。

(2)、初始化回调方法。

  displayInfo=new DisplayInfo(Method);

所谓“初始化回调方法”实际上就是实例化刚刚定义了的委托,这里作为参数的Method称为“回调方法”,它封装了对另一个线程中目标对象(窗体控件或其他类)的操作代码。 

(3)、触发对象动作

Opt  obj.Invoke(doSomeCallBack,arg);

其中Opt obj为目标操作对象,在此假设它是某控件,故调用其Invoke方法。Invoke方法签名为:

object  Control.Invoke(Delegate  method,params  object[] args);

它的第一个参数为委托类型,可见“触发对象动作”的本质,就是把委托doSomeCallBack作为参数传递给控件的Invoke方法,这与委托的使用方式是一模一样的。

最终作用于对象Opt obj的代码是置于回调方法体DoSomeMethod()中的,如下所示:

private void DoSomeMethod(type para)

{

     //方法体

    Opt obj.someMethod(para);

}

如果不用回调,而是直接在程序中使用“Opt obj.someMethod(para);”,则当对象Opt obj不在本线程(跨线程访问)时就会发生上面所示的错误。

从以上回调实现的一般过程可知:C#的回调机制,实质上是委托的一种应用。在C#网络编程中,回调的应用是非常普遍的,有了方法回调,就可以在.NET上写出线程安全的代码了。

使用方法回调,实现给文本框赋值:

using System;
using System.Threading;
using System.Windows.Forms;

namespace CrossThreadDemo
{
    public partial class Form1 : Form
    {
        //定义回调
        private delegate void DisplayInfo(string info);
        //声明回调
        private DisplayInfo displayInfo;


        public Form1()
        {
            InitializeComponent();
        }

        //测试按钮
        private void Btn_Test_Click(object sender, EventArgs e)
        {
            //实例化回调
            displayInfo = new DisplayInfo(SetInfo);

            //创建一个线程去执行这个方法(创建的线程默认是前台线程)
            Thread thread = new Thread(new ThreadStart(Test));
            //Start方法标记这个线程已经准备就绪,可以随时执行,具体什么时候执行这个程序由CPU决定
            //将线程设置为后台线程
            thread.IsBackground = true;
            thread.Start();
        }

        //测试方法
        private void Test()
        {
            for (int i = 0; i < 100; i++)
            {
                //使用回调
                this.TextBox_ShowInfo.Invoke(displayInfo,i.ToString());
            }
        }

        //定义回调方法
        private void SetInfo(string info)
        {
            this.TextBox_ShowInfo.Text += info + "\r\n";
        }

    }//Class_end
}

运行结果如下所示:

 

注意:本内容来自https://www.cnblogs.com/dotnet261010/p/6159984.html

猜你喜欢

转载自blog.csdn.net/xiaochenXIHUA/article/details/89365718