线程在多核时代的优势越来越明显,掌握多线程编程就尤为重要。但越来越多的人陷入线程的泥潭,最后搞得自己面目全非。越来越多的死锁,越来越多的异常数据,在并发性测试中让一个个线程程序员焦头烂额。自己在自己的编程环境下怎么都没事,单步调试也不会有任何错误,到了两个人,多个人测试的时候怎么就不行了呢?线程,同步与锁的问题渐渐的凸现在了每个程序员的面前。
可以通过开启线程将主线程中容易造成卡顿的程序交给其他的大脑进行工作!!!
一个应用程序开始运行,那么就会存在一个属于这个应用程序的进程。
进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
也即是说:进程是指在系统中正在运行的一个应用程序。
每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行【程序执行靠线程】,一个进程中至少有一个线程。一个进程中可以有多个线程的,这时这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
什么是多线程呢?即就是一个程序中有多个线程在同时执行。
线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。
通过下图来区别单线程程序与多线程程序的不同:
单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如,去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。
多线程程序:即,若有多个任务可以同时执行。如,去网吧上网,网吧能够让多个人同时上网。
或者好比厨师炒菜,如果只炒一锅菜,就是单线程;如果同时炒多口锅的菜,厨师就需要在多口锅之间切换,这就是多线程。
多线程的优点:
(1)可以同时完成多个任务;能适当提高程序的执行效率
(2)能适当提高资源利用率(CPU、内存利用率)
那么可能有人会问只是适当提高,为什么要去使用多线程执行呢?总结起来有下面两方面的原因:
(1)CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,从宏观角度来说是多线程并发的,因为CPU速度太快,察觉不到,看起来是同一时刻执行了不同的操作。但是从微观角度来讲,同一时刻只能有一个线程在处理。
(2)目前电脑都是多核CPU的,一个CPU在同一时刻只能运行一个线程,但是多核CPU在同一时刻就可以运行多个线程。
**
可以这样说:如果是单核CPU,没有所谓的多线程。
**
多线程的缺点:
(1)线程也是程序,所以线程需要占用内存(默认情况下,主线程占用1M,子线程占用512KB),线程越多,占用内存也越多。
(2)多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程。
(3)线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。
(4)线程太多会导致控制太复杂,最终可能造成程序缺陷。
程序执行靠线程。
1、分时调度
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
2、抢占式调度
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。(随机性非常重要)
抢占式调度详解:
大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用VS软件,一边使用录屏软件,同时还开着画图板,Word文档等软件。此时,这些程序是在同时运行,感觉这些软件好像在同一时刻运行着。
实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。也可以比喻一个厨师炒很多锅的菜,厨师在快速的切换着。
对于CPU的一个核而言,某个时刻,只能执行一个线程,而CPU在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
但是多核CPU就可以真正做到同时执行多个线程。
3、前台线程与后台线程
.Net的公用语言运行时(Common Language Runtime,CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
在进程中,只要有一个前台线程未退出,进程就不会终止。主线程就是一个前台线程。而后台线程不管线程是否结束,只要所有的前台线程都退出(包括正常退出和异常退出)后,进程就会自动终止。一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。
在C#中,线程是使用Thread类处理的,该类在System.Threading命名空间中。使用Thread类创建线程时,只需要提供线程入口,线程入口告诉程序让这个线程做什么(即提供一个方法)。通过 Thread类 可以控制当前应用程序域中线程的创建、挂起、停止、销毁。
1、Thread类 一些常用属性:
2、Thread 的优先级:
例1【01Thread】:传入ThreadStart类型参数,静态方法
ThreadStart 是一个接收无参数无返回值方法的委托,所以参数需要一个无参数无返回值的方法。(转到定义可看)
//Main方法由CLR创建的主线程来执行,这个主线程是一个前台线程
Thread thread = new Thread(new ThreadStart(() => { while (true) { Thread.Sleep(2000); Console.WriteLine("Hello World!"); } } )); thread.Start(); Console.ReadKey();
是为了建立程序单独的执行路径,让多部分代码实现同时执行——并发执行。
如果为了简单,也可以通过匿名方法或Lambda表达式来为Thread的构造方法赋值。
class Program { static void Main(string[] args) { Thread thread = new Thread(new ThreadStart(() => { while (true) { Thread.Sleep(2000); Console.WriteLine("Hello World!"); } } )); Thread thread1 = new Thread(test.ThreadWay); thread.Start(); thread1.Start(); Console.ReadKey(); } } public static class test { public static void ThreadWay() { Console.WriteLine("我是第二个线程"); } }
例3【03Thread】:运行通过匿名方法和Lambada表达式创建的方法
上面的例子中
Thread thread1 = new Thread(delegate() { test.ThreadWay(); });
//通过Lambda表达式创建 Thread thread2 = new Thread(() => Console.WriteLine("我是通过Lambda表达式创建的方法"));
后面会单独加上一个章节总结委托的妙处
class Program { static void Main(string[] args) { Console.WriteLine("hello world!"); Thread thread = new Thread(Worker); thread.Start(); thread.IsBackground = true; Console.WriteLine("从主线程退出"); //Console.ReadKey(); } static void Worker() { Thread.Sleep(1000); Console.WriteLine("后台线程退出"); } }
首先通过Thread类创建了一个线程对象,然后通过IsBackground属性指明该线程为后台线程,启动了后台线程,主线程将会继续执行。主线程运行完毕之后就会中止后台线程,然后使整个程序结束运行。所以Worker不会执行。但是最后有Console.ReadKey();后台线程也会执行,因为Console.ReadKey()就是在等待,只要超过1秒即后台线程就会执行Worker。
输出结果是:
hello world!
从主线程退出
如何使子线程的代码执行呢?
修改方案如下:
(1)第一种是将创建的线程设置为非后台线程(前台线程),只需要注释backThread.IsBackground = true即可;
其实不设置IsBackground属性,默认为前台线程。只有前台程序全部执行完才会退出程序。所以子线程的代码会被执行,但是这个时候一执行完马上就退出程序了。
输出结果是:
hello world!
从主线程退出
从后台线程退出
(2)另一种方式就是使主线程在后台线程执行完毕之后再执行,即使主线程也进入睡眠,且使睡眠时间比后台线程更长,修改后的代码如下。
static void Main(string[] args) { Console.WriteLine("hello world!"); Thread thread = new Thread(Worker); thread.Start(); thread.IsBackground = true; Thread.Sleep(2000);//主线程睡眠比子线程时间长 Console.WriteLine("从主线程退出"); //Console.ReadKey(); } static void Worker() { Thread.Sleep(1000); Console.WriteLine("后台线程退出"); }
输出结果是:
hello world!
从后台线程退出
从主线程退出
(3)此外还可以使用函数Join来实现,确保主线程会在后台线程结束后才开始运行。修改之后的代码如下:
static void Main(string[] args) { Console.WriteLine("hello world!"); Thread thread = new Thread(Worker); thread.Start(); thread.IsBackground = true; thread.Join();//执行这句话的线程(主线程)会等待后台线程结束之后才能继续执行 Console.WriteLine("从主线程退出"); //Console.ReadKey(); } static void Worker() { Thread.Sleep(4000); Console.WriteLine("后台线程退出"); }
输出结果为
使用Join的时候,主线程会等待后台线程结束之后才能继续执行。另外Join方法还有一个参数,表示等待多长时间。如果设置了时间,等了这个时间还没执行完就不会再等了。比如这里设置为500,那么就可能等不了子线程的执行了。
//thread1.Priority = ThreadPriority.Highest;
设置线程的优先级别,实际上只是建议操作系统这么设置,操作系统是否真的这么设置不一定。比如操作系统内部本身的一些线程的优先级别总是最高的,你设置为最高,操作系统一般就不认可的
//thread1.Join();//等待thread1线程执行完成,还有个重载可以设置等待时间
//thread1.Abort();//不得已情况下才使用,直接终结线程。正常退出线程是对应的方法执行完毕,调用Abort()直接剁掉,拦腰斩断
前面线程执行的方法是无参数无返回值的,如果方法有参数呢?就需要使用ParameterizedThreadStart类型。
例6:【06Thread】
static void Main(string[] args) { //Thread thread = new Thread(new ParameterizedThreadStart(Worker1)); Thread thread = new Thread(Worker); thread.Start("123"); Console.WriteLine("主程序退出"); } static void Worker(object data) { Thread.Sleep(1000); Console.WriteLine("传入的参数为:" + data.ToString()) ; Console.WriteLine("后台程序退出"); }
输出的结果为:
说明:ParameterizedThreadStart委托的参数类型必须是Object的。如何把参数传递给子线程的方法呢?
——通过线程对象的Start方法传入参数,如thread.Start(“123”);,此时参数“123”就会传递给子线程要执行的方法的参数。
**注意:**如果使用的是不带参数的委托(即是使用ThreadStart类型构建的线程),不能使用带参数的Start方法运行线程,否则系统会抛出异常。但使用带参数的委托,可以使用thread.Start()来运行线程,这时所传递的参数值为null。
但是就上面例6,子线程执行的方法用到了参数(data.tostring()),那么也会报错,因为null不能tostring()。
思考:如何传递多个参数呢?
——由于参数类型为Object,所以可以传入一个集合或者数组
static void Main(string[] args) { List<int> vs = new List<int>() {1,2,3 }; Thread thread = new Thread(Worker); thread.Start(vs); Console.WriteLine("主程序退出"); } static void Worker(object data) { Thread.Sleep(1000); List<int> vs = (List<int>)data; foreach (var item in vs) { Console.WriteLine("传入的参数为:" + item.ToString()); } Console.WriteLine("后台程序退出"); }
使用 ThreadStart 和 ParameterizedThreadStart 创建线程还是比较简单的,但是由于线程的创建和销毁需要耗费一定的开销,过多的使用线程反而会造成内存资源的浪费,从而影响性能,出于对性能的考虑,于是引入了线程池的概念。线程池并不是在 CLR 初始化的时候立刻创建线程的,而是在应用程序要创建线程来执行任务的时候,线程池才会初始化一个线程,初始化的线程和其他线程一样,但是在线程完成任务之后不会自行销毁,而是以挂起的状态回到线程池。当应用程序再次向线程池发出请求的时候,线程池里挂起的线程会再度激活执行任务。这样做可以减少线程创建和销毁所带来的开销。线程池建立的线程默认为后台线程。每个线程要消耗1M内存。
简单说:线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。线程池非常适合大量小的运算。当应用程序想要执行一个异步操作时,需要调用QueueUserWorkItem方法将对应的任务添加到线程池中。线程池会从队列中提取任务,并且将其委派给线程池中的线程执行。
下面通过实例学习线程池的基本应用。
例7:【07ThreadPool】测试下通过创建线程与利用线程池线程来做事所需时间及观察下利用线程池时是不是只用到少数几个线程
static void Main(string[] args) { Stopwatch sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 1000; i++) { Thread thread = new Thread(()=> { int count = 0; count++; }); thread.Start(); } sw.Stop(); Console.WriteLine("运行创建线程所花费时间为"+sw.ElapsedMilliseconds); sw.Restart(); for (int i = 0; i < 1000; i++) { ThreadPool.QueueUserWorkItem((s)=> { int count = 0; Console.WriteLine(Thread.CurrentThread.ManagedThreadId); count++; }); } sw.Stop(); Console.WriteLine("运行创建线程所花费时间为"+sw.ElapsedMilliseconds); Console.ReadKey(); }
运行的结果为
可以看到通过创建线程循环的输出远远大于利用线程池里线程循环输出所需时间,同时利用线程池的循环只用到了少数几个线程。
从前面的例子知道要使用多个线程工作时利用线程池工作效率高很多,只用到少数几个线程。接下来对ThreadPool.QueueUserWorkItem方法来深入理解。
例8【08ThreadPoolDemo1】: QueueUserWorkItem方法参数详解(2个重载)
static void Main(string[] args) { Console.WriteLine("主线程ID={0}",Thread.CurrentThread.ManagedThreadId); ThreadPool.QueueUserWorkItem(CallBackWorkItem); //第一个参数为线程池线程执行的回调方法,第二个参数表示传递给回调方法的参数state ThreadPool.QueueUserWorkItem(CallBackWorkItem,"work"); Thread.Sleep(3000); Console.WriteLine("主线程退出"); Console.ReadKey(); } static void CallBackWorkItem(object state) { Console.WriteLine("子线程执行"); if(state!=null) { Console.WriteLine("ID={0}:{1}", Thread.CurrentThread.ManagedThreadId,state.ToString()); } else { Console.WriteLine("ID={0}", Thread.CurrentThread.ManagedThreadId); } }
例9【09ThreadPoolDemo2】:执行非静态方法、参数为自定义类型
class Program { static void Main(string[] args) { ThreadDemoClass demoClass = new ThreadDemoClass();//实例化类的对象 //使用委托绑定线程池要执行的方法(无参数) WaitCallback waitCallback1 = demoClass.Run1; //将方法排入队列,在线程池变为可用时执行 ThreadPool.QueueUserWorkItem(waitCallback1); //使用委托绑定线程池要执行的方法(有参数) WaitCallback waitCallback2 = new WaitCallback(demoClass.Run1); //将方法排入队列,在线程池变为可用时执行 ThreadPool.QueueUserWorkItem(waitCallback2, "张三"); UserInfo userInfo = new UserInfo(); userInfo.Name = "李四"; userInfo.Age = 33; //使用委托绑定线程池要执行的方法(有参数,自定义类型的参数) WaitCallback waitCallback3 = new WaitCallback(demoClass.Run2); //将方法排入队列,在线程池变为可用时执行 ThreadPool.QueueUserWorkItem(waitCallback3, userInfo); Console.WriteLine(); Console.WriteLine("Main thread working..."); Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); Console.ReadKey(); } } public class ThreadDemoClass { public void Run1(object obj) { string name = obj as string; Console.WriteLine(); Console.WriteLine("Child thread working..."); Console.WriteLine("My name is " + name); Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); } public void Run2(object obj) { UserInfo userInfo = (UserInfo)obj; Console.WriteLine(); Console.WriteLine("Child thread working..."); Console.WriteLine("My name is " + userInfo.Name); Console.WriteLine("I'm " + userInfo.Age + " years old this year"); Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); } } public class UserInfo { public string Name { get; set; } public int Age { get; set; } }
使用线程池建立的线程也可以选择传递参数或不传递参数,并且参数也可以是值类型或引用类型(包括自定义类型)。看上面的结果发现了什么?没错,第一次执行的方法的线程ID为5,后面几次执行的方法的线程ID也为5。这就说明第一次请求线程池的时候,线程池建立了一个线程,当它执行完成之后就以挂起状态回到了线程池,在后面几次请求的时候,再次唤醒了该线程执行任务。这样就很容易理解了。
提醒:多运行测试看看结果如何?
在这里我还发现了一个问题,就是,每次运行的时候,输出的内容的顺序都不一定是一样的。(不只是线程池,前面的线程也是)这就是非线程的安全问题了(下一节讲)。让我举个栗子形容一下的话,就像以前在学校下课了去吃饭一样,一拥而上,毫无秩序。也就是CPU为谁服务就执行谁(哪个线程)。
非线程安全是指多线程操作同一个对象可能会出现问题。而线程安全则是多线程操作同一个对象不会有问题。
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
其实普通的多线程确实很简单,但是一个安全的高效的多线程却不那么简单。所以很多时候不正确的使用多线程反倒会影响程序的性能。下面先看一个例子 。
例10【10ThreadAnquan】非线程安全问题
static int num = 1; static void Main(string[] args) { Stopwatch stopWatch = new Stopwatch(); //开始计时 stopWatch.Start(); //ThreadStart threadStart = new ThreadStart(Run); for (int i = 0; i < 5; i++) { Thread thread = new Thread(Run); thread.Start(); } num++; Console.WriteLine("num is:" + num); Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); //停止计时 stopWatch.Stop(); //输出执行的时间,毫秒数 Console.WriteLine("The execution time is " + stopWatch.ElapsedMilliseconds + " milliseconds."); Console.ReadKey(); } public static void Run() { num++; Console.WriteLine("num is:" + num); Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); }
从上面可以看出变量 num 的值不是连续递增的,输出也是没有顺序的,而且每次输出的值都是不一样的,这是因为异步线程同时访问一个成员时造成的,所以这样的多线程对于我们来说是不可控的。以上这个例子就是非线程安全的,那么要做到线程安全就需要用到线程同步。线程同步有很多种方法,比如之前用到过的 Join() 方法,它也可以实现线程的同步。下面我们来试试:
static int num = 1; static void Main(string[] args) { Stopwatch stopWatch = new Stopwatch(); //开始计时 stopWatch.Start(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(Run); thread.Start(); thread.Join(); } num++; Console.WriteLine("num is:" + num); Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); //停止计时 stopWatch.Stop(); //输出执行的时间,毫秒数 Console.WriteLine("The execution time is " + stopWatch.ElapsedMilliseconds + " milliseconds."); Console.ReadKey(); } public static void Run() { num++; Console.WriteLine("num is:" + num); Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); }
这样就实现了简单的同步,相比起上面的代码也就只是添加了一行代码(thread.Join();),之前也提到了 Join() 这个方法用于阻止当前线程,直到前面的线程执行完成。可是这样虽然是实现了同步,但是却也阻塞了主线程的继续执行,这样和单线程貌似没什么区别了。那么有没有其他方法呢?——线程同步技术。
线程同步技术是指多线程程序中,为了保证后者线程,只有等待前者线程完成之后才能继续执行。就好比买票,前面的人没买到票之前,后面的人必须等待。所谓同步:是指在某一时刻只有一个线程可以访问变量。如果不能确保对变量的访问是同步的,就会产生错误。c#为同步访问变量提供了一个非常简单的方式,即使用c#语言的关键字Lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在c#中,关键字Lock定义如下:
Lock(expression) { statement_block }
expression代表你希望跟踪的对象。
statement_block就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。
这也就是实现线程同步锁机制。
【例11:11ThreadLock】
class Program { private object locker = new object(); static int num = 1; static void Main(string[] args) { Program program = new Program(); Stopwatch stopWatch = new Stopwatch(); //开始计时 stopWatch.Start(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(program.Run); thread.Start(); } num++; Console.WriteLine("num is:" + num); Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); //停止计时 stopWatch.Stop(); //输出执行的时间,毫秒数 Console.WriteLine("The execution time is " + stopWatch.ElapsedMilliseconds + " milliseconds."); Console.ReadKey(); } public void Run() { lock (locker) { num++; Console.WriteLine("num is:" + num); Console.WriteLine("Child thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); } } }
锁的执行过程:假设线程A先执行,线程B稍微慢一点。线程A执行到lock语句,判断locker是否已申请了互斥锁,判断依据是逐个与已存在的锁进行object.ReferenceEquals比较,如果不存在,则申请一个新的互斥锁,这时线程A进入lock里面了。
这时假设线程B启动了,而线程A还未执行完lock里面的代码。线程B执行到lock语句,检查到locker已经申请了互斥锁,于是等待;直到线程A执行完毕,释放互斥锁,线程B才能申请新的互斥锁并执行lock里面的代码。
lock 是一种比较好用的简单的线程同步方式,它是通过为给定对象获取互斥锁来实现同步的。可以看到这种方式的确没有阻塞主线程,而且成员变量的值也是连续递增的,说明是线程安全的。lock 锁机制表示在同一时刻只有一个线程可以锁定同步对象(在这里是locker),任何竞争的其它线程都将被阻止,直到这个锁被释放。
lock使用注意事项:
1、lock 的参数必须是基于引用类型的对象,不要是基本类型,比如 bool、int,这样根本不能同步,原因是lock的参数要求是对象,如果传入 int,势必要发生装箱操作,这样每次lock的都将是一个新的不同的对象。
2、最好避免使用public类型或不受程序控制的对象实例,因为这样很可能导致死锁。
3、最好不要锁字符串;使用lock同步时,应保证lock的是同一个对象,而给字符串变量赋值并不是修改它,而是重新创建了新的对象,这样多个线程以及每个循环之间所lock的对象都不同,因此达不到同步的效果。
4、常用做法是创建一个object对象,并且永不赋值。应lock一个不影响其他操作的私有对象。
例12【12ThreadTongbu】以书店卖书为例:
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 BookShop { //剩余图书数量 public int num = 1; public void Sale() { int tmp = num; if (tmp > 0)//判断是否有书,如果有就可以卖 { Thread.Sleep(1000); num -= 1; Console.WriteLine("售出一本图书,还剩余{0}本", num); } else { Console.WriteLine("没有了"); } } }
从运行结果可以看出,两个线程同步访问共享资源,没有考虑同步的问题,结果不正确(因为1线程还没运行完,2线程就进来了,这是tmp还是大于0的,所以都是执行了第一个分支)。
考虑线程同步,改进后的代码:保证在同一个时刻只有一个线程访问共享资源,以及保证前面的线程执行完成之后,后面的才会访问资源。
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 BookShop { private object locker = new object(); //剩余图书数量 public int num = 1; public void Sale() { //lock (this) lock (locker) { int tmp = num; if (tmp > 0)//判断是否有书,如果有就可以卖 { Thread.Sleep(1000); num -= 1; Console.WriteLine("售出一本图书,还剩余{0}本", num); } else { Console.WriteLine("没有了"); } } } }
lock(this)这种锁定用于锁定一个实例对象。只有基于当前实例对象的线程才会被同步,这里可以实现,但一般不建议这种。【这里锁定的实例对象是book】
lock(type)这种锁定用于锁定类型.只要线程调用方法时,没有获取该种类型的锁,则会被阻塞,一般不建议这种。
点击“测试”,创建一个线程,从0循环到10000给文本框赋值,代码如下:
例13【13ThreadCross】
private void Button1_Click(object sender, EventArgs e)
{
//创建一个线程去执行这个方法:创建的线程默认是前台线程
Thread thread = new Thread(new ThreadStart(Test));
//将线程设置为后台线程
thread.IsBackground = true;
//Start方法标记这个线程就绪了,可以随时被执行,具体什么时候行这个线程,由CPU决定
thread.Start();
}
private void Test()
{
for (int i = 0; i< 10000; i++)
{
this.textBox1.Text = i.ToString();
}
}
产生错误的原因:textBox1是由主线程创建的,thread线程是另外创建的一个线程,在.NET上执行的是托管代码,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。
解决方案:
1、在窗体的加载事件中,将C#内置控件(Control)类的CheckForIllegalCrossThreadCalls属性设置为false,屏蔽掉C#编译器对跨线程调用的检查。
private void Form1_Load(object sender, EventArgs e)
{
//取消跨线程的访问检查
Control.CheckForIllegalCrossThreadCalls = false;
}
使用上述的方法虽然可以保证程序正常运行并实现应用的功能,但是在实际的软件开发中,做如此设置是不安全的(不符合.NET的安全规范),在产品软件的开发中,此类情况是不允许的。如果要在遵守.NET安全标准的前提下,实现从一个线程成功地访问另一个线程创建的控件,可以使用C#的方法回调机制。
2、使用回调函数
什么叫回调函数呢?比如,你调用了一个函数,那么就叫调用,但是如果你在调用一个函数的时候,还需要把一个函数提供该函数,让这个函数来调用你的函数,那么你提供的这个函数就被称为回调函数(callback)
C#的方法回调函数(机制),也是建立在委托基础上的。
改进例13:使用方法回调,实现给文本框赋值:
//定义回调 private delegate void setTextValueCallBack(int value); //声明回调 private setTextValueCallBack setCallBack; private void Button1_Click(object sender, EventArgs e) { //实例化回调 setCallBack = SetValue; //创建一个线程去执行这个方法:创建的线程默认是前台线程 Thread thread = new Thread(Test); //Start方法标记这个线程就绪了,可以随时被执行,具体什么时执这个线程,由CPU决定 //将线程设置为后台线程 thread.IsBackground = true; thread.Start(); } /// <summary> /// 定义回调使用的方法。它封装了对另一个线程中目标对象(窗体控件或其他类)的操作代码 /// </summary> /// <param name="value"></param> private void SetValue(int value) { this.textBox1.Text = value.ToString(); } private void Test() { for (int i = 0; i < 10000; i++) { //textBox1.Invoke拥有此控件的基础窗口句柄的线程上执行指定的委托。 //使用回调。主线程执行setCallBack委托 //也就是说,Invoke()是一个方法,这方法执行的是委托,并且是在一个固定的线程上执行的。 //什么样的 线程? 拥有此控件的基础窗口句柄的线程。 // 这个“拥有此控件的基础窗口句柄的线程”实际上就是 “主线程”, //窗体加载时 程序会创建一个 “主线程”。 if (textBox1.InvokeRequired)//如果为跨线程访问 { textBox1.Invoke(setCallBack, i);//i就是传给回调函数的值 } } }
补充:Control.InvokeRequired
这个属性来进行判断是否是调用方对该控件进行调用控件,如果不是创建这个控件的线程来调用它,则返回true(即是跨线程访问就为true),否则返回Fale。
以上代码可以简化,只需要下面2个方法即可,重点是黄色底纹代码,利用内置委托Action,传入一个参数,内置委托即是回调函数,i是传给委托/回调函数的参数:
【例14】14ThreadCallBack——回调函数运用简写方法
private void Test() { for (int i = 0; i < 10000; i++) { if (textBox1.InvokeRequired) { textBox1.Invoke(new Action<int>(n =>{ this.textBox1.Text = n.ToString(); }),i); } } } private void Button1_Click(object sender, EventArgs e) { Thread th = new Thread(Test); th.IsBackground = true; th.Start(); }
从以上回调实现的一般过程可知:C#的回调机制,实质上是委托的一种应用。
例15【15Delegate】——委托的执行
static void Main(string[] args) { //定义一个委托,并初始化 Func<int, int, string> delFunc = (a, b) => (a + b).ToString();//黄色底纹部分换成{ return (a + b).ToString(); }更好理解 //同步方法调用(跟调用一个方法一样),即是主线程执行这个委托 string str = delFunc(1, 2);//以往做法 Console.WriteLine(str); Console.ReadKey(); }
例16【16AsyncThread】
static void Main(string[] args) { Console.WriteLine("主线程id:"+Thread.CurrentThread.ManagedThreadId); //定义一个委托,并初始化 Func<int, int, string> delFunc = (a, b) => { //由于下面执行这个委托时使用了BeginInvoke方法,所以就为开启一个新线程去执行,所以称为异步线程 Console.WriteLine("异步线程id:" + Thread.CurrentThread.ManagedThreadId); //Thread.Sleep(3000); return (a + b).ToString(); }; //同步方法调用,即是主线程执行这个委托 //string str = delFunc(1, 2); //BeginInvoke() 方法用于异步委托的执行开始。有返回值,返回值为IAsyncResult,并不是执行委托方法的返回值 //BeginInvoke() 是可以接受多个参数的,它的参数个数和参数类型取决于定义委托时的参数个数和类型,无论它有多少个参数,最后两个参数都是不变的。 倒数第二个参数为回调函数,暂用null, //最后一个参数是给回调函数传入参数的参数,暂用null //IAsyncResult result = delFunc.BeginInvoke(1, 2, null, null); delFunc.BeginInvoke(1, 2, null, null); //接下来我们看看是不是开启一个新的线程来执行这个委托,因此在最上面先打印出主线程 Console.WriteLine("主线程id:"+Thread.CurrentThread.ManagedThreadId);,改进下委托的初始化 Console.WriteLine(); Console.ReadKey(); }
从上面的执行结果就可以看出,是两个不同的线程在执行。所以称之为异步委托。也就是说使用委托的BeginInvoke方法,本质就是使用了一个线程池的线程去执行委托指向的方法。不是用主线程去执行。
如何拿到异步委托的结果呢?
例17【17AsyncThread】
//如何拿到异步委托的结果呢,即返回值,这也就是利用异步委托的优势哦,因为手动写线程只能执行没有返回值的委托(ThreadStart、ParameterizedThreadStart),定义如下:
public delegate void ThreadStart()
public delegate void ParameterizedThreadStart(object obj)
要执行有返回值的委托,就需要使用异步委托执行
//1.先拿到BeginInvoke方法的返回值result IAsyncResult result = delFunc.BeginInvoke(1, 2, null, null); //result.IsCompleted通过这个可以判断异步委托是否执行完成,执行完成返回true //if (!result.IsCompleted) //{ //异步委托没有执行完成做点其他事情 //} 没有执行完,主线程就一直执行下面的循环体 //while (!result.IsCompleted) //{ // Thread.Sleep(100); // Console.WriteLine("Main thread working..."); // Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); // Console.WriteLine(); //} //2.调用委托的EndInvoke方法,把BeginInvoke方法的返回值result传入,即可拿到委托方法的执行结果 string str = delFunc.EndInvoke(result); Console.WriteLine(str);
IAsyncResult.IsCompleted 用于监视异步委托的执行状态(true / false),这里的时间是不定的,也就是说一定要等到异步委托执行完成之后,这个属性才会返回 true。通过一个循环,委托方法没有执行完成之前,让主线程做点其他事情,如下:
while (!result.IsCompleted) { Thread.Sleep(100); Console.WriteLine("Main thread working..."); Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); Console.WriteLine(); }
这样,如果异步委托的方法耗时较长,那么主线程会一直工作(循环执行)下去。
在委托方法中加上Thread.Sleep(3000);语句,上面绿色底纹所示,再次运行发现运行结果要等待一会才显示出来。这说明EndInvoke()方法会阻塞当前线程,直到异步委托方法执行完成之后,才能继续往下执行。
BeginInvoke()方法无法转到定义查看,因为它是编译生成时自动生成的方法, 是可以接受多个参数的,它的参数个数和参数类型取决于定义委托时的参数个数和类型,不过无论它有多少个参数,最后两个参数都是不变的。
上面是简单方式的异步委托。
还可以用下面的方法 WaitOne(),自定义一个等待的时间,如果在这个等待时间内异步委托没有执行完成,那么就会执行 while 里面的主线程的逻辑,反之就不会执行。
while (!result.AsyncWaitHandle.WaitOne(1000))//等1秒,1秒后异步委托没有执行完成才执行循环体 { Thread.Sleep(100); Console.WriteLine("Main thread working..."); Console.WriteLine("Main thread ID is:" + Thread.CurrentThread.ManagedThreadId.ToString()); Console.WriteLine(); }
例18【18AsyncThread】
#region 有回调函数的异步委托 // //BeginInvoke方法倒数第二个参数为回调函数, //最后一个参数传给回调函数的参数 //AsyncCallback //第3个参数为AsyncCallback类型,转到定义发现也是一个委托,如下 //public delegate void AsyncCallback(IAsyncResult ar); //据此我们就可以根据该委托定义一个回调函数,见下 delFunc.BeginInvoke(1, 2, MyAsyncCallback,"123"); #endregion ublic static void MyAsyncCallback(IAsyncResult ar) { Console.WriteLine("执行回调函数线程的ID:"+Thread.CurrentThread.ManagedThreadId); }
从结果可以看出,异步线程id与回调函数的ID是同一个。因为执行BeginInvoke方法时到线程池中取出一个有效线程去执行委托delFunc,然后该线程又去执行回调函数。执行过程如下图所示。
//回调函数是干什么用的?什么时候会被执行呢? //回调函数:是异步委托方法执行完成之后,再来调回调函数,也就是说异步委托方法执行之后,还需要处理些事情就可以用回调函数来处理。 //1.在回调函数中如何拿到异步委托执行的结果(前面讲过靠调用委托的EndInvoke方法,即可拿到委托执行的结果,所以关键是如何拿到异步委托) public static void MyAsyncCallback(IAsyncResult ar) { //1.1把回调函数的参数ar强转为实例类型 AsyncResult result = (AsyncResult)ar; //1.2通过实例类型的AsyncDelegate属性拿到异步委托(实际就是delFunc),然后再强转为它的实际类型 var del=(Func<int, int, string>)result.AsyncDelegate; //1.3通过del的EndInvoke方法,参数为ar的实例类型或者就为ar,就可以得到异步委托执行结果 string returnvalue = del.EndInvoke(ar); Console.WriteLine(returnvalue); //2.如何拿到给回调函数的参数:利用ar.AsyncState或result.AsyncState属性 Console.WriteLine("传给回调函数的参数:"+ar.AsyncState); //总结:以上拿到了异步委托的执行结果和传给回调函数的参数,在后续就可以做些其他处理。 当然还有更简单些的方法拿到异步委托执行结果和给回调函数的参数。办法是改造前面的BeginInvoke方法最后一个参数,设置为当前的异步委托(因为最后一个参数为object类型,所以可改为任何类型) //delFunc.BeginInvoke(1, 2, MyAsyncCallback, delFunc); //这样的话ar.AsyncState获取的就是异步委托 var del= (Func<int, int, string>)ar.AsyncState; //接下来要获取返回值只要如下语句即可获取到执行异步委托的返回值 string str= del.EndInvoke(ar); Console.WriteLine(str); Console.WriteLine("回调函数的ID:"+Thread.CurrentThread.ManagedThreadId); }