涉及的类
- Thread //用于手动创建线程
- ThreadPool //线程池
- System.Threading.CancellationTokenSource //用于取消线程池线程
- Monitor //线程同步
线程(Thread)与进程
当我们打开一个应用程序后,操作系统就会为该应用程序分配一个进程ID。进程可以理解为一块包含了某些资源的内存区域,操作系统通过进程这一方式把它的工作划分为不同的单元。一个应用程序可以对应多个进程(比如应用双开)。
线程与进程之间的关系可以理解为:线程是进程中的独立执行单元,操作系统通过调度线程来使应用程序工作;而进程则是线程的容器,它由操作系统创建,又在具体的执行过程中创建了线程。一个进程中至少包含一个线程,我们把该线程称为主线程。
一个应用程序对应一个或多个进程,每个进程至少对应一个线程。
线程的调度
操作系统为每个线程分配了0~31中的某一级优先级,而且会把优先级高的线程优先分配给CPU去执行。程序可以通过设置Thread的Priority属性来改变线程的优先级,该属性的类型为ThreadPriority枚举类型,其成员包括Lowest、BelowNormal、Normal、AboveNormal和Highest。
在操作系统课程中,老师会介绍说“Windows是抢占式多线程操作系统”。之所以说它是抢占式的,是因为线程可以在任意时间里被抢占,来调度另一个线程。操作系统为每个线程分配了0~31中的某一级优先级,而且会把优先级高的线程优先分配给CPU去执行。
Windows支持7个相对线程优先级:Idle、Lowest、BelowNormal、Normal(默认)、AboveNormal、Highest和Time-Critical。
线程也分前后台
线程有前台线程和后台线程之分。在一个进程中,当所有前台线程停止运行后,CLR会强制结束所有仍在运行的后台进程,这些后台进程会直接被终止,而不会抛出异常。主线程将一直是前台线程。
我们可以使用Thread
类来创建线程(默认为前台线程),该类有三个构造函数,都会使用某委托类型的形参,该形参为线程绑定一个函数。
注意:你在写多线程应用程序的时候,一定要区分前台线程和后台线程,否则很可能会产生疑惑:自己写的代码为什么没有被执行? 还要记得: 通过在主函数中让后台线程调用Join方法,可以保证主线程会在后台线程执行结束后才开始运行。
Thread.CurrentThread.ManagedThreadId
这个属性可用于获取当前的线程ID。
线程池
why、what、how
由来:通过Thread类来手动创建或销毁线程是比较耗费时间的。为避免这样的性能损失,就引入了线程池。
线程池是指用来存放应用程序中要使用的线程集合,这种集中存放的方式有利于对线程进行管理或重复利用。
CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列,当应用程序想要执行一个异步操作时,你需要调用QueueUserWorkItem
方法来将对应的任务添加到线程池的请求队列中。线程池实现的代码会从队列中提取任务,并将其委派给线程池中的线程去执行。
如果线程池中没有空闲的线程,线程池就会创建一个新线程去执行提取的任务。而当线程池线程完成了某个任务后,线程也不会被销毁,而是返回到线程池中,等待响应另一个请求。由于线程不会被销毁,所以也就避免了由此产生的性能损失。由线程池创建的线程是后台线程,且它的默认优先级为Normal。
要使用线程池中的线程,需调用静态方法:ThreadPool.QueueUserWorkItem
,以指定线程要运行的方法。这个静态方法有两个重载的版本。
public static bool QueueUserWorkItem ( WaitCallBack callback ) ;
public static bool QueueUserWorkItem ( WaitCallBack callback,Object state ) ;
这两个方法用于向线程池队列添加一个工作项(work item)以及一个可选的状态数据。然后这两个方法就会立即返回。工作项是指一个由callback参数标识的委托对象,被委托对象包装的回调方法将由线程池线程来执行。传入的回调方法必须匹配System.Threading.WaitCallBack
委托类型,该委托定义如下:
public delegate void WaitCallBack ( Object state ) ;
线程池中的线程
线程池中的线程是由CLR来管理的。线程池使得线程可以充分有效地被使用,减少了任务启动的延迟。但不是所有的情况都适合使用线程池中的线程,注意:
- 任务运行的时间比较短(<250ms),这样CLR可以充分调配现有的空闲线程来处理该任务;
- 耗时长或有阻塞情况的不用线程池中的线程。
要使用线程中的线程,主要有下面两种方式:
// 方式1:Task.Run,.NET Framework 4.5 才有
Task.Run (() => Console.WriteLine ("Hello from the thread pool"));
// 方式2:ThreadPool.QueueUserWorkItem
ThreadPool.QueueUserWorkItem (t => Console.WriteLine ("Hello from the thread pool"));
创建不用线程池中的线程,可以直接通过new Thread()
来创建,也可以通过下面的代码来创建:
Task task = Task.Factory.StartNew (() => ...,TaskCreationOptions.LongRunning);// 注意必须带TaskCreationOptions.LongRunning参数
关于线程的知识很多,这里不再深入了,因为这些已经足够让我们应付Web开发了。
协作式取消线程池线程
直接看代码就懂了
线程同步
线程同步技术是指在多线程程序中,为了保证后者线程在只有等待前者线程完成之后才能继续进行。即在使用多线程技术的场景中,确保某一时刻只有一个线程来操作共享资源。
举例来说,火车票售票系统程序允许多人同时购票,因此该线程肯定采用了多线程技术。但由于系统中有多个线程对同一资源(火车票)进行操作,我们必须确保只有在其他线程执行结束后,新的线程才开始执行。这样可以避免多位顾客买到同一张车票。此时需要使用的就是线程同步技术。
监视器对象(Monitor)能够确保线程拥有对共享资源的互斥访问权。C#通过lock 语句
来提供简化的语法。
我们必须把
Monitor.Exit
函数放在finally语句块中,因为finally语句块中的代码即使在线程退出时也一样会执行。所以在实际的开发过程中,要尽量避免使用线程同步技术,避免使用共享数据(如静态字段等)。全局共享数据,要是只读的倒还行,不用考虑线程同步问题,要是可编辑的,那最好不要用线程同步技术。
除了Monitor对象来实现线程同步,我们还可以使用Interlocked、信号量和Mutex等对象来实现线程同步,它们的使用方式与Monitor类似,大家可以自行参考MSDN,进行试验。
线程安全
这是针对共享数据被并发访问(且有进行更新的可能)时,保证数据访问总是按照先来后到的顺序来处理,避免造成数据修改上的混乱。
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
线程安全问题大多是由全局变量及静态变量引起的,若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,(被访问对象同一时刻只接收一个访问处理,别的等着)否则的话就可能影响线程安全。
并发、同步、异步、锁
并发:某一资源(数据或方法),同时被多个线程访问,这种情况就叫并发。
同步:按一定的顺序做某事,前一件事做完之后,后续的事才能开始。
异步:同时进行多个事项的处理。
锁这个用处,可以想象动车里的卫生间,有人进去后门就立马锁上了,别人进不去,想进去得等待。等上一个人进去办完事儿出来了,卫生间门的锁开了,下一个人才可以进去。
c#常见锁(Lock,也叫排它锁或互斥锁)
互斥锁 lock
语句
作用:将会锁住代码块的内容,并阻止其他线程进入该代码块,直到该代码块运行完成,释放该锁。
注意:
- 定义的锁对象应该是 私有的、静态的、只读的、引用类型的对象,这样可以防止外部改变锁对象 。
- 避免对不同的共享资源使用相同的 lock 对象实例,因为这可能导致死锁或锁争用。
参考:
lock 语句 - 同步对共享资源的线程访问 | Microsoft Learn
互斥锁 Monitor
作用:将会锁住代码块的内容,并阻止其他线程进入该代码块,直到该代码块运行完成,释放该锁。
注意:定义的锁对象应该是 私有的、静态的、只读的、引用类型的对象,这样可以防止外部改变锁对象 。
使用示例:
private static readonly object Lock = new object();
try{
Monitor.Entry(Lock);
//todo ……
}
finally{
Monitor.Exit(Lock);
}
以上代码,等同于lock语句。
读写锁
读写锁允许在有其他程序正在写的情况下读取资源,所以如果资源允许脏读,用这个比较合适。
private static ReaderWriterLockSlim LockSlim = new ReaderWriterLockSlim();
private static int lockSlimInt;
private static void LockSlimIntAdd()
{
for (var i = 0; i < runTimes; i++)
{
LockSlim.EnterWriteLock();
lockSlimInt++;
LockSlim.ExitWriteLock();
}
}
更新于:2023-05-07
标签:同步,Monitor,c#,代码,编程,池中,线程,多线程 From: https://www.cnblogs.com/idasheng/p/17379772.html