多线程异步

本文全面解析C#中的多线程概念,包括进程与线程的区别、多线程的发展历程、线程池的优势与不足、Task与Parallel的使用技巧、异步编程的Await/Async语法糖、线程同步机制的深入探讨以及锁的种类与应用。文章旨在帮助开发者掌握多线程编程的核心知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程和线程

进程:计算机概念。程序在服务器运行时占据全部计算资源综合。虚拟的
线程:计算机概念。进程在响应操作时的最小单位。也包含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
catchAggregateException 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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值