线程原理概述
线程基本概念
• 线程是程序执行的基本原子单位. 一个进程可以由多个线程组成.
• 每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。线程上下文包括为使线程在线程的宿主进程地址空间中无缝地继续执行所需的所有信息,包括线程的CPU 寄存器组和堆栈。
• 在分布式编程中,正确使用线程能够很好的提高应用程序的性能及运行效率.实现原理是将一个进程分成多个线程,然后让它们并发异步执行,来提高运行效率.
• 并发执行并不是同时执行(占有CPU),任意时刻还是只能有一个线程占用CPU,只不过是它们争夺CPU频繁一些,感觉到他们似乎都在运行.
进程举例
设一个进程要完成两个任务:任务1和任务2, 并且任务1要经历: A1->B1->C1三个步骤才能完成;任务2要经历:A2->B2->C2三个步骤才能完成。
1. 如果两个任务同步执行的话完成两个任务是这样执行的:
花费时间段: 1 2 3 4 5 6
A1->B1->C1->A2->B2-C2
这样从A1一直到c2只能一个一个地执行. 当A1占用CPU执行时,从B1到C2线程只能在等待.
2. 如果两个任务异步执行的话,完成两个任务是这样执行的:
花费时间段: 1 2 3 4 5 6
A1->B1->C1
A2->B2->C2
这样,任务1和任务2就分成两个独立的执行对象
什么时候用线程?
• 一般情况下,如果多个线程在执行时都要抢占某一个资源或某几个资源,则最好不用异步线程执行.因为它们是并发执行,很可能同时争夺某个资源有CPU,这时要么执行资源分配算法(比如要判断哪个线程优先级高,这要花费时间),或者是按时间片算法(这样要付出轮询CUP/交接/让出CPU所需的时间).
• 如果多个线程所需要的系统资源是比较均匀的,这时完全可以让它们异步并发执行
使用线程的缺点
• 统将为进程和线程所需的上下文信息使用内存。因此,可以创建的进程、AppDomain 对象和线程的数目会受到可用内存的限制。
• 跟踪大量的线程将占用大量的处理器时间。如果线程过多,则其中大多数线程都不会产生明显的进度。如果大多数当前线程处于一个进程中,则其他进程中的线程的调度频率就会很低。
• 使用许多线程控制代码执行非常复杂,并可能产生许多错误。
• 销毁线程需要了解可能发生的问题并对那些问题进行处理。
.NET下的多线程编程
System.Threading
• 提供一些使得可以进行多线程编程的类和接口。此命名空间包括管理线程组的ThreadPool 类、使得可以在指定的时间后调用委托的Timer 类以及用于同步互斥线程的Mutex 类。System.Threading 还提供用于线程调度、等待通知和死锁解析的类。
• using System.Threading;
Thread 类
• a.启动线程:即新建并启动一个线程:
– Thread thread1 = new Thread(new ThreadStart( Count));其中的Count 是将要被新线程执行的函数。
• b.杀死线程
– 在杀死一个线程前最好先判断它是否还活着(通过IsAlive 属性),然后就可以调用Abort 方法来杀死此线程。
• c.暂停线程
– 即让一个正在运行的线程休眠一段时间。如thread.Sleep(1000);就是让线程休眠1秒钟。
• d.优先级
– Thread类中ThreadPriority属性,它用来设置优先级,但不能保证操作系统会接受该优先级。一个线程的优先级可分为5种:
Normal, AboveNormal, BelowNormal, Highest, Lowest。
• e.挂起线程
– Thread类的Suspend方法用来挂起线程,直到调用Resume,此线程才可以继续执行。如果线程已经挂起,那就不会起作用。
• f.恢复线程
– Resume方法用来恢复已经挂起的线程,以让它继续执行,如果线程没挂起,也不会起作用。
Thread
• 一个线程的方法不包含任何参数,同时也不返回任何值。它的命名规则和一般函数的命名规则相同。它既可以是静态的(static)也可以是非静态的(nonstatic)。当它执行完毕后,相应的线程也就结束了,其线程对象的IsAlive属性也就被置为false了。
• .Net的公用语言运行时(CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束
• 一个线程是前台线程还是后台线程可由它的IsBackground属性来决定。
下面我看看多线程的简单应用:
using System;
using System.Threading;
namespace NoParameter
{
class Program
{
static void Main (string[] args)
{
Console.WriteLine("在主线程启动一个新的线程");
Thread th = new Thread(new ThreadStart(ThreadProc));
th.Start();
Thread th2 = new Thread(new ThreadStart(ThreadProc));
th2.Start();
th2.Suspend();
for(int i=0;i<4;i++)
{
Console.WriteLine("主线程输出");
Thread.Sleep(0);
}
Console.WriteLine("线程调用Join方法:");
//继续执行标准的COM、SendMessage消息泵处理期间
//阻止线程调用,直到某个线程中止,或者经过指定时间为止
th.Join();
Console.WriteLine("th线程结束");
th2.Resume();
//如果th2是后台线程,那么主线程不用等待它结束就可以退出
//如果不是后台线程,那么必须等待它结束,主线程才能退出。
th2.IsBackground=false;
}
private static void ThreadProc()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Thread:{0}", i);
Thread.Sleep(0);
}
}
}
}
带参数线程
• 创建线程时,将使用采用ThreadStart 委托作为其唯一参数的构造函数创建Thread 类的新实例。但线程在调用Start 方法前不会开始执行。调用“开始”后,将从由ThreadStart 委托引用的方法的第一行开始执行。
• 当创建ThreadStart 委托时,将标识处理事件的方法。若要使事件处理程序与事件关联,请将该委托的一个实例添加到事件。每当事件出现时就调用事件处理程序,除非移除了该委托。
• 线程还能够调用带参数的方法,即使ThreadStart 委托只带一个参数——表示状态的
对象。正是该对象应将参数传送给被调用方法。
【代码分析】
using System;
using System.Threading;
namespace WithParameter
{
//要实现带参数的线程,我们可以利用一个类的构造函数方法实现
internal class SimpleThread
{
private string _param;
public SimpleThread(string param)
{
this._param = param;
}
public void DoWork()
{
Console.WriteLine("参数:{0}", this._param);
}
}
class Program
{
static void Main (string[] args)
{
SimpleThread st = new SimpleThread("parameter list");
Thread th = new Thread(new ThreadStart(st.DoWork));
th.Start();
th.Join(Timeout.Infinite);
Console.ReadLine();
}
}
}
委托与线程
1. 委托基础
– 委托使用的目标:把函数作为参数传递
– 类似于C++中的函数指针
– 是事件处理的基础
– 函数指针只能引用静态函数,而委托可以引用静态方法和实例方法。当委托引用实例方法时,委托不仅存储对方法入口点的引用,还存储对为其调用该方法的类实例的引用。
– 委托声明:
• delegate double process(double db1);
• delegate 函数返回类型名委托名(函数参数)
【代码分析】
using System;
using System.Threading;
namespace DelegateThread
{
public class SimpleThread
{
public delegate void start(object o);
private class Args
{
public object o;
public start s;
public void work()
{
s(o);
}
}
public static Thread CreateThread(start s, object arg)
{
Args a = new Args();
a.o = arg;
a.s = s;
Thread t = new Thread(new ThreadStart(a.work));
return t;
}
}
public class worker
{
public static void WorkMethod(object o)
{
Console.WriteLine("参数:" + o);
}
}
class Program
{
static void Main (string[] args)
{
Thread th = SimpleThread.CreateThread(new SimpleThread.start(worker.WorkMethod),"nedu");
th.Start();
th.Join(Timeout.Infinite);
Console.ReadLine();
}
}
}
应用程序域
• 操作系统和运行库环境通常会在应用程序间提供某种形式的隔离。为确保在一个应用程序中运行的代码不会对其他不相关的应用程序产生不良影响,这种隔离是必需的。
• .Net中新增了一个隔离层,称为应用程序域AppDomain,它是进程内部一个逻辑独立部分。
• 应用程序域提供安全而通用的处理单元,公共语言运行库可使用它来提供应用程序之间的隔离。在一个应用程序中出现的错误不会影响其他应用程序。
• 使用应用程序域:
– 能够在不停止整个进程的情况下停止单个应用程序。
– 在一个应用程序中运行的代码不能直接访问其他应用程序中的代码或资源。
– 代码行为的作用范围由它运行所在的应用程序决定。
– 向代码授予的权限可以由代码运行所在的应用程序域来控制。
应用程序域和线程
• 线程是公共语言运行库用来执行代码的操作系统构造。在运行时,所有托管代码均加载到一个应用程序域中,由特定的操作系统线程来运行。
• 应用程序域和线程之间不具有一对一的相关性。在任意给定时间,在单个应用程序域中可以执行几个线程,而且特定线程并不局限在单个应用程序域内。也就是说,线程可以自由跨越应用程序域边界;不为每个应用程序域创建新线程。
• 在任意给定时间,每一线程都在一个应用程序域中执行。运行库会跟踪在哪些应用程序域中有哪些线程正在运行。通过调用Thread.GetDomain方法,您可以随时确定线程执行所在的域
【代码分析】
临界区操作
• Lock关键字:将某个语句块标记为临界区,另一个线程不进入临界区。
• Interlocked 类:为多个线程共享的变量提供原子操作。Increment 和Decrement 方法递增或递减变量并将结果值存储在单个操作中。
• Monitor 类:提供同步对对象的访问的机制。Monitor 类通过向单个线程授予对象锁来控制对对象的访问。使用Enter 和Exit 方法标记临界区的开头和结尾。
异步编程
同步处理
• 同步化操作:由前后紧接的组件或函数调用组成。一个同步化调用会阻塞整个进程直到这一个操作完成。
异步处理
• 异步化操作:不会阻塞启动操作的调用线程。调用程序必须通过轮流检测、软件中的中断信号或只是明确地等待完成信号来发现调用的完成。
异步委托
• 异步委托提供以异步方式调用同步方法的能力。
• 当同步调用一个委托时,调用方法直接对当前线程调用目标方法。如果编译器支持异步委托,则它将生成该调用方法以及BeginInvoke 和EndInvoke 方法。
• 如果调用BeginInvoke 方法,则公共语言运行库将对请求进行排队并立即返回到调用方。将对来自线程池的线程调用该目标方法。提交请求的原始线程自由地继续与目标方法并行执行,该目标方法是对线程池线程运行的
• 在回调中,使用EndInvoke 方法来获取返回值和输入/输出参数。如果没有对BeginInvoke 指定回调,则可以在提交请求的原始线程上使用EndInvoke