C# 多线程

同步和异步的差别

同步单线程方法卡界面——主(UI)线程忙于计算,所以不能响应

异步多线程方法不卡界面——计算任务交予子线程,主线程已经闲置,可以响应别的操作

cs:按钮后不能卡死——上传文件不卡死

bs:用户注册发邮件/发短信

 

同步单线程方法慢——只有一个线程计算

异步多线程方法快——多个线程并发计算

多线程就是用资源换性能,但不是线性增长

多线程的协调管理额外成本——项目经理

资源也有上限——5辆车只有3条道

线程并不是不是越多越好

 

无序性——不可预测性

启动无序:几乎是同一时间向操作系统请求线程,也是个需要CPU处理的请求。因为线程是操作系统资源,CLR 只能去申请,具体什么顺序无法掌控。

执行时间不确定:同一线程同一个任务耗时也可能不同,这更操作系统的调度策略有关,CPU分片(计算能力太强,1s拆成1000份,宏观上就变成了并发的)。所以任务执行过程就得看运气了——线程优先级可影响操作系统的调度

结束无序:以上可得

因为多线程具备不可预测性,很多时候你的你的想法并不一定能够贯彻实施,大多数情况可能是可行的,但总有一定的概率出问题(随着时间的推移,任何小概率事件都会成为必然事件)

电商下订单:增加订单表-日志-发邮件-生成支付-物流通知。多线程,有顺序要求,等待500ms执行下一个动作,顺序测试1000次都没问题,上线也没问题。。。但一个月就会发现总有那么几次顺序错了,而且这是无法重现的。随着用户增加,数据量累计,数据库压力变大,服务器硬件资源不够,会影响执行效率,各种情况都有,所以就错了,而且很难跟进。

使用多线程时,不要通过延时等方式掌控顺序,也不要试图“多模式“掌控顺序。

 

委托 的异步回调控制顺序

AsyncCallback ,回调委托

action.BeginInvoke(”xxx“,AsyncCallback实例,AsyncCallback实例.AsyncState 参数(需要传递的信息参数));

 

IsCompleted判断线程是否完成

 

信号量:

IAsyncResult可以接受action.BeginInvoke(),调用asyncResult.AsyncWaitHandle.WaitOne() ; // 阻塞当前线程,直到收到信号量,从asyncResult发出。WaitOne可以设置等待时间。//做超时控制

 

EndInvoke:

Func<string> func = ()=>DateTime.Now.ToString();

IAsyncResult asyncResult = func.BeginInvoke(null,null);  //异步调用结果,描述异步操作的

string sResult = func.EndInvoke(asyncResult);
//string sResult=func.EndInvoke(func.BeginInvoke(null,null));

 

Thread 和Task的历程

class Program
	{
		static void Main(string[] args)
		{
            #region .NetFramework 1.0  1.1

            //         //.NetFramework  1.0  1.1
            //         ThreadStart threadStart = () =>
            //{
            //	Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Start");
            //	Thread.Sleep(2000);
            //	Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} End");
            //};
            //Thread thread = new Thread(threadStart);
            //thread.Start();
            ////thread.Join();
            ////thread.Suspend();
            ////thread.Resume();
            ////thread.IsBackground=true;
            ////thread.Abort();
            ////Thread.ResetAbort();
            ////Thread 的 API 特别丰富,玩的很花,但很难玩好
            ////——线程是操作系统管理的,所以响应并不是那么灵敏
            ////Thread 启动线程是没有控制的,可能导致死机

            #endregion

            #region .NetFeamework 2.0(新的CLR) ThreadPool
            // 池化资源管理涉及思想:线程是一种资源
            // ,之前每次要调用线程,就去申请一个线程,使用完之后,释放掉;
            // 池化就是做一个容器,容器提前申请那个线程,程序需要使用线程,
            // 直接找容器获取,用完后再放回容器(控制状态),避免频繁的申请
            // 和销毁;容器自己会根据闲置的数量去申请和释放;
            //1 线程复用    2 可以限制最大线程数量(get;set;)
            //API又太少了,线程等待顺序控制特别弱,MRE,影响了实战。
             WaitCallback waitCallback=r=>
            {
                Console.WriteLine($"ThreadPool {Thread.CurrentThread.ManagedThreadId} Start");
                Thread.Sleep(2000);
                Console.WriteLine($"ThreadPool {Thread.CurrentThread.ManagedThreadId} End");
            };
            ThreadPool.QueueUserWorkItem(waitCallback);
            #endregion

            #region .NetFramework 3.0 Task被称之为多线程的最佳实践!
            // 1 Task线程全部是线程池线程  2 提供了丰富的API,非常适合开发实践
            Action action=()=>
            {
                Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} Start");
                Thread.Sleep(2000);
                Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} End");
            };
            Task task = new Task(action);
            task.Start();
            #endregion


            Console.ReadLine();
		}

	}

 

Parallel
{
    // Parallel 可以启动多线程,主线程也参与计算,节约一个线程
    // 可通过给ParallelOptions轻松控制最大并发数量
    // Parallel.f
    Parallel.Invoke(
        () =>
        {
            Console.WriteLine($"Paralle11 {Thread.CurrentThread.ManagedThreadId} Start1");
                Thread.Sleep(2000);
            Console.WriteLine($"Paralle11 {Thread.CurrentThread.ManagedThreadId} End1");
        }, () =>
       {
            Console.WriteLine($"Paralle12 {Thread.CurrentThread.ManagedThreadId} Start2");
            Thread.Sleep(2000);
            Console.WriteLine($"Paralle12 {Thread.CurrentThread.ManagedThreadId} End2");
        }, () =>
        {
            Console.WriteLine($"Paralle13 {Thread.CurrentThread.ManagedThreadId} Start3");
            Thread.Sleep(2000);
            Console.WriteLine($"Paralle13 {Thread.CurrentThread.ManagedThreadId} End3");
        }, () =>
        {
            Console.WriteLine($"Paralle14 {Thread.CurrentThread.ManagedThreadId} Start4");
            Thread.Sleep(2000);
            Console.WriteLine($"Paralle14 {Thread.CurrentThread.ManagedThreadId} End4");
        }
    );
}

 

Task

使用多线程提高效率,建立在任务是彼此之间互不干扰的基础上。

Task.Run( action )    : 创建一个线程并执行

Task.WaitAny( xxx.ToArray() )  :阻塞当前线程,直到任一任务结束——主线程被阻塞,会卡界面

Task.WaitAll( xxx.ToArray() ) :阻塞当前线程,直到所有任务结束——主线程被阻塞

 

尽量不要线程套线程,容易翻车,全部由子线程完成的话,不能直接操作界面

 

TaskFactory Task工厂

taskFactory.ContinueWhenAll( Task[] task, Action<Task[]> continuationAction )  :创建一个延续任务,该任务在一组指定的任务完成后开始

taskFactory.ContinueWhenAny(Task[] task, Action<Task[]> continuationAction)  

continue 的后续线程,可能是新线程,可能是刚完成任务的线程,还可能是同一个线程,就不可能是主线程

线程是不可预测的,所以几个动作先后都有可能

上面四个方法已经可以掌控90%的多线程顺序,灵活运用是重点

 

线程安全

// 这里面的i全部都是5是因为多线程在调用i的时候,i已经循环自增到5了
// k是因为每一次都是全新的k,作用域不一样了
// outofrange 不注意就是常发生的问题
for (int i = 0; i < 5; i++)
{
    int k = i;
    Task.Run(() =>
    {
        Console.WriteLine($" {i} {k} Task {Thread.CurrentThread.ManagedThreadId} Start");
        Thread.Sleep(2000);
        Console.WriteLine($" {i} {k} Task {Thread.CurrentThread.ManagedThreadId} End");
    });
}

 

多线程安全问题:一段代码,单线程执行和多线程执行结果不一致,就表明有线程安全问题

 

            //多线程去访问一个集合一般是没问题的  线程安全问题都是出在修改一个对象上
            List<int> intList = new List<int>();
            for (int i = 0; i < 10000; i++)
            {
                // 多线程之后,结果就变成小于10000——数据丢失了
                Task.Run(() =>      // 覆盖了
                {
                    intList.Add(i);
                });
            }
            Thread.Sleep(5000);
            Console.WriteLine(intList.Count);

 

            #region 解决线程安全问题
            List<int> intList = new List<int>();
            for (int i = 0; i < 10000; i++)
            {
                Task.Run(() =>
                {
                    lock (LOCK)
                    {
                        intList.Add(i);
                    }
                });
            }
            // 加lock 就能解决线程安全问题——就是单线程化
            // ——lock就是保证方法块任意时刻只有一个线程能进去,其他线程就排队
            // ——单线程化
            // lock原理——语法糖——等价于Monitor
            // ——锁定一个内存引用地址——所以不能是值类型——null编译不报错,但也不行
            Thread.Sleep(5000);
            Console.WriteLine(intList.Count);
            #endregion

 

 lock语句是可以在同一线程中递归调用的。VS中运行了很多次发现它并不会发生死锁。在《CLR via C#》第二版(中文版,清华大学出版社出版)的第530页中第7行找到了这样的描述:“同样需要引起注意的是线程可以递归拥有同步块”。即同一线程可以递归调用lock语句。 此处参考:点这里

 

async/await

 await/async:是个新语法,出现C#5.0   .NetFramework在4.5及以上(CLR4.0)
             是一个语法糖,不是一个全新的异步多线程使用方式
            (语法糖就是编译器提供的新功能)
             本身并不会产生新的多线程,但是依托于Task而存在,
             所以程序执行时,也是有多线程的
           
async可以随便添加,可以不用await
await只能出现在task前面,但是方法必须声明async,不能单独出现

await/async 之后,原本没有返回值的,可以返回Task
                       原本返回X类型的,可以返回Task<X>

          一般来说,尽量不要再返回void,因为不能再await

 

可以认为,加了await 就等于将 await 后面的代码,包装成一个回调——其实回调的线程具有多种可能性,回调回来执行的线程不确定

可以用同步编码的形式去写异步

底层是用递归实现的

posted @   xunzf  阅读(169)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示