c# 多线程入门记录
c# 多线程入门记录
记录下c#多线程的常用使用方法。
基础:一个进程可以包含多个线程,把进程比作工厂,用来完成某个业务,那么线程相当于该工厂的车间。多个车间相互合作完成各自的任务。使得工厂完成它的业务。而车间与车间是共享使用一个工厂的资源的如电力、人力、资金等。
-
多线程的开启方式
命名空间:using System.Threading;
- <h2>委托方式开启</h2>
调用一个委托的BeginInvoke方法可以开启一个线程
```c#```
static void TestMethod(){
Console.WriteLine("测试线程");
}
static void Main(string []args){
Action a=TestMethod;
IAsyncResult ar= a.BeginInvoke(null,null);
//通过ar可以对该线程进行操作,具体详细方法可查msdn:IAsyncResult。
Console.ReadKey();
}
```
注意:
BeginInvoke的两参数。第一个是一个委托参数M,第二个是Object参数O。 M是回调委托,当ar执行完毕后会调用M,而O是这个M的参数。
```c#```
//这里演示,当函数有参数和返回值时的情况
static int TestMethod(string str){
Console.WriteLine("测试线程"+str);
Thread.Sleep(1000);
//Thread的静态方法,可暂停当前线程x ms。
Console.WriteLine("测试结束");
return 100;
}
static void Main(string []args){
Func<string,int> a=TestMethod;
IAsyncResult ar= a.BeginInvoke(null,null);
if(ar.AsyncWaitHandle.WaitOne(2000)){
var res=ar.EndInvoke(ar);
Console.WriteLine(res);
}
Console.WriteLine("main");
//这里用于等待线程执行完毕。2000代表最长等待时间 单位是毫秒。
Console.ReadKey();
}
```
输出:
测试线程
测试结束
100
main
用回调方式也可检测结束,以下为回调方式的main函数
```c#```
static void Main(string []args){
Func<string,int> a=TestMethod;
a.BeginInvoke(ar=>{
Console.WriteLine(a.Envoke(ar));
},null)
Console.WriteLine("main");
Console.ReadKey();
}
```
输出:
main
测试线程
测试结束
100
- <h2>对象方法开启</h2>
实例化一个Tread对象,也可以开启一个线程。
```c#```
static void TestMethod(object o){
Console.WriteLine("测试线程"+o);
Thread.Sleep(1000);
//Thread的静态方法,可暂停当前线程x ms。
Console.WriteLine("测试结束");
}
static void Main(string []args){
Thread t=new Thread(TestMethod);
t.Start("Test");
Console.WriteLine("main");
Console.ReadKey();
}
```
输出:
main
测试线程Test
测试结束
注意:
采用这种方式,函数不能有返回值,参数必须为Object型。Thread有四种构造方式,其余种类可查Msdn。当然该种方法,也可以将线程执行单独写一个类,执行某个对象的成员方法。返回值的问题很容易就解决了。
Thread 对象提供多种对线程的操作方式。如Start、Abort、Join等,这里不再多加介绍,可自行查找。
-
多线程前台后台概念以及优先级
- 前后台
c#中 线程分为前台线程和后台线程。
当前台线程执行完毕后,将终止所有的后台线程。一个进程只有所有的前台线程执行完毕,才算进程执行完毕。
使用new方法 如Thread t= new Thread(delegete), 声明的线程,默认为 前台线程,可以通过 t.IsBackGround=true;的方式设置为后台线程。 - 优先级
优先级是每一个线程带有的一个属性,CPU在一个时间中 只能执行一个线程。而线程优先级,会使线程调度器判断 什么线程优先执行。
- 前后台
-
线程池
类似对象池的一个东西,C# 预定义了一个ThreadPool类,用户可以想线程池中申请一个新线程来完成某项工作。也是一种开启线程的方式。具体操作例子如下: ```c#``` static void TestMethod(object o){ Console.WriteLine("测试线程"+o); Thread.Sleep(1000); //Thread的静态方法,可暂停当前线程x ms。 Console.WriteLine("测试结束"); } static void Main(string []args){ ThreadPool.QueueUserWorkItem(TestMethod,"T1"); ThreadPool.QueueUserWorkItem(TestMethod,"T2"); ThreadPool.QueueUserWorkItem(TestMethod,"T3") ThreadPool.QueueUserWorkItem(TestMethod,"T4") Console.ReadKey(); } ``` 输出: 测试线程T3 测试线程T1 测试线程T2 测试线程T4 测试结束 测试结束 测试结束 测试结束注意:
线程池开启线程的 顺序不是确定的,所以T1、2、3、4的输出顺序,每次运行都会不同。此外线程池中的线程不能改变其优先级、也不能改变它的前后台状态。线程池中的线程 都是 后台线程。尽量在工作量小的线程才去使用线程池。 -
任务
任务是线程的一种封装,任务也是一种开启线程的方式。
c#
static void TestMethod(object o){
Console.WriteLine("测试线程"+o);
Thread.Sleep(1000);
//Thread的静态方法,可暂停当前线程x ms。
Console.WriteLine("测试结束");
}
static void Main(string []args){
Task t = new Task(Th, "Test");
t.Start();
Console.ReadKey();
}输出: 测试线程Test 测试结束 或者: ```c#``` static void TestMethod(object o){ Console.WriteLine("测试线程"+o); Thread.Sleep(1000); //Thread的静态方法,可暂停当前线程x ms。 Console.WriteLine("测试结束"); } static void Main(string []args){ var tf= new TaskFactory(); tf.StartNew(Th, "Test") Console.ReadKey(); }
输出结果 与上述一至
最常见的TaskFactory的用法:
C#
static void Th(object o)
{
Console.WriteLine("TH"+o);
Console.WriteLine("ID:" + Task.CurrentId);
Thread.Sleep(1000);
Console.WriteLine("TH");
}
static void Sh(Task t,object o)
{
Console.WriteLine("Sh" + o);
Console.WriteLine("PreTaskId:" + t.Id);
Console.WriteLine("ID:" + Task.CurrentId);
Thread.Sleep(1000);
Console.WriteLine("Sh");
}
static void Main(string []args){
var tf = new TaskFactory();
Task t= tf.StartNew(Th, "T1");
Task t2 = t.ContinueWith(Sh,"S2");
Console.ReadKey();
}输出 : THT1 ID:1 TH ShS2 PreTaskId:1 ID:2 Sh 利用任务模式很容易可以控制线程的依赖关系。上述例子中t2就是t的一个子线程,只有当t执行完毕,t2才能开始执行。且利用TaskFactory产生的任务都有唯一不可改的ID值。便于操作,也可以与其他线程分隔开。非常好用。
-
加锁
当一个数据需要被多个线程使用时,可能会产生逻辑问题。
如下例子:
c#
class Test{
private int num;
public Test(){num=1;}
public Work(){
while(true){
num=1;
if(num>1)
Console.Write(num)
num++;
}
}
}
static void Main(string []args){
var test=new Test();
var t1=new Task(test.work());
t1.Start();
//var t2=new Task(test.work());
//t2.Start();
}很明显当main函数中这样写时。代码正常运行。不会输出任何的东西。 但是当把注释的代码取消注释后运行。发现可以循环输出num; 这是因为。两个线程异步执行时,会出现两者中一个线程a执行到if(num>1)时,另外的一个线程b执行到了num++;造成num=2;进行输出的情况。 但当我们加锁后,即可避免该种情况。 将main函数代码做如下修改: ```c#``` static void Main(string []args){ var test=new Test(); var t1=new Task(() => { lock (t) // { t.work(); } //判断t是否被锁,如果被锁则将在lock位置等待,直到t变自由,若为自由状态,则往下执行,并把该对象加锁。直到执行完毕,将t对象自由。 }); t1.Start(); var t2=new Task(() => { lock (t) { t.work(); } }); t2.Start(); }
注意:
使用这种方法,即可确保t.work()在同一时刻,仅有一个线程进行访问。lock 中的参数必须是引用类型。
注意避免 死锁
c#
static void x(){
lock(a){
lock(b){
....
}
}
}
static void y(){
lock(b){
lock(a){
....
}
}
}
上述是一段 可能发生死锁的伪代码。 即当线程y 将b加锁后,且线程x将a加锁后,即死锁。两线程都无法等待到第二个加锁数据的自由状态。将x、y中的a、b加锁顺序保持一致,则可以避免该种情况。