一、进程与线程
进程是指一个程序在计算机上运行时,全部的计算资源的合集; 线程是程序的最小执行单位,包含计算资源,任何一个操作的响应都是线程来完成的;
多线程是指多个线程并发执行; 多线程虽然能够提升程序的运行效率,但是消耗的资源更多,所以线程并不是越多越好。
二、同步和异步
同步和异步都是针对方法而言; 同步方法指的是程序按照顺序执行,必须等待上一操作完成后才能继续往后执行;
异步方法指的是方法在调用之后立即返回,以便程序在该被调用方法返回后调用其它方法执行其他任务。
三、并行、并发、异步、同步的概念区分
说到这些个概念,就不得不提我们的CPU,它是整个计算机的核心,也就相当于人类的大脑。
如此一来,我们可以用现实生活中的情形来类比说明这几个概念:
- 1、并行和并发
并行:英文单词是parallel,意思是平行的。就好比操场跑道有多个赛道,可以同时有多名运动员进行比赛,而不会互相干扰。换做计算机,就是多个逻辑运算单元可以同时进行运算,以达到时间上的充分利用。那么这里有个前提就是多核,单核CPU就无法实现真正意义上的并行了。
并发:英文单词是concurrence,意思是同时发生。这种描述对时间上的要求就不是那么严格,是人为感知意义上的同时。比如你吃饭的时候打电话,你的嘴巴既需要咀嚼,又要说话,表面上你是在同时进行两件事,但实际上它们是交错进行的。于是,并发就可以理解为交错发生。
- 2、异步和同步
这两个概念很容易和日常说法混淆,比如我们说放电影,应当是音画同步进行的,但在计算机术语中这其实是“异步”,指两个线程同时执行任务,相互之间没有阻碍,一方发生阻塞另一方仍能进行。
比如放电影的过程中突然没有画面了,但是声音还是能继续播放的。
以上是对异步的概念解析,那什么是同步呢?
同步实际是顺序执行,可以是单线程,也可以是多线程。
单线程很好理解,因为我们的代码是顺序执行的,在同一线程中会执行完一个方法再执行下一个方法。
而多线程就没那么容易控制,谁先谁后执行我们无法预料,因此要想实现线程间同步,需要我们加以干涉,主动等待线程结束,再继续执行后面的方法。
这里的等待也分两种:一种是阻塞等待,一种是非阻塞等待。
阻塞等待就是等待过程不能做其他事情,如果是UI线程被阻塞,那么界面就会假死,无法进行任何操作;
非阻塞等待就是为了解决等待的过程还能正常进行其他的事情,比如我们界面点击保存需要一个较长时间的等待,如果一直阻塞在那里,则用户体验会很不好。用非阻塞的方式,可以让用户还能做些其他的不影响保存结果的事情,至少能让界面不出现假死现象,好让用户知道后台是在正常运行的。
————————————————
在异步编程中要理解的最重要的事情是控制流如何从一个方法移动到另一个方法。
Thread (线程,.Net Framework 1.0 )
功能最强大,但很多方法已经不建议使用,也是唯一一个可以使用前台线程概念的方法,后续的ThreadPool等都是后台线程
当未指定 thread.IsBackground = true;直接调用thread.Start();则为前台线程
————————————————
{
ThreadStart threadStart = new ThreadStart(() => DoSomething("lucas"));
Thread thread = new Thread(threadStart);
thread.IsBackground = true; //若开启,则为后台线程,UI线程退出后,也跟着退出
thread.Start();//前台线程,UI线程退出后,还会继续执行完
}
{
/*
* Thread控制线程的顺序可以使用下面的方式
*/
thread.Join();//等待子线程执行完主线程再继续执行
while (thread.ThreadState != System.Threading.ThreadState.Running) //判断状态
{
}
#region 结合委托来实现回调
/// <summary>
/// 定义一个基于Thread的回调,与BeginInvoke相似
/// </summary>
/// <param name="threadStart"></param>
/// <param name="callback"></param>
private void CallbackByThread(ThreadStart threadStart, Action callback)
{
ThreadStart startNew = new ThreadStart(() =>
{
threadStart();
callback.Invoke(); //此处会等待threadStart执行完再执行
});
Thread thread = new Thread(startNew);
thread.Start();
}
/// <summary>
/// 定义一个带返回值的回调
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="funcT"></param>
/// <returns></returns>
private Func<T> ReturnByThread<T>(Func<T> funcT)
{
T t = default(T);
ThreadStart startNew = new ThreadStart(() =>
{
t = funcT.Invoke();
});
Thread thread = new Thread(startNew);
thread.Start();
return new Func<T>(() => {
thread.Join(); //等待子线程执行完毕再继续执行
return t;
});
}
{
CallbackByThread(()=>{ Console.WriteLine("这里是新开的线程"); },()=>{ Console.WriteLine("这里是回调方法"); });
Func<string> func = ReturnByThread(()=>{ Console.WriteLine("新开线程执行,后续可以获取返回值"); });
string result = func.Invoke(); //获取到返回值,类似EndInvoke
}
#endregion
}
ThreadPool( .Net Framework 2.0)
线程池,.Net Framework 2.0 出现,目的为了去掉各种复杂的api避免想thread一样的滥用,因为线程是由CLR管理的,同时引入池化的概念,减少线程创建和销毁的成本.
无论是Thread还是ThreadPool,都无法同时满足广大码友既想精准控制每个线程的起承转合,又不想操心线程管理的这种高(tōu)贵(lǎn)需求,于是出现了Task这种堪称ThreadPool的无敌强化版本。
下面来介绍Task的几个强大之处:
1、使用Task的任务类型,可以创建如同Thread的独立线程
在下面的示例当中,通过设置任务类型TaskCreationOptions为LongRunning,实现了创建一个不依赖于线程池的独立线程。
static void Main(string[] args)
{
ThreadPool.SetMaxThreads(8, 8);
Task.Factory.StartNew((o) => { Tasks(o); }, 1, TaskCreationOptions.LongRunning);
Task.Factory.StartNew((o) => { Tasks(o); }, 2, TaskCreationOptions.PreferFairness);
System.Threading.Thread.Sleep(500);
GetActiveThreads();
Console.ReadKey();
}
public static void Tasks(object obj)
{
Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId},传入参数为:{(obj == null ? "null" : obj.ToString())}");
Thread.Sleep(10000);
}
public static void GetActiveThreads()
{
int work, io;
int maxwork, maxio;
ThreadPool.GetAvailableThreads(out work, out io);
ThreadPool.GetMaxThreads(out maxwork, out maxio);
Console.WriteLine($"当前活跃工作线程数:{maxwork - work}");
}
当我们在程序中需要频繁创建线程来执行任务时,每次都new一个新的Thread对象则显得很不爽,且对资源的消耗也是很大的。由此一来,我们可以使用线程池来协助我们创建和管理线程。
特别说明:线程池中创建的线程都是后台线程。
线程池就是存放Thread的大池子,我们需要用到多线程时,只要把过程封装成一个方法丢进去就好了,系统会自动分配当前处于空闲状态的线程来执行该方法,如果没有空闲线程,则会在队列中等待。这一动作,通过ThreadPool.QueueUserWorkItem() 方法来实现。该方法的简单示例如下:
————————————————
{
ThreadPool.QueueUserWorkItem(o => {
DoSomething("lucas");
}, "lucas");
ThreadPool.QueueUserWorkItem(o =>
{
DoSomething("lucas");
// 回调,再包一层
new Action(() => {
Console.WriteLine("这里是回调方法");
}).Invoke();
});
/*
* 线程池的执行顺序可以用 ManualResetEvent 来控制
* 任何时候如果需求不需要等待,则不要随便去设置这个
*/
ManualResetEvent mre = new ManualResetEvent(false); //关闭
ThreadPool.QueueUserWorkItem( o => {
Thread.Sleep(5000);
Console.WriteLine("");
mre.Set(); //此处调用Set之后变成true,WaitOne()才会继续往下执行
});
Console.WriteLine("WaitOne 之前");
mre.WaitOne(); // 需要set之后才会往后执行,如果没有Set,不会往后执行,再Set之后还可以Reset将其关闭,则会继续等待
Console.WriteLine("WaitOne 之后");
}
{
#region PoolSet
ThreadPool.SetMaxThreads(8, 8);
ThreadPool.SetMinThreads(8, 8);
int workerThreads = 0;
int ioThreads = 0;
ThreadPool.GetMaxThreads(out workerThreads, out ioThreads);
Console.WriteLine(String.Format("Max worker threads: {0}; Max I/O threads: {1}", workerThreads, ioThreads));
ThreadPool.GetMinThreads(out workerThreads, out ioThreads);
Console.WriteLine(String.Format("Min worker threads: {0}; Min I/O threads: {1}", workerThreads, ioThreads));
ThreadPool.GetAvailableThreads(out workerThreads, out ioThreads);
Console.WriteLine(String.Format("Available worker threads: {0}; Available I/O threads: {1}", workerThreads, ioThreads));
#endregion
}
Task(.Net Framework 3.0 提供)
任务,.Net Framework 3.0 提供,使用了线程池,所以全部是后台线程
{
/*两种启动方式是一样的*/
Task.Run(new Action(() => {
Thread.Sleep(5000);
}));
Task.Factory.StartNew(() => { Console.WriteLine("");});
}
{
/*
* 需要多线程加快速度,同时又有要求全部完成后,才能返回
* 多业务操作,希望并发,但是全部完成后再返回
*/
TaskFactory taskFactory = Task.Factory;
List<Task> taskList = new List<Task>();
taskList.Add(taskFactory.StartNew(() => { }));
taskList.Add(taskFactory.StartNew(() => { }));
taskList.Add(taskFactory.StartNew(() => { }));
taskList.Add(taskFactory.StartNew(() => { }));
Task.WaitAll(taskList.ToArray()); //等待,任务全部完成后再往后执行
/*
* 需要多线程加快速度,同时又有要求某个完成后,才能返回
* 多业务操作,希望并发,但是任意某个任务完成后再返回
*/
Task.WaitAny(taskList.ToArray());
//想知道是哪个任务完成,可以传入第二个参数,然后通过ContinueWith来获得
Task task = taskFactory.StartNew(o => { }, "lucas");
//通过task.AsyncState 就是下面的 t的 AsyncSate来进行判断
Task task1 = taskFactory.StartNew(o => { }, "lucas").ContinueWith(t => Console.WriteLine($"这里是{t.AsyncState}的回调"));
//不阻塞主线程使用回调
taskFactory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId.ToString()}"));
taskFactory.ContinueWhenAll(taskList.ToArray(), tList => Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId.ToString()}"));
//带返回值的
Task<int> intTask = taskFactory.StartNew(() => 123);
int result = intTask.Result; //获得返回值
}
Parallel (.Net Framework 4.0)
并行运算,.Net Framework 4.0 提供
{
//跟task很像,等价于 task+waitall ,启动多个线程计算,而且主线程也参与计算,节约了一个线程
Parallel.Invoke(() => DoSomething("Task_001"),
() => DoSomething("Task_002"),
() => DoSomething("Task_003"),
() => DoSomething("Task_004"),
() => DoSomething("Task_005"));
Parallel.For(0, 5, t =>
{
DoSomething($"Task_00{t}");
});
Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, t =>
{
DoSomething($"Task_00{t}");
});
ParallelOptions options = new ParallelOptions()
{
MaxDegreeOfParallelism = 3 //设置启动并发任务数量最多是3个
};
//只会开启3个线程执行,并且包含了主线程
Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, options, t =>
{
DoSomething($"Task_00{t}");
});
//可以利用委托的第二个参数state来结束并行运算
Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, options, (t, state) =>
{
DoSomething($"Task_00{t}");
//state.Stop();//结束全部的
//state.Break();//停止当前的
//return;
});
}
async/await(C# 5.0 .Net Framework 4.5 提供)
C# 5.0 .Net Framework 4.5 提供
是一个语法糖,由编译器提供,而不是CLR升级的
1、不能单独的使用await,使用await时方法声明必须有async,指使用async的话和普通方法没区别
2、await只能放在Task前面
3、不推荐void返回值,使用Task来代替
4、async/await本身不会产生多线程,只是和Task使用,本质上还是Task
在.net Framework 4.5以后,微软提供了async+await语法糖,目的是希望能够在执行某一耗时较长的任务时,仍能做些其他的事情。这一机制运用在UI交互时则显得非常好用。
这里也创建一个窗体项目来作为演示:
private static async void NoReturn()
{
//主线程执行
Console.WriteLine($"NoReturn Sleep before await,ThreadId={Thread.CurrentThread.ManagedThreadId}");
TaskFactory taskFactory = new TaskFactory();
Task task = taskFactory.StartNew(() =>
{
Console.WriteLine($"NoReturn Sleep before,ThreadId={Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(3000);
Console.WriteLine($"NoReturn Sleep after,ThreadId={Thread.CurrentThread.ManagedThreadId}");
});
await task;//主线程到这里就返回了,执行主线程任务
//一流水儿的写下去的,耗时任务就用await
//子线程执行 其实是封装成委托,在task之后成为回调(编译器功能 状态机实现)
//task.ContinueWith()
//这个回调的线程是不确定的:可能是主线程 可能是子线程 也可能是其他线程
Console.WriteLine($"NoReturn Sleep after await,ThreadId={Thread.CurrentThread.ManagedThreadId}");
}
/// <summary>
/// 无返回值 async Task == async void
/// Task和Task<T>能够使用await, Task.WhenAny, Task.WhenAll等方式组合使用。Async Void 不行
/// </summary>
/// <returns></returns>
private static async Task NoReturnTask()
{
//这里还是主线程的id
Console.WriteLine($"NoReturnTask Sleep before await,ThreadId={Thread.CurrentThread.ManagedThreadId}");
Task task = Task.Run(() =>
{
Console.WriteLine($"NoReturnTask Sleep before,ThreadId={Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(3000);
Console.WriteLine($"NoReturnTask Sleep after,ThreadId={Thread.CurrentThread.ManagedThreadId}");
});
await task;
Console.WriteLine($"NoReturnTask Sleep after await,ThreadId={Thread.CurrentThread.ManagedThreadId}");
//return new TaskFactory().StartNew(() => { }); //不能return 没有async才行
}
/// <summary>
/// 带返回值的Task
/// 要使用返回值就一定要等子线程计算完毕
/// </summary>
/// <returns>async 就只返回long</returns>
private static async Task<long> SumAsync()
{
Console.WriteLine($"SumAsync 111 start ManagedThreadId={Thread.CurrentThread.ManagedThreadId}");
long result = 0;
await Task.Run(() =>
{
for (int k = 0; k < 10; k++)
{
Console.WriteLine($"SumAsync {k} await Task.Run ManagedThreadId={Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
}
for (long i = 0; i < 999999999; i++)
{
result += i;
}
});
return result;
}
{
Task<long> t = SumAsync();
Console.WriteLine($"Main Thread Task ManagedThreadId={Thread.CurrentThread.ManagedThreadId}");
long lResult = t.Result;//访问result,主线程等待Task的完成
t.Wait();//等价于上一行
}
线程的异常处理
多线程里面的异常不会被外部线程捕获到,除非调用WaitAll进行等待,所以使用多线程的时候,是不允许抛出异常的,也就是说线程的异常需要在内部自己try/catch处理掉。
try{
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
for (int i = 0; i < 20; i++)
{
string name = string.Format($"btnThreadCore_Click_{i}");
Action<object> act = t =>
{
try
{
Thread.Sleep(2000);
if (t.ToString().Equals("btnThreadCore_Click_11"))
{
throw new Exception(string.Format($"{t} 执行失败"));
}
if (t.ToString().Equals("btnThreadCore_Click_12"))
{
throw new Exception(string.Format($"{t} 执行失败"));
}
Console.WriteLine("{0} 执行成功", t);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
};
taskList.Add(taskFactory.StartNew(act, name));
}
Task.WaitAll(taskList.ToArray()); //此处调用WaitAll后上面抛出的异常才能被捕获,否则无法捕获异常
}
catch(AggregateException aex)
{
foreach (var item in aex.InnerExceptions)
{
Console.WriteLine(item.Message);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
线程的取消
线程取消不能试图去操作线程,而是要操作信号量(一个共享变量,多个线程可以共同访问到的,可以说hi是变量,可以是数据库的数据,也可以是硬盘上的某数据),每个线程在执行的过程当中,经常的去查看这个信号量,然后自己结束掉自己,线程不能被其他线程终止,只能自己结束自己,结束的时候延迟的时间是不可避免的。
————————————————
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
//CancellationTokenSource可以在cancel后,取消没有启动的任务
CancellationTokenSource可以在cancel后,取消没有启动的任务
CancellationTokenSource cts = new CancellationTokenSource();//bool值
for (int i = 0; i < 40; i++)
{
string name = string.Format("btnThreadCore_Click{0}", i);
Action<object> act = t =>
{
try
{
Thread.Sleep(2000);
if (t.ToString().Equals("btnThreadCore_Click11"))
{
throw new Exception(string.Format("{0} 执行失败", t)); //抛异常做测试
}
if (t.ToString().Equals("btnThreadCore_Click12"))
{
throw new Exception(string.Format("{0} 执行失败", t));
}
if (cts.IsCancellationRequested)//检查信号量
{
Console.WriteLine("{0} 放弃执行", t);
return;
}
else
{
Console.WriteLine("{0} 执行成功", t);
}
}
catch (Exception ex)
{
cts.Cancel(); //这里抛异常后进行了Cancel,其他线程运行的时候检测到信号量便返回不再执行
Console.WriteLine(ex.Message);
}
};
taskList.Add(taskFactory.StartNew(act, name, cts.Token));
}
Task.WaitAll(taskList.ToArray());
线程安全
当多个线程访问共有变量(都能访问的局部变量/全局变量/数据库的一个值/硬盘)时,可能会出现线程冲突。
.Net中提供了线程安全的集合:System.Collections.Concurrent.ConcurrentDictionary
解决线程冲突的方法:
1、使用lock,lock方法块里面是单线程的,因此lock里面的代码要尽量少,只包含共享的就好
2、没有冲突,尽量从数据上隔离开
lock的使用:
1、lock ==Monitor.Enter 检查下这个变量有没有被lock 如果有就等着,没有就占用,然后进去执行,执行完了释放
2、尽量不要使用 lock(this) 锁定当前实例,因为如果其他地方要使用这个实例变量则会都被锁定了
3、如果每个实例想要单独的锁定,声明一个 private object 的变量
4、不要锁定一个字符串,例如 string a=”123456” ;lock(a) { } string b=”123456”;由于.Net中字符串是使用享元模式的内存分配,字符串值是唯一的,会锁定别的变量b
5、声明 private static readonly object LockObject= new object(); 的时候最好加上readonly,这也是微软推荐的解决线程冲突lock的最佳使用方法,防止该变量在lock中被更改失效。
————————————————
private static readonly object LockObject= new object();
private int TotalCount = 0;//
private List<int> IntList = new List<int>();
for (int i = 0; i < 10000; i++)
{
int newI = i;
taskList.Add(taskFactory.StartNew(() =>
{
int m = 3 + 2;
lock (LockObject)//lock后的方法块,任意时刻只有一个线程可以进入
//Monitor.Enter(LockObject)
{
//这里就是单线程
this.TotalCount += 1;//多个线程同时操作,有时候操作被覆盖了
IntList.Add(newI);
}
}));
}
Task.WaitAll(taskList.ToArray());
Console.WriteLine(this.TotalCount);
Console.WriteLine(IntList.Count());
四、 总结
1、使用多线程的时候,尽量不要是用Thread,ThreadPool
2、最好使用Task,Parallel,async/await
3、使用委托进行异步操作的时候,尽量不要自己声明委托了,因为微软已经提供了Action 、Func两种委托,足够使用了
4、使用线程的时候要注意异常的捕获,和线程安全问题
- 异步方法旨在成为非阻塞操作。
- 当等待的任务正在运行时,异步方法中的await表达式不会阻塞当前线程。
- 相反,表达式将方法的其余部分注册为延续,并将控制权返回给异步方法的调用者。
- async \await关键字不会导致创建额外的线程。
- 异步方法不需要多线程,因为异步方法不在其自己的线程上运行。
- 该方法在当前同步上下文上运行,并且仅在该方法处于活动状态时才使用线程上的时间。
- 您可以使用Task.Run将受 CPU 限制的工作转移到后台线程,但后台线程对等待结果可用的进程没有帮助。
- 在几乎所有情况下,基于异步的异步编程方法都优于现有方法。
- 特别是,对于 I/O 绑定操作,这种方法比BackgroundWorker类更好,因为代码更简单,而且您不必防范竞争条件。
- 结合Task.Run方法,对于 CPU 密集型操作,异步编程比BackgroundWorkerTask.Run更好,
- 因为异步编程将运行代码的协调细节与传输到线程池的工作分开。
- 标记的 async 方法可以使用await来指定暂停点。
运算符告诉编译器,在await等待的异步过程完成之前,异步方法不能继续超过该点。与此同时,控制权返回给异步方法的调用者。
- 在表达式处暂停异步方法await并不构成该方法的退出,并且finally块不会运行。
- 标记的异步方法本身可以由调用它的方法等待。The marked async method can itself be awaited
by methods that call it.