隐藏

C#教程之Threads(异步和多线程)

发布:2019/10/15 21:48:05作者:管理员 来源:本站 浏览次数:1102

Task是.NET Framework3.0出现的,线程是基于线程池的,然后提供丰富的api,Thread方法很多很强大,但是太过强大,没有限制。

DoSomethingLong方法如下:

复制代码
 /// <summary> /// 一个比较耗时耗资源的私有方法 /// </summary> /// <param name="name"></param> private void DoSomethingLong(string name)
 {
     Console.WriteLine($"****************DoSomethingLong Start  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); long lResult = 0; for (int i = 0; i < 1_000_000_000; i++)
     {
         lResult += i;
     }
     Thread.Sleep(2000);

     Console.WriteLine($"****************DoSomethingLong   End  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
 }
复制代码

Task的使用:

复制代码
{
    Task task = new Task(() => this.DoSomethingLong("btnTask_Click_1"));
    task.Start();
}
{
    Task task = Task.Run(() => this.DoSomethingLong("btnTask_Click_2"));
}
{
    TaskFactory taskFactory = Task.Factory;
    Task task = taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_3"));
}
复制代码

 

 

 如果这样去调用:

复制代码
ThreadPool.SetMaxThreads(8, 8); for (int i = 0; i < 100; i++)
{ int k = i;
    Task.Run(() => {
        Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(2000);
    });
}
复制代码

 

 

如果去掉设置最大线程的代码:

复制代码
for (int i = 0; i < 100; i++)
{ int k = i;
    Task.Run(() => {
        Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(2000);
    });
}
复制代码

运行结果如下:

 

 

 

 ThreadPool.SetMaxThreads(8, 8);

线程池是单例的,全局唯一的,设置后,同时并发的Task只有8个,而且是复用的,Task的线程是源于线程池的,全局的,请不要这样设置。

假如我想控制下Task的并发数量,改怎么做?

复制代码
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("在Sleep之前");
    Thread.Sleep(2000);//同步等待--当前线程等待2s 然后继续 Console.WriteLine("在Sleep之后");
    stopwatch.Stop();
    Console.WriteLine($"Sleep耗时{stopwatch.ElapsedMilliseconds}");
}
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("在Delay之前");
    Task task = Task.Delay(2000)
        .ContinueWith(t => {
            stopwatch.Stop();
            Console.WriteLine($"Delay耗时{stopwatch.ElapsedMilliseconds}");

            Console.WriteLine($"This is ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        });//异步等待--等待2s后启动新任务 Console.WriteLine("在Delay之后");
    stopwatch.Stop();
    Console.WriteLine($"Delay耗时{stopwatch.ElapsedMilliseconds}");
}
复制代码

运行结果如下:

 

 如果将最后一个stopwatch注释掉:

 

复制代码
 {
     Stopwatch stopwatch = new Stopwatch();
     stopwatch.Start();
     Console.WriteLine("在Sleep之前");
     Thread.Sleep(2000);//同步等待--当前线程等待2s 然后继续 Console.WriteLine("在Sleep之后");
     stopwatch.Stop();
     Console.WriteLine($"Sleep耗时{stopwatch.ElapsedMilliseconds}");
 }
 {
     Stopwatch stopwatch = new Stopwatch();
     stopwatch.Start();
     Console.WriteLine("在Delay之前");
     Task task = Task.Delay(2000)
         .ContinueWith(t => {
             stopwatch.Stop();
             Console.WriteLine($"Delay耗时{stopwatch.ElapsedMilliseconds}");

             Console.WriteLine($"This is ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
         });//异步等待--等待2s后启动新任务 Console.WriteLine("在Delay之后"); //stopwatch.Stop(); //Console.WriteLine($"Delay耗时{stopwatch.ElapsedMilliseconds}"); }
复制代码

 

 

 

什么时候用多线程?

任务并发是时候

多线程能干嘛?

提升速度,优化用户体验。

 

比如,现在有一个场景,在公司开会,领导在分配任务,不能并发,因为只能有一个领导在讲话分配任务,当任务分配下去,开发们确实可以同时开始撸代码,这个是可以并发的。

复制代码
 TaskFactory taskFactory = new TaskFactory();
 List<Task> taskList = new List<Task>();
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle1", "Portal")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle2", " DBA ")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle3", "Client")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle4", "BackService")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle5", "Wechat")));
复制代码

 

 现在要求,谁第一个完成,获得红包奖励(ContinueWhenAny);所有完成后,一起庆祝下(ContinueWhenAll),将其放入一个List<Task>里面去

复制代码
 TaskFactory taskFactory = new TaskFactory();
 List<Task> taskList = new List<Task>();
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle1", "Portal")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle2", " DBA ")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle3", "Client")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle4", "BackService")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle5", "Wechat"))); //谁第一个完成,获取一个红包奖励 taskFactory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine($"XXX开发完成,获取个红包奖励{Thread.CurrentThread.ManagedThreadId.ToString("00")}")); //项目完成后,一起庆祝一下 taskList.Add(taskFactory.ContinueWhenAll(taskList.ToArray(), rArray => Console.WriteLine($"开发都完成,一起庆祝一下{Thread.CurrentThread.ManagedThreadId.ToString("00")}")));
复制代码

ContinueWhenAny  ContinueWhenAll 非阻塞式的回调;而且使用的线程可能是新线程,也可能是刚完成任务的线程,唯一不可能是主线程

复制代码
//阻塞当前线程,等着任意一个任务完成 Task.WaitAny(taskList.ToArray());//也可以限时等待 Console.WriteLine("准备环境开始部署"); //需要能够等待全部线程完成任务再继续  阻塞当前线程,等着全部任务完成 Task.WaitAll(taskList.ToArray());
Console.WriteLine("5个模块全部完成后,集中点评");
复制代码

 

   Task.WaitAny  WaitAll都是阻塞当前线程,等任务完成后执行操作,阻塞卡界面,是为了并发以及顺序控制,网站首页:A数据库 B接口 C分布式服务 D搜索引擎,适合多线程并发,都完成后才能返回给用户,需要等待WaitAll,列表页:核心数据可能来自数据库/接口服务/分布式搜索引擎/缓存,多线程并发请求,哪个先完成就用哪个结果,其他的就不管了。

假如说我想控制下Task的并发数量,该怎么做?  20个

复制代码
 List<Task> taskList = new List<Task>(); for (int i = 0; i < 10000; i++)
 { int k = i; if (taskList.Count(t => t.Status != TaskStatus.RanToCompletion) >= 20)
     {
         Task.WaitAny(taskList.ToArray());
         taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList();
     }
     taskList.Add(Task.Run(() => {
         Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
         Thread.Sleep(2000);
     }));
 }
复制代码

 

Parallel并发执行多个Action线程,主线程会参与计算---阻塞界面。等于TaskWaitAll+主线程计算

 Parallel.Invoke(() => this.DoSomethingLong("btnParallel_Click_1"),
     () => this.DoSomethingLong("btnParallel_Click_2"),
     () => this.DoSomethingLong("btnParallel_Click_3"),
     () => this.DoSomethingLong("btnParallel_Click_4"),
     () => this.DoSomethingLong("btnParallel_Click_5"));
Parallel.For(0, 5, i => this.DoSomethingLong($"btnParallel_Click_{i}"));
Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, i => this.DoSomethingLong($"btnParallel_Click_{i}"));

ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 3;
Parallel.For(0, 10, options, i => this.DoSomethingLong($"btnParallel_Click_{i}"));

有没有办法不阻塞?

复制代码
Task.Run(() => {
    ParallelOptions options = new ParallelOptions();
    options.MaxDegreeOfParallelism = 3;
    Parallel.For(0, 10, options, i => this.DoSomethingLong($"btnParallel_Click_{i}"));
});
复制代码

几乎90%以上的多线程场景,以及顺序控制,以上的Task的方法就可以完成,如果你的多线程场景太复杂搞不定,那么请梳理一下你的流程,简化一下。建议最好不要线程嵌套线程,两三次勉强能懂,三层就hold不住了,更多的只能求神。

 

多线程异常:

复制代码
try {

    List<Task> taskList = new List<Task>(); for (int i = 0; i < 100; i++)
    { string name = $"btnThreadCore_Click_{i}";
        taskList.Add(Task.Run(() => { if (name.Equals("btnThreadCore_Click_11"))
            { throw new Exception("btnThreadCore_Click_11异常");
            } else if (name.Equals("btnThreadCore_Click_12"))
            { throw new Exception("btnThreadCore_Click_12异常");
            } else if (name.Equals("btnThreadCore_Click_38"))
            { throw new Exception("btnThreadCore_Click_38异常");
            }
            Console.WriteLine($"This is {name}成功 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        }));
    } //多线程里面抛出的异常,会终结当前线程;但是不会影响别的线程; //那线程异常哪里去了? 被吞了, //假如我想获取异常信息,还需要通知别的线程 Task.WaitAll(taskList.ToArray());//1 可以捕获到线程的异常 } catch (AggregateException aex)//2 需要try-catch-AggregateException { foreach (var exception in aex.InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
} catch (Exception ex)//可以多catch  先具体再全部 {
    Console.WriteLine(ex);
} //线程异常后经常是需要通知别的线程,而不是等到WaitAll,问题就是要线程取消 //工作中常规建议:多线程的委托里面不允许异常,包一层try-catch,然后记录下来异常信息,完成需要的操作
复制代码

线程取消:

 View Code

临时变量:

复制代码
 for (int i = 0; i < 5; i++)
 {
     Task.Run(() => {
         Console.WriteLine($"This is btnThreadCore_Click_{i} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
     });
 }
复制代码

 

 为什么运行结果后,都是5呢?

临时变量问题,线程是非阻塞的,延迟启动的;线程执行的时候,i已经是5了

那么该如何解决呢?

每次都声明一个变量k去接收,k是闭包里面的变量,每次循环都有一个独立的k,5个k变量  1个i变量

复制代码
for (int i = 0; i < 5; i++)
{ int k = i;
    Task.Run(() => {
        Console.WriteLine($"This is btnThreadCore_Click_{i}_{k} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    });
}
复制代码

这样再运行,结果就正常了。

 

 线程安全&lock:

线程安全:如果你的代码在进程中有多个线程同时运行这一段,如果每次运行的结果都跟单线程运行时的结果一致,那么就是线程安全的

线程安全问题一般都是有全局变量/共享变量/静态变量/硬盘文件/数据库的值,只要多线程都能访问和修改

发生是因为多个线程相同操作,出现了覆盖,怎么解决?

1 Lock解决多线程冲突

Lock是语法糖,Monitor.Enter,占据一个引用,别的线程就只能等着

推荐锁是private static readonly object,

 A不能是Null,可以编译不能运行;

B 不推荐lock(this),外面如果也要用实例,就冲突了

复制代码
//Test test = new Test(); //Task.Delay(1000).ContinueWith(t => //{ // lock (test) // { // Console.WriteLine("*********Start**********"); // Thread.Sleep(5000); // Console.WriteLine("*********End**********"); // } //}); //test.DoTest(); //C 不应该是string; string在内存分配上是重用的,会冲突 //D Lock里面的代码不要太多,这里是单线程的 Test test = new Test(); string student = "水煮鱼";
Task.Delay(1000).ContinueWith(t => { lock (student)
    {
        Console.WriteLine("*********Start**********");
        Thread.Sleep(5000);
        Console.WriteLine("*********End**********");
    }
});
test.DoTestString(); //2 线程安全集合 //System.Collections.Concurrent.ConcurrentQueue<int> //3 数据分拆,避免多线程操作同一个数据;又安全又高效 for (int i = 0; i < 10000; i++)
{ this.iNumSync++;
} for (int i = 0; i < 10000; i++)
{
    Task.Run(() => { lock (Form_Lock)//任意时刻只有一个线程能进入方法块儿,这不就变成了单线程  { this.iNumAsync++;
        }
    });
} for (int i = 0; i < 10000; i++)
{ int