进程和线程
进程:计算机概念。程序在服务器运行时占据全部计算资源综合。虚拟的
线程:计算机概念。进程在响应操作时的最小单位。也包含CPU内存、网络、硬盘IO,虚拟的概念,
一个进程会包含多个线程。线程隶属于某个进程。进程销毁线程也就没了。
句柄:是一个long数字。是操作系统的表示应用程序
C#里边的多线程,
Thread类是C#语言对线程对象的一个封装
多线程的原因:
1、多个CPU的核可以并行工作
2、4核8线程、这里的线程指的是模拟核
3、CPU分片,1S的处理能力分成1000份。操作哦系统调度去响应不同的任务
从宏观角度来说,感觉就是多个任务在并发执行
从微观角度来说。一个物理CPU同一时刻只能为下一个任务服务
多线程就是资源换性能,1、资源不是无限的;2、资源调度有限制
同步异步
同步:发起调用,完成后才继续下一行:非常符合开发思维、有序执行;
异步:发起调用,不等待完成。直接进入下一行,启动一个新线程去完成
并行并发
并行:多个核之间叫并行
并发:CPU分片的叫并发
什么时候使用多线程
1、改善用户体验。不会出现卡界面的情况
2、并发计算,速度更快
3、任务可以并发的时候,提升速度,优化用户体验
多线程的问题
同步方法有序进行。异步方法是无序的
启动无序。耗时不同。则结束也不同。
那么怎么控制多线程下的启动顺序呢?
1、回调函数:将后续动作通过回调参数传递进去。子线程完成计算后。去调用这个回调委托。
tips:回调参数传参。在Action委托中。回调函数的参数就是IAsyncResult类中的AsyncState这个方法。这个是Object的
public void funcc(){
Action<string> action=this.DoSomething;
IAsyncResult async=null;//对异步操作的描述
AsyncCallback callback= Ar=>{
System.Console.WriteLine(object.ReferenceEquals(Ar,async));
System.Console.WriteLine("计算成功"+Thread.CurrentThread.ManagedThreadId.ToString("00")+Ar.AsyncState);
};
async=action.BeginInvoke("funcc",callback,"haha");
}
public interface IAsyncResult
{
[__DynamicallyInvokableAttribute]
bool IsCompleted { get; }//是否完成
[__DynamicallyInvokableAttribute]
WaitHandle AsyncWaitHandle { get; }
[__DynamicallyInvokableAttribute]
object AsyncState { get; }//回调的函数
[__DynamicallyInvokableAttribute]
bool CompletedSynchronously { get; }
}
2、使用IsCompleted 去实现顺序。
3、WaitOne等待。即时等待
async.AsyncWaitHandle.WaitOne();//直接等待任务完成
async.AsyncWaitHandle.WaitOne(-1);//一直等待任务完成
async.AsyncWaitHandle.WaitOne(1000);//最多等待1000ms,超时就不等了
4、EndInvoke等待,保证顺序,即可以等待。而且可以获取到返回值
5、task.ContinueWith也能保证顺序
Thread
1.0时代的产物
ThreadStart method=()=>this.Do;
Thread th=new Thread(Method);
th.start();//开启线程
th.Suspend();//暂停
th.Resume();//继续
th.Abort();//销毁
//线程时计算机资源。程序向停下来。但是只能向操作系统通知(线程抛异常)
//会有延迟,且不一定停下来
//以上三个快被抛弃了
th.sleep(1000);
th.join();//运行这句代码的线程。等待thread的完成
th.Priority=ThreadPriority.Highest//优先执行。但是并不是优先完成,不靠谱。默认是normal
public enum ThreadPriority
{
Lowest = 0,
BelowNormal = 1,
Normal = 2,
AboveNormal = 3,
Highest = 4
}
th.IsBackground=false//默认false。如果为true,关闭界面的时候。线程也就关闭了。
//false的话就是一个前台线程。页面关闭后,任务执行完成后才会退出。
但是Thread功能太过于强大。反而不太好用
其二。线程的数量没有限制,
ThreadPool
2.0诞生的
如果某个对象的创建和销毁代价比较高。而且可以反复的使用。我们就需要一个池子保存多个这样的对象。需要用的时候从池子里边获取。用完之后不用销毁而是放回池子里。降低了开销(享元模式思想)
此外还能管控总数量。防止滥用。
委托的异步调用。Task–Parrael–async/await,全部都是线程池的数量,然后线程池是全局的。所以。你自己随便设置会影响到其他的功能。
线程池里边有work现成和complication现成。可以设置它的最大最小值。
在设置最大值的时候需要比当前CPU的核数大。否则设置无效。
ThreadPool.QueueUserWorkItem(o=>this.DoSomething);
ThreadPool.QueueUserWorkItem(o=>this.DoSomething,"gogo");
ThreadPool.SetMaxThreads(2,5);
ThreadPool.GetMaxThreads(out int workT,out int comt);
System.Console.WriteLine(workT,comt);
线程池的等待ManualResetEvent
ManualResetEvent manualReset=new ManualResetEvent(false);
//false--对应着关闭,--Set可以打开。waitone就能通过
//如果直接是true就是直接打开。那么直接可以waitone。也可以reset去关闭它
ThreadPool.QueueUserWorkItem(o=>
{
this.DoSomething;
manualReset.Set();
});
System.Console.WriteLine("111111111");
manualReset.WaitOne();
System.Console.WriteLine("2222222222222");
AutoResetEvent
同样也可以做线程的等待。和ManualResetEvent相同用法
Semaphore
信号量
public Semaphore(
int initialCount,
int maximumCount
)
参数
initialCount
Type: System.Int32
可以同时授予的信号量的初始请求数。
maximumCount
Type: System.Int32
可以同时授予的信号量的最大请求数。
static Semaphore sema = new Semaphore(1,1);
声明一个信号量,指示控制的资源初始和最大线程并发数为1
static Semaphore sema = new Semaphore(1,1);
sema.WaitOne();
for (int i = 0; i < 3; i++)
{
Console.WriteLine($"ThreadName:{ Thread.CurrentThread.Name} i:{i}");
Thread.Sleep(1000);
}
sema.Release();
使用以上两个方法控制资源,某个线程执行sema.WaitOne()方法时,若有其他线程已经占用资源,此方法将阻塞,直到,其他线程释放,即调用sema.Release();方法
Task
多线程的最佳实现。3.0出现的,基于线程池。提供了丰富的API
三种启动方式
任何一个多线程启动的时候都离不开委托
{
Task task=new Task(()=>DoSomething);
task.start();
Task task1=Task.Run(()=>this.DoSomething);
TaskFactory factory=Task.Factory;
Task task2=factory.StartNew(()=>this.DoSomething);
}
Thread.Sleep(2000);//同步等待
Task task3=Task.Delay(2000).ContinueWith(task3=>{});//异步等待
delay的本质就是一个Timer。在任务中去等待。sleep会导致阻塞。delay则不会。
Task.WaitAll(Task[])
则可以在多线程中去阻塞当前线程。来等待Task[]完成之后再进行
比如一个网站要加载多个数据源的数据。我们可以异步进行。主线程等待全部加载完成再返回给页面。
Task.WaitAny(Task[])
则可以在多线程中去阻塞当前线程。来等待Task[]任意一个完成之后再进行
多个相同的数据源多线程并发请求。哪个先完成就用哪个。
不过这两个方法都会一定程度的阻塞线程。我们可以采用TaskFactory的
taskFactory.ContinueWhenAny()/taskFactory.ContinueWhenAll()的这两个方法。都是非阻塞的回调,而且使用的线程可能是新的 线程。也可能是刚完成任务的线程。唯一不可能的就是主线程。
控制Task的并发数量?
我们可以查看task的Status是否在RanToCompletion 状态,如果数量大于一个值则waitany一下等待完成后释放线程池内一个资源再继续下去
public enum TaskStatus
{
Created = 0,
WaitingForActivation = 1,
WaitingToRun = 2,
Running = 3,
WaitingForChildrenToComplete = 4,
RanToCompletion = 5,
Canceled = 6,
Faulted = 7
}
Parallel
Parallel.Invoke(()=>this.DoSomething,
()=>this.DoSomething,
()=>this.DoSomething,
()=>this.DoSomething
);
Parallel会把主线程也参与进来。等于TaskWaitAll+主线程
ParallelOptions options=new ParallelOptions();
options.MaxDegreeOfParallelism=3;
Parallel.For(0,10,options,o=>this.DoSomething);
Parallel可以通过ParallelOptions 的MaxDegreeOfParallelism属性来实现控制线程的数量
Await Async
4.5的产物 属于一个语法糖。由编译器提供相关的功能
Await Async 一般是成对出现的,await在方法体内。则这个方法必须是async/task的。
主线程调用 Await Async方法,主线程遇到await返回执行后续动作
await后面的代码会等着task任务的完成后再继续执行
有点类似task.ContinueWith()的方法
然后回调动作可能是Task线程。也可能是新的线程。也有可能是主线程
async的方法。没有返回值。可以直接写一个返回值Task
Await Async能够使用同步的方式去写代码。但又是非阻塞的
Await Async 原理
Async在编译之后会生成一个状态机(实现了IAsyncStateNachine接口)
状态机:初始化状态为0
执行就修改状态为1
再执行就修改为0 参考红绿灯
Async中的状态机
Async方法里面的逻辑其实都在movenext里边-主线程new一个状态机状态是-1
执行了await之前的东西。到了await时开始一个新的Task线程。然后把主线程状态机修改为0,回到主线程继续去肝自己的事儿,子线程呢再去movenext,状态机又回归了-1,再执行后续的逻辑
如果还有await则可以继续循环。当整个出现错误的时候。状态机就会变成-2抛出异常
多线程综合问题
多线程获取随机数。如何去重?
1、数据隔离。
2、操作一个已存在数字的List,每次进行一次去重,不过需要用一个lock去锁住
private static readonly object SSQ_Lock=new objecct();
lock(SSQ_Lock){
XXXX;
}
多线程中捕获异常
如果直接使用多线程,一般来说不要在线程中抛出异常,而是在线程内部代码中使用try/catch代码块!
其他两种方法:
- 1.新建一个List把所有的线程包裹起来。 然后Task.WailAll(Tash.toArray)来等待所有异常结束。。此时try catch则可以捕获到异常
- 2多线程的异常。catch(AggregateException)也可以获取到异常。、里边的InnerException是一个ReadOnlyCollection
catch(AggregateException aex){
foreach(var exception in aex.InnerException)
cw(exception.message);
}
线程取消
多线程并发的时候。有一个错误了。就全部停下来。因此需要线程取消。
可以通过CancellationTokenSource的IsCancellationRequesrd属性来断开。通过Cancel方法实现线程状态修改,通过cts.Token来实现线程取消
CancellationTokenSource cts=new CancellationTokenSource ();
Task.Run(()=>
{
try{
if(cts.IsCancellationRequesrd){
cw("成功");}
else
cw("失败0");
}
catch(except e){
cts.Cancel();
}
},cts.Token);
多线程中常用的锁
我现在知道C#中的加锁有三种方式:
1.Lock:一般的锁
2.Monitor.Enter()/Monitor.Exit():最优秀的锁
3.InterLocked:自旋锁。
1. 描述线程与进程的区别?
一个应用程序实例是一个进程,一个进程内包含一个或多个线程,线程是进程的一部分;
进程之间是相互独立的,他们有各自的私有内存空间和资源,进程内的线程可以共享其所属进程的所有资源;
2. 为什么GUI不支持跨线程访问控件?一般如何解决这个问题?
因为GUI应用程序引入了一个特殊的线程处理模型,为了保证UI控件的线程安全,这个线程处理模型不允许其他子线程跨线程访问UI元素。解决方法还是比较多的,如:
利用UI控件提供的方法,Winform是控件的Invoke方法,WPF中是控件的Dispatcher.Invoke方法;
使用BackgroundWorker;
使用GUI线程处理模型的同步上下文SynchronizationContext来提交UI更新操作
上面几个方式在文中已详细给出。
3. 简述后台线程和前台线程的区别?
应用程序必须运行完所有的前台线程才可以退出,或者主动结束前台线程,不管后台线程是否还在运行,应用程序都会结束;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
通过将 Thread.IsBackground 设置为 true,就可以将线程指定为后台线程,主线程就是一个前台线程。
4. 说说常用的锁,lock是一种什么样的锁?
常用的如如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,lock是一个混合锁,其实质是Monitor['mɒnɪtə]。
5. lock为什么要锁定一个参数,可不可锁定一个值类型?这个参数有什么要求?
lock的锁对象要求为一个引用类型。她可以锁定值类型,但值类型会被装箱,每次装箱后的对象都不一样,会导致锁定无效。
对于lock锁,锁定的这个对象参数才是关键,这个参数的同步索引块指针会指向一个真正的锁(同步块),这个锁(同步块)会被复用。
6. 多线程和异步有什么关系和区别?
多线程是实现异步的主要方式之一,异步并不等同于多线程。实现异步的方式还有很多,比如利用硬件的特性、使用进程或纤程等。在.NET中就有很多的异步编程支持,比如很多地方都有Begin***、End***的方法,就是一种异步编程支持,她内部有些是利用多线程,有些是利用硬件的特性来实现的异步编程。
7. 线程池的优点有哪些?又有哪些不足?
优点:减小线程创建和销毁的开销,可以复用线程;也从而减少了线程上下文切换的性能损失;在GC回收时,较少的线程更有利于GC的回收效率。
缺点:线程池无法对一个线程有更多的精确的控制,如了解其运行状态等;不能设置线程的优先级;加入到线程池的任务(方法)不能有返回值;对于需要长期运行的任务就不适合线程池。
8. Mutex和lock有何不同?一般用哪一个作为锁使用更好?
Mutex是一个基于内核模式的互斥锁,支持锁的递归调用,而Lock是一个混合锁,一般建议使用Lock更好,因为lock的性能更好。
小总结
c#的多线程发展历程
Thread 1.0版本的宠儿,功能强大。但是太过复杂,而且需要不断的创建和销毁线程,导致了大量的cpu资源浪费。多线程的上下文切换也非常消耗资源
ThreadPool 2.0诞生。采用了池化思想。消除了不断销毁进程的问题。但是存在一个无法获取线程内返回值的问题
Task和Parallel 3.0版本来了。底层还是采用的ThreadPool的思想。不过进行了封装。而且通过委托的方式能够得到返回值。有很多自己的方法。waitall,continuewith等,
Async 用同步的方式去写异步的方法。内部是由一个状态机去实现的。线程遇到await的时候。就会返回到主线程去继续工作。等task完成之后。才会继续运行await后面的内容。
用户模式构造
基元用户模式比基元内核模式速度要快,她使用特殊的cpu指令来协调线程,在硬件中发生,速度很快。但也因此Windows操作系统永远检测不到一个线程在一个用户模式构造上阻塞了。举个例子来模拟一下用户模式构造的同步方式:
线程1请求了临界资源,并在资源门口使用了用户模式构造的锁;
线程2请求临界资源时,发现有锁,因此就在门口等待,并不停的去询问资源是否可用;
线程1如果使用资源时间较长,则线程2会一直运行,并且占用CPU时间。占用CPU干什么呢?她会不停的轮询锁的状态,直到资源可用,这就是所谓的活锁;
缺点有没有发现?线程2会一直使用CPU时间(假如当前系统只有这两个线程在运行),也就意味着不仅浪费了CPU时间,而且还会有频繁的线程上下文切换,对性能影响是很严重的。
当然她的优点是效率高,适合哪种对资源占用时间很短的线程同步。.NET中为我们提供了两种原子性操作,利用原子操作可以实现一些简单的用户模式锁(如自旋锁)。
System.Threading.Interlocked:易失构造,它在包含一个简单数据类型的变量上执行原子性的读或写操作。
Thread.VolatileRead 和 Thread.VolatileWrite:互锁构造,它在包含一个简单数据类型的变量上执行原子性的读和写操作。
以上两种原子性操作的具体内涵这里就细说了(有兴趣可以去研究文末给出的参考书籍或资料),
int a = 0;
System.Threading.Tasks.Parallel.For(0, 100000, (i) =>
{
a++;
});
Console.Write(a);
上面代码是通过并行(多线程)来更新共享变量a的值,结果肯定是小于等于100000的,具体多少是不稳定的。解决方法,可以使用我们常用的Lock,还有更有效的就是使用System.Threading.Interlocked提供的原子性操作,保证对a的值操作每一次都是原子性的:
System.Threading.Interlocked.Add(ref a, 1);//正确
内核模式构造
这是针对用户模式的一个补充,先模拟一个内核模式构造的同步流程来理解她的工作方式:
线程1请求了临界资源,并在资源门口使用了内核模式构造的锁;
线程2请求临界资源时,发现有锁,就会被系统要求睡眠(阻塞),线程2就不会被执行了,也就不会浪费CPU和线程上下文切换了;
等待线程1使用完资源后,解锁后会发送一个通知,然后操作系统会把线程2唤醒。假如有多个线程在临界资源门口等待,则会挑选一个唤醒;
看上去是不是非常棒!彻底解决了用户模式构造的缺点,但内核模式也有缺点的:将线程从用户模式切换到内核模式(或相反)导致巨大性能损失。调用线程将从托管代码转换为内核代码,再转回来,会浪费大量CPU时间,同时还伴随着线程上下文切换,因此尽量不要让线程从用户模式转到内核模式。
她的优点就是阻塞线程,不浪费CPU时间,适合那种需要长时间占用资源的线程同步。
内核模式构造的主要有两种方式,以及基于这两种方式的常见的锁:
基于事件:如AutoResetEvent、ManualResetEvent
基于信号量:如Semaphore
混合线程同步
既然内核模式和用户模式都有优缺点,混合构造就是把两者结合,充分利用两者的优点,把性能损失降到最低。大概的思路很好理解,就是如果是在没有资源竞争,或线程使用资源的时间很短,就是用用户模式构造同步,否则就升级到内核模式构造同步,其中最典型的代表就是Lock了。
常用的混合锁还不少呢!如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockSlim,这些锁各有特点和锁使用的场景。这里主要就使用最多的lock来详细了解下。
lock的本质就是使用的Monitor,lock只是一种简化的语法形式,实质的语法形式如下:
bool lockTaken = false;
try
{
Monitor.Enter(obj, ref lockTaken);
//...
}
finally
{
if (lockTaken) Monitor.Exit(obj);
}