线程概念
要掌握线程,首先要理解什么是进程。
进程(Process)是Window系统中的一个基本概念。进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
简单来说:每一个执行文件都是一个独立的进程。
线程(Thread)是进程中的基本执行单位,是操作系统分配CPU时间的基本单位。
进程与线程的关系
- 一个线程可以创建和撤销另一个线程;
- 同一个进程中的多个线程之间可以并发执行。
问:为什么我们打开任务管理器会显示正在处理多个进程?
答:理论上单核Cpu在同一时刻只能运行一个进程,但是因为cpu处理进程速度很快,并且在做高速切换的动作,所以我们才感觉它在同时处理多个进程。并且现在很多计算机都是多核cpu。
同步与异步
同步:即如果一个方法被调用,调用者需要等待方法执行完毕才能进行下一步操作
异步:如果一个方法被调用,调用者无需等待方法执行完,直接进行下一步操作。
线程分类
- 主线程:CLR为进程主动创建。
- 前台线程:自主创建的新线程默认都为前台线程,前台线程全部关闭后应用程序才能关闭。
- 后台线程:后台线程与前台线程的区别是当程序关闭后,后台线程就会被关闭。
Parallel类
parallel静态类是.Net Framework提供用于数据和任务并行性的类。属于同步执行。
- Parallel.For();
- Parallel.Foreach();
- Parallel.Invoke();
其中FOR和FOREACH属于数据并行,而INVOKE属于任务并行。
如何区别数据并行和任务并行呢,举个例子:
- 期末考试后,3个教师有900份考卷需要批改
数据并行方法:将900份/3,每个老师手上拿到300份来各自批改。
任务并行方法:900份整体不变,而是老师将试卷上的题目/3,每人负责批改对应的900份试题。
开启多线程
Thread
public class Program
{
public static void main(string[] args)
{
//被调用的是无参数方法
Thread thread = new Thread(SendMessage);
thread.Start();
//被调用的是带参数方法
Thread thread = new Thread(new ParameterizedThreadStart(SendMessage1));
thread.Start("Lok");
}
public void SendMessage()
{
Console.WriteLine("Hello");
}
public void SendMessage1(string str)
{
Console.WriteLine(str);
}
}
Action或者Func
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine($"主线程 id:{Thread.CurrentThread.ManagedThreadId} start");
AsyncCallback callback = ar =>
{
MessageBox.Show($"线程callback {ar.AsyncState}");
};
Action<string> action = (str) =>
{
Thread.Sleep(4000);
};
//
IAsyncResult ar = action.BeginInvoke("lok", callback, "LOK");
ar.AsyncWaitHandle.WaitOne();
Console.WriteLine($"主线程 id:{Thread.CurrentThread.ManagedThreadId} end");
}
private void button2_Click(object sender, EventArgs e)
{
Func<int, int, int> func = (a, b) =>
{
Thread.Sleep(2000);
return a + b;
};
IAsyncResult ar = func.BeginInvoke(2, 6, null, null);
ar.AsyncWaitHandle.WaitOne();
int result = func.EndInvoke(ar);
MessageBox.Show(result.ToString());
}
Invoke是线程同步,BeginInvoke是异步线程。
BeginInvoke的有两个固定参数:
- AsyncCallback前的参数是action的输入参数;
- AsyncCallback是线程的回调函数,它的本质是一个委托,当线程结束就出调用此函数;
- 最后一个Object 作用是向callback中传入数据,在callback中能通过ar.AsyncState获取object的值。
IAsyncResult 中的AsyncWaitHandle.WaitOne()是等待线程发出结束信号量,起到阻塞线程的作用。waitone还可以添加参数millisecondsTimeout,此参数是超时范围,当在0-millisecondsTimeout时间内没有收到信号,主线程就直接执行下一条命令。
当使用Func开启带返回值的多线程,要想获取返回值,需要利用EndInvoke()方法。
Task
static void Main(string[] args)
{
List<Task> tasks = new List<Task>();
Console.WriteLine($"主线程 id:{Thread.CurrentThread.ManagedThreadId} start");
tasks.Add( Task.Run(() => GoToWork("Lok ", "C# ")));
tasks.Add( Task.Run(() => GoToWork("abbie", "Computer")));
tasks.Add( Task.Run(() => GoToWork("Kathy", "Math ")));
tasks.Add( Task.Run(() => GoToWork("Junyu", "English ")));
// Task.WaitAll(tasks.ToArray());
TaskFactory taskFactory = new TaskFactory();
taskFactory.ContinueWhenAll(tasks.ToArray(), tArray =>
{
Console.WriteLine("所有线程完成");
});
Console.WriteLine($"主线程 id:{Thread.CurrentThread.ManagedThreadId} end");
Console.ReadKey();
}
public static void GoToWork(string name, string work)
{
Console.WriteLine($"子线程 {name} id:{Thread.CurrentThread.ManagedThreadId},工作内容:{work} start");
int k = 0;
for (int i = 0; i < 10000; i++)
{
k += i;
}
Thread.Sleep(2000);
Console.WriteLine($"子线程 {name} id:{Thread.CurrentThread.ManagedThreadId},工作内容:{work} end");
}
Task开启线程的方法为Task.Run(delegate k);
接收Task线程的信号量的方法有两种:
- WaitAll或者WaitAny:这两个的特点是会阻塞主线程;
- TaskFactory类中的ContinueWhenAll和ContinueWhenAny:这两个的特点相反,不阻塞主线程。
反馈:当所有子线程结束时,输出的最后两行结果如下
主线程 id:1 end
所有线程完成
ContinueWhenAll方法输出的比WaitAll输出的要慢,如何结果这个顺序问题?
答:WaitAll是接收所有Task完成的信号,而ContinueWhenAll也是Task类型,可以将ContinueWhenAll左右任务添加到list中,执行到WaitAll时会同时检测ContinueWhenAll这个任务完成才执行。改进后程序如下:
static void Main(string[] args)
{
List<Task> tasks = new List<Task>();
Console.WriteLine($"主线程 id:{Thread.CurrentThread.ManagedThreadId} start");
tasks.Add( Task.Run(() => GoToWork("Lok ", "C# ")));
tasks.Add( Task.Run(() => GoToWork("abbie", "Computer")));
tasks.Add( Task.Run(() => GoToWork("Kathy", "Math ")));
tasks.Add( Task.Run(() => GoToWork("Junyu", "English ")));
// Task.WaitAll(tasks.ToArray());
TaskFactory taskFactory = new TaskFactory();
tasks.Add( taskFactory.ContinueWhenAll(tasks.ToArray(), tArray =>
{
Console.WriteLine("所有线程完成");
}));
Task.WaitAll(tasks.ToArray());
Console.WriteLine($"主线程 id:{Thread.CurrentThread.ManagedThreadId} end");
Console.ReadKey();
}
public static void GoToWork(string name, string work)
{
Console.WriteLine($"子线程 {name} id:{Thread.CurrentThread.ManagedThreadId},工作内容:{work} start");
int k = 0;
for (int i = 0; i < 10000; i++)
{
k += i;
}
Thread.Sleep(2000);
Console.WriteLine($"子线程 {name} id:{Thread.CurrentThread.ManagedThreadId},工作内容:{work} end");
}
Task task=Task.Run(()=>
{
Thread.Sleep(1000);
Console.WriteLine("Lok")
});
task.ContinueWith(callback=>{Console.WriteLine("End")});
public Task ContinueWith(Action continuationAction)是Task的回调函数;
线程安全
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。也就是说如果出现数据丢失,或者其他与同步线程出现不一样结果的即为线程不安全。
Lock
private static readonly object LOCK = new object();
static void Main(string[] args)
{
List<int> vs = new List<int>();
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
lock (LOCK)
{
vs.Add(i);
}
});
}
Thread.Sleep(1000);
Task.WaitAll();
Console.WriteLine(vs.Count);
Console.ReadKey();
}
上述例子,假如不添加lock,输出的值是小于10000的值。添加lock其实就是单线程化,vs.add()确保每个时刻都只有一个线程在访问,其余线程都需要排队访问。
注意:
- LOCK不能是值类型,不能为null。
- 当两个方法的lock共用一个LOCK,作用是这两个方法相互阻塞,分别有序地进行;当两个方法的lock分别用单独的LOCK,那么他们可以并发进行。
Async/Await
Async/Await的好处在于可以用同步编写方法来执行异步线程,当程序执行到Await时,先释放调用线程,其后的代码等用于包装成一个回调函数。
public class Program
{
public static readonly object LOCK = new object();
static void Main(string[] args)
{
MyAsync myAsync = new MyAsync();
Console.WriteLine($"Main Start {Thread.CurrentThread.ManagedThreadId}");
myAsync.ReturnTask();
Console.WriteLine($"Main End {Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
}
public class MyAsync
{
public async Task ReturnTask()
{
Console.WriteLine($"NoReturn Start {Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() =>
{
Console.WriteLine($"NoReturnTask1 Start {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
Console.WriteLine($"NoReturnTask1 End {Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"NoReturn1 End {Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() =>
{
Console.WriteLine($"NoReturnTask2 Start {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(2000);
Console.WriteLine($"NoReturnTask2 End {Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"NoReturn2 End {Thread.CurrentThread.ManagedThreadId}");
}
}