多线程情况下主要需解决两类问题:1、访问公共数据;2、控制线程执行顺序
一个进程中的多个线程都是可以访问其进程的其他资源,多线程若不加以控制也是并发执行的,若在多线程的执行方法中包含操作全局变量、者静态变量或是使用I/O设备的时候,很容易的就会产生线程安全的问题,从而导致不可预估的错误。
一、普通线程的同步方式:
1、Join(控制顺序)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadTest
{
public static class threadTest1
{
static void Main1()
{
Thread t1 = new Thread(Menthod1);
Thread t2 = new Thread(Menthod2);
t1.Start();
t1.Join();
t2.Start("线程2参数");
t2.Join();
Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
Console.ReadLine();
}
static void Menthod1()
{
Thread.Sleep(2000);
Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
}
static void Menthod2(object obj)
{
Thread.Sleep(4000);
Console.WriteLine("线程2的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("obj:{0}", obj);
Console.WriteLine("--------------------");
}
}
}
2、同步事件AutoResetEvent和 ManualResetEvent(控制顺序)
它们之间唯一不同的地方就是在激活线程之后,状态是否自动由终止变为非终止。AutoResetEvent自动变为非终止,就是说一个AutoResetEvent只能激活一个线程。而ManualResetEvent要等到它的Reset方法被调用,状态才变为非终止,在这之前,ManualResetEvent可以激活任意多个线程。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadTest
{
/// <summary>
/// 同步事件:AutoResetEvent和 ManualResetEvent
/// </summary>
public static class threadTest7
{
public static ManualResetEvent muilReset = new ManualResetEvent(false);
public static void Main7()
{
Thread t1 = new Thread(Menthod1);
t1.Start();
Thread t2 = new Thread(Menthod2);
t2.Start("params");
Thread t3 = new Thread(Menthod3);
t3.Start();
muilReset.WaitOne();
Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
Console.ReadLine();
}
public static void Menthod1()
{
muilReset.WaitOne();
Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
}
public static void Menthod2(object obj)
{
muilReset.WaitOne();
Console.WriteLine("线程2的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("obj:{0}", obj);
Console.WriteLine("--------------------");
}
public static void Menthod3()
{
Thread.Sleep(3000);
Console.WriteLine("线程3的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("激活线程...");
Console.WriteLine("--------------------");
muilReset.Set();
}
}
}
注:ManualResetEvent会给所有引用的线程都发送一个信号(多个线程可以共用一个ManualResetEvent,当ManualResetEvent调用Set()时,所有线程将被唤醒),而AutoResetEvent只会随机给其中一个发送信号(只能唤醒一个)。
这里的线程同步还可以使用委托与事件(推荐使用事件)来实现线程间的简单通讯,比如在某一线程执行到某一结点后,通过事件向另一个或者多个线程发送更多的信息。
3、Monitor和lock(使用公共资源)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadTest
{
public static class threadTest4
{
public static readonly object locker = new object();
public static List<int> lst = new List<int>();
public static void Main4()
{
Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
for (int i = 0; i < 100; i++)
{
Thread t = new Thread(new ThreadStart(Menthod1));
t.Start();
}
Console.ReadLine();
}
public static void Menthod()
{
//Monitor提供了三个静态方法Monitor.Pulse(),Monitor.PulseAll()和Monitor.Wait() ,
//用来实现一种唤醒机制的同步。
Monitor.Enter(locker);
lst.Add(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(lst.Count);
Console.WriteLine(lst[0]);
Console.WriteLine("--------------------");
lst.Clear();
Monitor.Exit(locker);
}
public static void Menthod1()
{
lock (locker)
{
lst.Add(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(lst.Count);
Console.WriteLine(lst[0]);
Console.WriteLine("--------------------");
lst.Clear();
}
}
}
}
4、补充
线程同步的方式还有很多,比如Mutex。还有很多的方法,以后用到的时候在研究吧。
Mutex:Mutex不具备Wait,Pulse,PulseAll的功能,因此,我们不能使用Mutex实现类似的唤醒的功能;不过Mutex有一个比较大的特点,Mutex是跨进程的,因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。
二、线程池的同步方式
使用线程池并发执行任务同样会遇到线程安全的问题,一样需要进行同步,在涉及线程使用公共资源,Monitor、lock等方法与上述线程使用一样,同样能达到理想的效果,就不重复介绍了;但是对于控制执行顺序上,这个没有使用new线程来的自由。
1、使用线程池
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadTest
{
/// <summary>
/// 线程池:默认最大线程数量 = 处理器数 * 250
/// </summary>
public static class threadTest5
{
public static void Main5()
{
//这里QueueUserWorkItem方法需要传入一个QueueUserWorkItem委托(带object类型的参数,
//无返回值),所以我们需要线程执行的任务需要带一个object的参数,
//并且QueueUserWorkItem方法加入时存在一个重载,可以在这里传入一个参数。
ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod1));
ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod2), "asdas");
Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
Console.ReadLine();
}
public static void Menthod1(object obj)
{
Thread.Sleep(2000);
Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
}
public static void Menthod2(object obj)
{
Thread.Sleep(4000);
Console.WriteLine("线程2的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("obj:{0}", obj);
Console.WriteLine("--------------------");
}
}
}
2、同步事件(控制顺序)
在线程池中,没有Join方法,若想控制线程的执行顺序,我推荐使用主线程等待线程池任务执行完毕,阻塞主线程的方式,这里可以使用WaitHandle:
在这里,给线程池每个相关的线程都创建一个AutoResetEvent,在执行完毕之后分别把属于自己的AutoResetEvent变为非终止,WaitHandle使用WaitAll方法阻塞主线程、等待所有的AutoResetEvent事件变为true,另外WaitHandle还有一个WaitAny方法阻塞,不过是只要其中一个线程结束,就会继续运行,不再阻塞。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace ThreadTest
{
/// <summary>
/// 线程池:控制线程执行顺序
/// </summary>
public static class threadTest6
{
//在线程池中,没有Join方法,若想控制线程的执行顺序,
//我推荐使用主线程等待线程池任务执行完毕,阻塞主线程的方式,这里可以使用WaitHandle:
public static void Main6()
{
List<WaitHandle> handles = new List<WaitHandle>();
//在这里,给线程池每个相关的线程都创建一个AutoResetEvent,
//在执行完毕之后分别把属于自己的AutoResetEvent变为非终止,
//WaitHandle使用WaitAll方法阻塞主线程、等待所有的AutoResetEvent事件变为true,
//另外WaitHandle还有一个WaitAny方法阻塞,不过是只要其中一个线程结束,就会继续运行,不再阻塞。
AutoResetEvent autoReset1 = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod1), autoReset1);
handles.Add(autoReset1);
AutoResetEvent autoReset2 = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod2), autoReset2);
handles.Add(autoReset2);
WaitHandle.WaitAll(handles.ToArray());
Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
Console.ReadLine();
}
public static void Menthod1(object obj)
{
//Thread.Sleep(2000);
Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
AutoResetEvent handle = (AutoResetEvent)obj;
handle.Set();
}
public static void Menthod2(object obj)
{
//Thread.Sleep(4000);
Console.WriteLine("线程2的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
AutoResetEvent handle = (AutoResetEvent)obj;
handle.Set();
}
}
}
注:
1、WaitHandle同样可以用于new创建线程的同步事件;
2、WaitHandle等待方法(WaitAll、WaitAny)的数组长度的数目必须少于或等于 64 个,为了解决此限制,有网友封装了一个类,比较好用:
三、Task
Task与线程或者说线程池关系紧密,可以说是基于线程池实现的,虽说任务最终还是要抛给线程去执行,但是Task仍然会比线程、线程池的开销要小,并且提供了可靠的API来控制线任务执行。
使用Task来执行的任务最终会交给线程池来执行,若该任务需要长时间执行,可以将其标记为LongRunning,这是便会单独去请求创建线程来执行该任务。
1、创建Task
Task的创建也存在两种方式,使用new或者使用静态工厂方式来创建:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ThreadTest
{
/// <summary>
/// Task:Task的创建也存在两种方式,使用new或者使用静态工厂方式来创建:
/// </summary>
public static class threadTest2
{
static void Main2()
{
Task t = new Task(Menthod1);
t.Start();
Task.Factory.StartNew(Menthod2);
Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
Console.ReadLine();
}
static void Menthod1()
{
Thread.Sleep(2000);
Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
}
static void Menthod2()
{
Thread.Sleep(4000);
Console.WriteLine("线程2的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
}
}
}
2、参数与返回值
使用Task也可以传入参数(object类型)与返回值:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ThreadTest
{
/// <summary>
/// Task:Task的创建也存在两种方式,使用new或者使用静态工厂方式来创建:
/// </summary>
public static class threadTest3
{
static void Main3()
{
Task.Factory.StartNew(Menthod1, 233);
//获取返回值,会阻塞主线程
Task<object> t = Task.Factory.StartNew(new Func<object, object>(Menthod3), 233);
object result = t.Result;
Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("result:{0}", result);
Console.WriteLine("--------------------");
Console.ReadLine();
}
static void Menthod1(object obj)
{
Thread.Sleep(2000);
Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("obj:{0}", obj);
Console.WriteLine("--------------------");
}
static object Menthod3(object obj)
{
Thread.Sleep(6000);
Console.WriteLine("线程3的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("obj:{0}", obj);
Console.WriteLine("--------------------");
return obj;
}
}
}
3、等待Task
可以使用Task实例的Wait方法来实现等待任务结束,也可以向多线程一样,使用WaitAll和WaitAny一样来等待多个任务结束,不过操作更为简单:
t.Wait();
Task.WaitAll(t1, t2 ...);
Task.WaitAny(t1, t2 ...);
4、取消Task
任务也是可以事先取消的,不过需要使用CancellationTokenSource:
static void Main(string[] args)
{
Task.Factory.StartNew(Menthod1);
CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
Task.Factory.StartNew(Menthod2, cancelTokenSource.Token);
cancelTokenSource.Cancel();
Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
Console.ReadLine();
}
上述代码就可以将线程2给取消掉,当然,Cancel方法可以自己找个合适的地方调用。
5、继续Task
在Task中,可以实现在一个任务结束后开启另一个新的任务:
static void Main(string[] args)
{
Task t = Task.Factory.StartNew(Menthod1);
t.ContinueWith(new Action<Task>(Menthod4));
Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("任务t的ID:{0}", t.Id);
Console.WriteLine("--------------------");
Console.ReadLine();
}
static void Menthod1()
{
Thread.Sleep(2000);
Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("--------------------");
}
static void Menthod4(Task t)
{
Console.WriteLine("线程4的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("任务t的ID:{0}", t.Id);
Console.WriteLine("--------------------");
}
这里因为使用了委托,也可以使用lambda表达式,更简单一些调用:
Task t = Task.Factory.StartNew(Menthod1);
t.ContinueWith(task =>
{
Console.WriteLine("线程4的ID:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("任务t的ID:{0}", task.Id);
Console.WriteLine("--------------------");
});
三、关于线程的知识
1、线程有前台线程和后台线程之分,使用new创建的线程默认为前台线程(可以使用IsBackground属性来进行更改),线程池里面都是后台线程前台线程:
前台线程是不会被立即关闭的,它的关闭只会发生在自己执行完成时,不受外在因素的影响。假如应用程序退出,造成它的前台线程终止,此时CLR仍然保持活动并运行,使应用程序能继续运行,当它的的前台线程都终止后,整个进程才会被销毁。
后台线程:后台线程是可以随时被CLR关闭而不引发异常的,也就是说当后台线程被关闭时,资源的回收是立即的,不等待的,也不考虑后台线程是否执行完成,就算是正在执行中也立即被终止。
2、线程被系统调度到CPU执行时存在优先级:这里的优先级不是优先执行,而是被调度到CPU执行的概率高;使用new创建线程与线程池的优先级默认都是Normal,不过前者可以通过Priority属性来设置优先级。优先级有5个级别:Highest、AboveNormal、Normal、BelowNormal和Lowest。
3、线程存在Suspend与Resume这两个过时的方法,但不是代表不能使用,只是微软不推荐你用,MSDN给出的原因是:请不要使用 Suspend 和 Resume 方法来同步线程活动。 没有办法知道当你暂停执行线程什么代码。 如果在安全权限评估期间持有锁,您挂起线程中的其他线程 AppDomain 可能被阻止。 如果执行类构造函数时,您挂起线程中的其他线程 AppDomain 中尝试使用类被阻止。 可以很容易发生死锁。你可以无视这个警告继续使用这两个方法进行线程同步,若觉得不怎么靠谱,那么可以在线程代码加入判断来保证执行正确性,或者使用控制同步事件(AutoResetEvent等)来实现线程同步。
4、线程池的线程很珍贵,因为数量是有限的,所以不适合执行长时间的作业任务,适合执行短期并且频繁的作业任务,若想执行长时间的作业任务,建议使用new创建新线程的方式。毕竟线程池设计的初衷就是为了解决频繁创建与撤销线程而造成的资源浪费。
5、使用传统线程方式来进行多线程编程的时候,对线程的控制总是不到位,产生一些奇奇怪怪的问题;或是代码写得很杂乱;或是开发人员乱用线程,比方说无限制的创建线程、将线程池线程占满,等等。
Task的出现,实现对传统线程操作的封装,提供可靠高效的API来控制线程,极大的方便多线程编程,所以可以用到Task的地方尽量使用Task;当然,这里仍会产生线程安全的问题,同样需要进行线程同步。