19.1 多线程编程知识
19.1.1 进程与线程的概念
进程:
-
可以理解为一块包含某些资源的内存区域,操作系统通过进程方式把它工作划分为不同的单元。
-
一个应用程序可以对应多个进程。
线程:
-
线程是进程中的独立执行单元,操作系统调度线程来使应用程序工作。
-
一个进程中至少包含一个线程,称为主线程。
进程与线程的关系
线程是进程的执行单元,操作系统通过调度线程使应用程序工作。
进程是线程的容器,由操作系统创建,由在具体的执行过程中创建线程。
19.1.2 线程的调度
生活中吃饭的时候看电视,你需要来回切换这两个动作,他们由你来进行调度的。计算机里,线程相当于你的动作,操作系统相当于你,操作系统需要调度线程使他们轮流工作。
Windows是抢占式多线程系统。因为线程可以在任意事件里被抢占,来调度另一个线程。操作系统为每个线程分配了0~31某一优先级,优先级高的优先分配给CPU。
windows 支持7个相对线程优先级,Idle、Lowest、BelowNormal、Normal、AboveNormal、Highest、Time-Critical 。程序可以设置 Thread 的 Priority 属性来改变线程的优先级,该属性为 ThreadPriority 枚举类型,成员包含了 Lowest、BelowNormal、Normal、AboveNormal、Highest,CLR 为自己保留了 Idle、Time-Critical
1、Time-critical:关键时间(最高的相对线程优先级)
2、Heightest:最高(翻译是这么翻译,但是并不是最高的相对线程优先级)
3、Above normal:高于标准
4、Normal:标准
5、Below normal:低于标准
6、Lowest:最低(翻译是这么翻译,但是并不是最低的相对线程优先级)
7、Idle:空闲
19.1.3 线程也分前后台
前台线程:只有所有的前台线程都关闭才能完成程序关闭。(主线程一直是前台线程)
后台线程:只要所有的前台线程都结束,后台线程自动结束(CLR会强制结束所有仍运行的后台线程,却不会抛出异常)。
-
class Program
-
{
-
static void Main(string[] args)
-
{
-
Thread backThread = new Thread(Worker);
-
backThread.IsBackground = true;
-
backThread.Start();
-
Console.WriteLine( "从主线程中退出");
-
}
-
-
public static void Worker()
-
{
-
Thread.Sleep(1000);
-
Console.WriteLine("从后台线程退出");
-
}
-
}
|
CLRT + F5 (不调试)效果图
上面代码通过 Thread 创建一个线程对象,设置 IsBackground 属性指明线程为后台线程。不设置 IsBackground 属性,则所创建的线程默认为前台线程。
接着,调用 Start 函数启动该线程,此时后台线程会执行 Worker 函数代码。
从前面分析中看出,该控制台有两个线程,一个运行 Main 函数的主线程,另一个运行 Worker 函数的后台线程。由于前台执行完毕后 CLR 会无条件终止后台线程运行,所以前面代码中若启动后台线程,则主线程将会继续运行。
主线程运行完 Console.WriteLine( "从主线程中退出") 语句就会退出。此时, CLR 发现主线程运行结束,则会终止后台线程,然后整个程序结束运行。所以 Worker 函数中的 Console.WriteLine("从后台线程退出") 语句将不会执行。
通过分析,按 CLRT + F5 运行程序,你将会看到如图效果。如果按 F5 你将会看到输出结果一闪而过,因为主线程退出后,整个应用程序也跟着退出,就关闭了控制台程序。
分析执行后台线程的方法
1.所创建线程默认为非后台线程,所以注释掉 //backThread.IsBackground = true;(这时候不是后台线程了)
2.使主线程在后台线程执行完毕后再执行,即使主线程进入睡眠,且睡眠时间比后台线程长。
3.通过调用主函数的 Join 函数方法,确保主线程会在后台线程执行结束后开始运行。
以上代码调用 backThread.Start() 确保主线程会在后台线程结束后再运行。这种方式涉及
线程同步的概念:在某些情况下,需要两个线程同步运行。即一个线程必须等待另外一个线程结束之后才能运行。
// System.Threading.ParameterizedThreadStart 委托,它表示此线程开始执行时要调用的方法。
public Thread(ParameterizedThreadStart start);
// System.Threading.ThreadStart 委托,它表示此线程开始执行时要调用的方法。
public Thread(ThreadStart start);
public Thread(ParameterizedThreadStart start, int maxStackSize);
ParameterizedThreadStart 与 ThreadStart 的区别:ParameterizedThreadStart 可以有参数,ThreadStart 没有参数。本实例就是创建的ThreadStart。
19.2 线程的容器——线程池
通过 Thread 类手动创建线程的创建和销毁会耗费大量时间,这样的手动操作将造成性能的损失。因此,.NET引入了线程池机制。
19.2.1 线程池
-
线程池是一个存放线程的地方,这种集中存放有利于线程的管理。
-
CLR初始化时,线程池中没有线程。(线程池内部维护了一个操作请求队列).
-
执行异步操作时,需要调用 QueueUserWorkItem 方法将任务添加到线程池队列。线程池实现的代码会从队列中提取任务,并委派给线程池中的线程执行。
-
没有空闲的线程,线程池会创建一个新线程去执行提取的任务。
-
当线程池完成了某个任务,线程不会销毁,而是返回到线程池中,等待下一个请求。(由于不销毁,所以不会产生性能损失)
19.2.2 通过线程池来实现多线程
使用线程池的线程,要调用静态方法 ThreadPool.QueueUserWorkItem ,以指定线程要调用的方法。该静态方法有两种:
public static bool QueueUserWorkItem(WaitCallback callBack);
public static bool QueueUserWorkItem(WaitCallback callBack, object state);
-
static void Main(string[] args)
-
{
-
Console.WriteLine("主线程ID={0}",Thread.CurrentThread.ManagedThreadId);
-
ThreadPool.QueueUserWorkItem(CallbackWorkItem);
-
ThreadPool.QueueUserWorkItem(CallbackWorkItem, "work");
-
Thread.Sleep(3000);
-
Console.WriteLine("主线程退出");
-
}
-
-
private 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);
-
}
-
}
|
19.2.3 协作式取消线程池线程
.NET Framework 提供了取消操作的模式,这个模式是协作式的。为了取消操作,我们必须创建一个 System.Threading.CancellationTokenSource 对象。
-
static void Main(string[] args)
-
{
-
Console.WriteLine("主线程运行");
-
CancellationTokenSource cts = new CancellationTokenSource();
-
ThreadPool.QueueUserWorkItem(callback, cts.Token);
-
Console.WriteLine("按下回车键来取消操作");
-
Console.Read();
-
cts.Cancel();
-
Console.ReadKey();
-
}
-
-
private static void callback(object state)
-
{
-
CancellationToken token = (CancellationToken) state;
-
Console.WriteLine("开始计数");
-
Count(token,1000);
-
}
-
-
private static void Count(CancellationToken token, int countto)
-
{
-
for (int i = 0; i < countto; i++)
-
{
-
if (token.IsCancellationRequested)
-
{
-
Console.WriteLine("计数取消");
-
return;
-
}
-
Console.WriteLine("计数为:" + i);
-
Thread.Sleep(300);
-
}
-
Console.WriteLine("计数完成");
-
}
|
首先创建一个 CancellationTokenSource 实例,将该实例作为参数传入QueueUserWorkItem 方法。线程池会创建一个线程池线程,运行该方法传入的回调函数 callback ,并在 callback 中执行 Count 函数来计数。
在 Count 函数中检查 CancellationTokenSource 类实例的状态。当用户按回车键时,该实例的 IsCancellationRequested 属性将返回 ture 。因此退出 Count 方法。否则一直运行。
19.3 线程同步
多线程中,为了保证后者线程,只有等待前者线程完成之后才能继续执行。好比排队买票,前面的人没买票之前,后面的人必须等待。
19.3.1 多线程程序中存在的隐患
多线程可以提高程序的性能和用户体验。然而当我们创建多个线程后,它们可能同时访问某一个共享资源,这将损坏资源中所保存的数据。这时候我们需要使用线程同步,确保某一时刻只有一个线程在操作共享资源。
举例来说,火车票销售系统允许多人同时购买,因此该系统肯定采用了多线程技术。但由于系统中有多个线程在对同一资源(火车票)进行操作,我们必须确保只有其他线程执行结束后,新的线程才开始执行。这样可以避免多位顾客买到同一张票。
-
private static int tickets = 100;
-
static void Main(string[] args)
-
{
-
Thread thread1 = new Thread(SaleTicketThread1);
-
Thread thread2 = new Thread(SaleTicketThread2);
-
thread1.Start();
-
thread2.Start();
-
Thread.Sleep(4000);
-
}
-
-
private static void SaleTicketThread1()
-
{
-
while (true)
-
{
-
if (tickets>0)
-
{
-
Console.WriteLine("线程1出票:"+tickets--);
-
}
-
else
-
break;
-
}
-
}
-
private static void SaleTicketThread2()
-
{
-
while (true)
-
{
-
if (tickets > 0)
-
{
-
Console.WriteLine("线程2出票:" + tickets--);
-
}
-
else
-
break;
-
}
-
}
|
线程1从100张票开始出售,执行一段时间后,线程2开始执行,执行一段时间后,线程1又继续执行,两个线程交替,直至售完100张票。
线程1和线程2在售票时,火车票号码不连续,说明以上程序售票过程不正确,这就是多线程所存在的问题,因为两个线程访问同一个全局静态变量——tickets。
19.3.2 使用监视器对象实现线程同步
监视器对象(Monitor)能够确保线程拥有对共享资源的互斥访问权,C# 通过 lock 关键字来提供简化语法。
-
private static int tickets = 100;
-
static object gloalObj =new object();
-
static void Main(string[] args)
-
{
-
Thread thread1 = new Thread(SaleTicketThread1);
-
Thread thread2 = new Thread(SaleTicketThread2);
-
thread1.Start();
-
thread2.Start();
-
Console.ReadKey();
-
}
-
-
private static void SaleTicketThread1()
-
{
-
while (true)
-
{
-
try
-
{
-
Monitor.Enter(gloalObj);
-
Thread.Sleep(1);
-
if (tickets > 0)
-
{
-
Console.WriteLine("线程1出票:" + tickets--);
-
}
-
else
-
break;
-
}
-
finally
-
{
-
Monitor.Exit(gloalObj);
-
}
-
}
-
}
-
private static void SaleTicketThread2()
-
{
-
while (true)
-
{
-
try
-
{
-
Monitor.Enter(gloalObj);
-
Thread.Sleep(1);
-
if (tickets > 0)
-
{
-
Console.WriteLine("线程2出票:" + tickets--);
-
}
-
else
-
break;
-
}
-
finally
-
{
-
Monitor.Exit(gloalObj);
-
}
-
}
-
}
|
使用 Monitor 锁定的对象需要引用类型,而不是值类型。
因为值类型变量传递给方法时,它将被装箱为一个单独的对象,之后传递给 Enter 方法;
而在将变量传递给 Exit 方法时,也会创建一个单独的引用对象。此时,传递给 Enter 方法的对象和传递给 Exit 方法的对象不同, Monitor 将会引发 SynchronizationLockException 异常。
使用 try.....finally 语句执行 Monitor ,如果不使用,程序会出现“死锁现象”;
原因:售出最后一张票后,tickets变量为0,若正在执行的是线程1,此时它们将进入 else 语句块执行 break 语句,线程1直接退出,却没有执行 Monitor.Exit 语句。所以线程2在Monitor.Enter 代码处一直等待,导致线程2不能退出。线程2又是前台线程,使整个程序也不能退出,这时发生“死锁现象”。
19.3.3 线程同步技术存在的问题
-
它的使用比较繁琐。要用额外的代码把被多个线程同时访问的数据保卫起来,并获取和释放线程的同步锁。如果一个代码块忘记获取锁,就可能造成数据损坏。
-
使用线程同步会影响程序性能。因为获取和释放同步锁需要时间,并且决定哪个线程先获取锁,CPU也必须进行协调。
-
线程同步每次只允许一个线程访问资源,会导致线程阻塞。继而,系统会创建更多的线程,CPU也就负担更繁琐的调度工作。
-
static void Main(string[] args)
-
{
-
int x = 0;
-
const int iterationNumber = 5000000;
-
Stopwatch sw = Stopwatch.StartNew();
-
for (int i = 0; i < iterationNumber; i++)
-
{
-
x++;
-
}
-
Console.WriteLine("不使用锁的情况下话费的时间:{0} ms", sw.ElapsedMilliseconds);
-
sw.Restart();
-
-
for (int i = 0; i < iterationNumber; i++) //使用锁的情况
-
{
-
Interlocked.Increment(ref x);
-
}
-
Console.WriteLine("使用锁的情况下话费的时间:{0} ms", sw.ElapsedMilliseconds);
-
Console.Read();
-
}
|
调用 StartNew 函数对 Stopwatch 实例化,将运行时间设为0,开始测量时间。