一、线程同步
所谓同步:是指在某一时刻只有一个线程可以访问变量。
如果不能确保对变量的访问是同步的,就会产生错误。
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
}
运行结果如下所示: