先来看几个基本概念(纯属个人见解,可能不准确):
进程:程序运行时,占用的全部运行资源的总和。
线程:线程由线程调度程序在内部管理,CLR通常将这一功能委托给操作系统。线程也可以有自己的计算资源,是程序执行流的最小单位。任何的操作都是由线程来完成的。
多线程:多核cpu协同工作,多个执行流同时运行,是用资源换时间。(单核cpu,不存在所谓的多线程)。
Thread
Thread的对象是非线程池中的线程,有自己的生命周期(有创建和销毁的过程),所以不可以被重复利用(一个操作中,不会出现二个相同Id的线程)。
Thread的常见属性:
Thread的常见用法:
join
调用join方法可以等待另一个线程结束。
private void button5_Click(object sender, EventArgs e) {
Console.WriteLine($"===============Method start time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}===================");
//开启一个线程,构造方法可重载两种委托,一个是无参无返回值,一个是带参无返回值
Thread thread = new Thread(a => DoSomeThing("Thread"));
//当前线程状态
Console.WriteLine($"thread's state is {thread.ThreadState},thread's priority is {thread.Priority} ,thread is alived :{thread.IsAlive},thread is background:{thread.IsBackground},thread is pool threads: {thread.IsThreadPoolThread}");
//告知操作系统,当前线程可以被执行了。
thread.Start();
//阻塞当前执行线程,等待此thread线程实例执行完成。无返回值
thread.Join();
//最大等待的时间是5秒(不管是否执行完成,不再等待),返回一个bool值,如果是true,表示执行完成并终止。如果是false,表示已到指定事件但未执行完成。
thread.Join(5000);
Console.WriteLine($"===============Method end time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},,Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}===================");
}
private void DoSomeThing(string name)
{
Console.WriteLine($"do some thing start time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}");
long result = 0;
for (long i = 0; i < 10000 * 10000; i++)
{
result += i;
}
Console.WriteLine($"do some thing end time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}");
}
Sleep
Thread.Sleep()会暂停当前线程,,并等待一段时间。其实,Thread.Sleep只是放弃时间片的剩余时间,让系统重新调度并选择一个合适的线程。
在没有其他活动线程的情况下,使用Thread.Sleep(0)还是会选上自身,即连任,系统不会对其做上下文切换。
static void Main(string[] args)
{
Stopwatch stopwatch=new Stopwatch();
stopwatch.Start();
Thread.Sleep(0);
stopwatch.Stop();
System.Console.WriteLine(stopwatch.ElapsedMilliseconds); //返回0
}
而Thread.Sleep(大于0)却让当前线程沉睡了,即使只有1ms也是沉睡了,也就是说当前线程放弃下次的竞选,所以不能连任,系统上下文必然发生切换。
阻塞
下面是ThreadState的所有值(官方截图):
ThreadState是一个flags枚举,通过按位的形式可以合并数据的选项。
bool isBlocked = (thread.ThreadState & ThreadState.WaitSleepJoin) != 0;
本地独立(Local)
CLR为每个线程分配自己的内存栈(Statck),以便本地变量保持独立。
static void Main(string[] args) { //在新线程上调用Test() new Thread(Test).Start(); //在主线程上调用Test() Test(); } static void Test() { //i 是本地的局部变量,在每个线程的内存栈上,都会创建i独立的副本 for (int i = 0; i < 5; i++) { System.Console.Write("o"); } } //结果会输出十个o
共享(Shared)
如果多个线程都引用了同一个对象的实例,那么它们就共享了数据。
class Program { bool flag; static void Main(string[] args) { //创建一个实例 Program program = new Program(); //分别在新线程上和主线程上调用同一实例的Test()方法 new Thread(program.Test).Start(); program.Test(); //输出一个FLAG } void Test() { if (!flag) { flag = true; System.Console.WriteLine("FLAG"); } } }
被Lambda表达式或匿名委托所捕获的本地变量,会被编译器转换为字段(field),所以也会被共享。
class Program { static void Main(string[] args) { //此局部变量会被编译器转换为所在类的字段,进行处理,所以会被共享。 bool flag = false; ThreadStart threadStart = () => { if (!flag) { flag = true; System.Console.WriteLine("FLAG"); } }; new Thread(threadStart).Start(); threadStart(); }
静态字段(field)也会在线程间共享数据。
class Program { //静态字段在同一应用域下的所有线程中被共享 static bool flag = false; static void Main(string[] args) { new Thread(Test).Start(); Test(); } static void Test() { if(!flag){ flag=true; System.Console.WriteLine("FLAG"); } } }
线程安全
由于线程中数据可以共享,可能会引发线程安全(Thread Safety)问题(上面三个例子都有线程安全问题,应尽可能的避免使用共享状态)。
在读取和写入共享数据的时候,通过使用一个互斥锁(exclusive lock),就可以解决上面三个例子的线程安全问题。锁可以基于任何引用类型对象。使用lock语句来加锁,当两个线程同时竞争一个锁的时候,一个线程会等待或阻塞,直到锁变成可用状态。
class Program { //静态字段在同一应用域下的所有线程中被共享 static bool flag = false; static readonly object locker = new object(); static void Main(string[] args) { new Thread(Test).Start(); Test(); } static void Test() { lock (locker) { if (!flag) { System.Console.WriteLine("FLAG"); Thread.Sleep(1000); flag = true; } } } }
注意:防止lock使用不当引起死锁,最好不要lock public的东西,例如:
1、lock(this) 2、 lock(“string”) 3、lock(typeof(int))
向线程传递数据
往线程的启动方法里传递数据:
1、使用Thread的重载构造方法和Thread.Start()的重载方法来传递数据。(只能接收object类型的参数,是C# 3.0之前的用法)
class Program { static void Main(string[] args) { new Thread(Print).Start("asdf"); } static void Print(object? message) { System.Console.WriteLine(message); } }
2、使用Lambda表达式,在里面使用参数调用方法。
class Program
{
static void Main(string[] args)
{
new Thread(()=>Print("Hello World!")).Start();
}
static void Print(string message)
{
System.Console.WriteLine(message);
}
}
使用Lambda表达式可以很简单的给Thread传递参数。但是线程开始后,可能会不小心修改了被捕获的变量,这要多加注意。
static void Main(string[] args) { //i是一个瞬时的临时变量 for (int i = 0; i < 10; i++) { //每个线程对Console.Write(i)的调用,传递的是当时的i值。 new Thread(()=>Console.Write(i)).Start(); } }
解决办法:
static void Main(string[] args) { for (int i = 0; i < 10; i++) { //每次循环都有一个临时变量记录i的值。 int temp=i; new Thread(()=>Console.Write(temp)).Start(); } //但是输出顺序依然无法保证 }
前台和后台线程
默认情况下,手动创建的线程就是前台线程。只要有前台线程在运行,那么应用程序就会一直处于活动状态。
thread 默认是前台线程,启动后一定要完成任务的,即使程序关掉(进程退出)也要执行完。可以把thread 指定为后台线程,随着进程的退出而终止。
//false,默认是前台线程,启动后一定要完成任务的,即使程序关掉(进程退出)也要执行完。 Console.WriteLine(thread.IsBackground); thread.IsBackground = true;//指定为后台线程。(随着进程的退出而退出)
static void Main(string[] args) { Thread thread = new Thread(() => { Console.ReadLine(); }); if (args.Length > 0) thread.IsBackground = true; thread.Start(); /* 如果运行时不传递参数,thread为前台线程,程序会等待输入而不会退出。 如果在运行时传递参数(dotnet run XXXX),thread变成后台线程,会随着进程的结束而终止线程。 */ }
注意:线程的前台、后台状态与它的优先级无关。
Thread的回调用法:
Thread没有像Framework中的delegate的回调用法,如果需要回调得自动动手改造:
private void CallBack(Action action, Action calback)
{
Thread thread = new Thread(() => { action(); calback(); });
thread.Start();
}
//无参无返回值
CallBack(() => Console.WriteLine("好吗?"), () => Console.WriteLine("好的!"));
private Func<T> CallBackReturn<T>(Func<T> func)
{
T t = default(T);
Thread thread = new Thread(() =>
{
t = func();
});
thread.Start();
return () =>
{
thread.Join();
return t;
};
}
//带返回值得用法
Func<int> func = CallBackReturn<int>(() => DateTime.Now.Second);
Console.WriteLine("线程未阻塞");
int result = func.Invoke();
Console.WriteLine("result:" + result);
ThreadPool 线程池