c#中线程和异步编程

一 线程

  当我们提及多线程的时候会想到thread和threadpool,这都是异步操作,threadpool其实就是thread的集合,具有很多优势,不过在任务多的时候全局队列会存在竞争而消耗资源。thread默认为前台线程,主程序必须等线程跑完才会关闭,而threadpool相反。
  总结:threadpool确实比thread性能优,但是两者都没有很好的api区控制,如果线程执行无响应就只能等待结束,从而诞生了task任务。

二 thread

https://blog.csdn.net/weixin_44295783/article/details/117464768
https://blog.csdn.net/qq_20161893/article/details/78415427
https://blog.csdn.net/qizijuesha/article/details/80660116
https://blog.csdn.net/CAI____NIAO/article/details/122092358

二 task

1 什么是task

  .NET 4.0推出了新一代的多线程模型Task,task简单地看就是任务,那和thread有什么区别呢?Task的背后的实现也是使用了线程池线程,但它的性能优于ThreadPoll,因为它使用的不是线程池的全局队列,而是使用的本地队列,使线程之间的资源竞争减少。同时Task提供了丰富的API来管理线程、控制。但是相对前面的两种耗内存,Task依赖于CPU,对于多核的CPU性能远超前两者,单核的CPU三者的性能没什么差别。

2 task的两种创建

  创建一个task任务有两种模式:使用factory创建会直接执行,使用new创建不会执行,必须等到start启动之后才执行。

static void DownLoad(object str)
{
    Console.WriteLine("DownLoad Begin ID = " + Thread.CurrentThread.ManagedThreadId + " " + str);
    Thread.Sleep(1000);
    Console.WriteLine("DownLoad End");
}
static void Main(string[] args)
{

    //创建任务
    //Task task = new Task(DownLoad, "人民日报");
    //启动任务
    //task.Start();
    
	//创建任务工厂
    TaskFactory taskFactory = new TaskFactory();
    //开始新的任务
    taskFactory.StartNew(DownLoad, "纽约时报");
    
    Console.WriteLine("Main");   
    Console.ReadKey();
}

3 线程id和任务id

static void Main()
{
    Console.WriteLine("主线程Id:" + Thread.CurrentThread.ManagedThreadId.ToString());

    Console.WriteLine();
    int taskId = 0;

    Task t1 = Task.Factory.StartNew(() =>
    {
        taskId = Task.CurrentId ?? 0;
        Console.WriteLine("Task.CurrentId1:" + taskId.ToString());
        Console.WriteLine("当前线程Id1:" + Thread.CurrentThread.ManagedThreadId.ToString());
    });
    t1.Wait();
    Console.WriteLine("任务Id1:" + t1.Id.ToString());
    Console.WriteLine();
    taskId = 0;

    Task t2 = new Task(() =>
    {
        taskId = Task.CurrentId ?? 0;
        Console.WriteLine("Task.CurrentId2:" + taskId.ToString());
        Console.WriteLine("当前线程Id2:" + Thread.CurrentThread.ManagedThreadId.ToString());
    });
    t2.Start();
    t2.Wait();
    Console.WriteLine("任务Id2:" + t2.Id.ToString());
    Console.WriteLine();
    taskId = 0;

    Task t3 = Task.Run(() =>
    {
        taskId = Task.CurrentId ?? 0;
        Console.WriteLine("Task.CurrentId3:" + taskId.ToString());
        Console.WriteLine("当前线程Id3:" + Thread.CurrentThread.ManagedThreadId.ToString());
    });
    t3.Wait();
    Console.WriteLine("任务Id3:" + t3.Id.ToString());

    Console.WriteLine("\n执行完成");
    Console.ReadLine();
}
/*
主线程Id:9
Task.CurrentId1:1
当前线程Id1:10
任务Id1:1
Task.CurrentId2:2
当前线程Id2:11
任务Id2:2
Task.CurrentId3:3
当前线程Id3:10
任务Id3:3
执行完成
*/

3 task的生命周期

var testTask = new Task(() =>
{
    Console.WriteLine("task start");
    System.Threading.Thread.Sleep(2000);
});
Console.WriteLine(testTask.Status);
testTask.Start();
Console.WriteLine(testTask.Status);
Console.WriteLine(testTask.Status);         
testTask.Wait();
Console.WriteLine(testTask.Status);
Console.WriteLine(testTask.Status);
/*
输出结果:
Created
task start
Running
Running
RanToCompletion
RanToCompletion
*/

  可以看出task确实是异步执行,并且wait很好地控制了task。

4 等待子线程运行结束

  通过wait()对单个task进行等待,Task.waitall()对多个task进行等待,waitany()执行任意一个task就往下继续执行。

var testTask = new Task(() =>
    {
        Console.WriteLine("task start");
        System.Threading.Thread.Sleep(2000);
    });
testTask.Start();
testTask.Wait();
var testTask = new Task(() =>
    {
       Console.WriteLine("task start");
       System.Threading.Thread.Sleep(2000);
    });
testTask.Start();
var factoryTeak = Task.Factory.StartNew(() =>
    {                 
        Console.WriteLine("factory task start");
     });
Task.WaitAll(testTask, factoryTeak);
Console.WriteLine("end");
var testTask = new Task(() =>
{
    Console.WriteLine("task start");
    System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var factoryTeak = Task.Factory.StartNew(() =>
{                  
    Console.WriteLine("factory task start");
});
Task.WaitAny(testTask, factoryTeak);
Console.WriteLine("end");

  waitall和list:

var tasklst = new List<Task>();
for (int i = 0; i < urls.Count; i++)
{
	tasklst.Add(Task.Factory.StartNew<bool>(Request, urls[i]));
}
//等待所需的结果返回
Task.WaitAll(tasklst.ToArray());
bool Request(string url)
{
	//执行方法	
	return true;
}

5 连续任务

var testTask = new Task(() =>
{
    Console.WriteLine("task start");
    System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var resultTest = testTask.ContinueWith<string>((Task) =>
{
    Console.WriteLine("testTask end");
    return "end";
});
Console.WriteLine(resultTest.Result);

6 task的取消

  首先创建一个取消task的令牌的实例,在不启动task直接取消:

var tokenSource = new CancellationTokenSource();//创建取消task实例
var testTask = new Task(() =>
{
    for (int i = 0; i < 6; i++)
    {
        System.Threading.Thread.Sleep(1000);
    }
},tokenSource.Token);
Console.WriteLine(testTask.Status);
tokenSource.Token.Register(()=>
{
    Console.WriteLine("task is to cancel");
});
tokenSource.Cancel();
Console.WriteLine(testTask.Status);
//输出结果:
/*
Created
task is to cancel
Canceled
*/

  如果task启动了真的取消了task?

CancellationTokenSource tokenSource = new CancellationTokenSource();//创建取消task实例
Task testTask = new Task(() =>
{
    for (int i = 0; i < 11; i++)
    {
        Console.WriteLine("fdsaf");
        Thread.Sleep(1000);
    }
}, tokenSource.Token);
Console.WriteLine(testTask.Status);
testTask.Start();
Console.WriteLine(testTask.Status);
tokenSource.Token.Register(() =>
{
    Console.WriteLine("task is to cancel");
});
Thread.Sleep(6000);
tokenSource.Cancel();
Console.WriteLine(testTask.Status);
for (int i = 0; i < 20; i++)
{
    Thread.Sleep(1000);
    Console.WriteLine(testTask.Status);
}
/*
输出结果:
Created
WaitingToRun
task is to cancel
Running
Running
Running
Running
Running
Running
RanToCompletion
RanToCompletion
RanToCompletion
RanToCompletion
RanToCompletion
*/

  可以看出其实并没有取消task,此时task还在继续跑。
  当我们调用了Cancel()方法之后,.NET Framework不会强制性的去关闭运行的Task。
  我们自己必须去检测之前在创建Task时候传入的那个CancellationToken。
  我们在创建Task是传入CancellationToken到构造函数,其实这个CancellationToken就是.NET Framework用来避免我们再次运行已经被取消的Task,可以说就是一个标志位。
  对于线程的取消,查看这里

7 task的嵌套

  在一个任务中可以启动子任务,两个任务异步执行。默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行。使用TaskCreationOptions.AttachedToParent显式指定将任务附加到任务层次结构中的某个父级。

var parentTask = new Task(()=>
{
    var childTask = new Task(() =>
    {
        System.Threading.Thread.Sleep(2000);
        Console.WriteLine("childTask to start");
    });
    childTask.Start();
    Console.WriteLine("parentTask to start");
});
parentTask.Start();
parentTask.Wait();
Console.WriteLine("end");

  此时为普通关联,父task和子task没影响

var parentTask = new Task(()=>
{
	var childTask = new Task(() =>{
	    System.Threading.Thread.Sleep(2000);
	    Console.WriteLine("childTask to start");
	}, TaskCreationOptions.AttachedToParent);
	childTask.Start();
	Console.WriteLine("parentTask to start");
} );
parentTask.Start();
parentTask.Wait();
Console.WriteLine("end");

  此时为父task和子task关联,wait会一直等待父子task执行完。
  如果父任务执行完了但是子任务没有执行完,则父任务的状态会被设置为WaitingForChildrenToComplete,只有子任务也执行完了,父任务的状态才会变成RunToCompletion

8 任务执行的结果

  使用Task的泛型版本,可以返回任务的执行结果。
  下面例子中的TaskWithResult的输入为object类型,返回一个元组Tuple<int, int>
  定义调用TaskWithResult的任务时,使用泛型类Task<Tuple<int, int>>,泛型的参数定义了返回类型。通过构造函数,传递TaskWithResult,构造函数的第二个参数定义了TaskWithResult的输入值。
  任务完成后,通过Result属性获取任务的结果。

static Tuple<int, int> TaskWithResult(object obj)
{
    Tuple<int, int> div = (Tuple<int, int>)obj;
    Thread.Sleep(1000);
    return Tuple.Create<int, int>(div.Item1 + div.Item2, div.Item1 - div.Item2);
}
static void Main(string[] args)
{
    var task = new Task<Tuple<int, int>>(TaskWithResult, Tuple.Create<int, int>(8, 3));            
    task.Start();
    Console.WriteLine(task.Result);
    task.Wait();
    Console.WriteLine("Result: {0} {1}", task.Result.Item1, task.Result.Item2);
    Console.ReadLine();
}
/*
执行结果
(11, 5)
result:11 5
*/

9 task死锁的问题

  我们可以设置最大等待时间,如果超过了等待时间,就不再等待,下面我们来修改代码,设置最大等待时间为5秒(项目中可以根据实际情况设置),如果超过5秒就输出哪个任务出错了。

10 对Spinlock的使用

  举例来说Parallel.for和Parallel.foreach是线程不安全的,有可能达不到你的预期,此时就需要加锁来解决此问题,我们可以加lock和spinlock(自旋锁)来解决。

SpinLock slock = new SpinLock(false);
var testLock= new object();
long sum1 = 0;
long sum2 = 0;
long sum3 = 0;
Parallel.For(0, 100000, i =>
{
    sum1 += i;
});

Parallel.For(0, 100000, i =>
{
    bool lockTaken = false;
    try
    {
        slock.Enter(ref lockTaken);
        sum2 += i;
    }
    finally
    {
        if (lockTaken)
            slock.Exit(false);
    }
});
Parallel.For(0, 100000, i =>
{
    lock(testLock)
    {
        sum3 += i;
    };
});
Console.WriteLine("Num1的值为:{0}", sum1);
Console.WriteLine("Num2的值为:{0}", sum2);
Console.WriteLine("Num3的值为:{0}", sum3);
/*            
输出结果:
Num1的值为:1660913202
Num2的值为:4999950000
Num3的值为:4999950000

Num1的值为:2754493646
Num2的值为:4999950000
Num3的值为:4999950000

Num1的值为:4999950000
Num2的值为:4999950000
Num3的值为:4999950000
*/

三 Thread & Task比较

1 线程资源释放

static void Main(string[] args)
{
    Console.WriteLine("Task   With Thread  Start !");
    for (int i = 0; i <= 5; i++)
    {
        Thread t = new Thread(Dotaskfunction);
        t.Start();
    }
    Console.WriteLine("Task   With Thread End !");
    Console.WriteLine("Task   With Task   Start !");
    for (int i = 0; i <= 5; i++)
    {
        Task.Run(() => { Dotaskfunction(); });
    }
    Console.WriteLine("Task   With Task End !");
    Console.ReadLine();
}
public static void Dotaskfunction()
{
    Console.WriteLine("task  has been done! ThreadID: {0},IsBackGround:{1} ", Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsBackground );
}

在这里插入图片描述
  可以看到Thread方法每次的Thread Id都是不同的,而Task方法的Thread Id是重复出现的。我们知道线程的创建和销毁是一个开销比较大的操作,Task每次执行将不会立即创建一个新线程,而是到CLR线程池查看是 否有空闲的线程,有的话就取一个线程处理这个请求,处理完请求后再把线程放回线程池,这个线程也不会立即撤销,而是设置为空闲状态,可供线程池再次调度, 从而减少开销。

2 实时性

static void Main(string[] args)
{            
	Stopwatch watch = new Stopwatch();
	watch.Start();
#if true
	Thread threadTest1 = new Thread(() =>
	{
	    Console.WriteLine("线程1开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2000);
	    Console.WriteLine("线程1结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest1.Start();
	Thread threadTest2 = new Thread(() =>
	{
	    Console.WriteLine("线程2开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2000);
	    Console.WriteLine("线程2结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest2.Start();
	Thread threadTest3 = new Thread(() =>
	{
	    Console.WriteLine("线程3开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程3结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest3.Start();
	Thread threadTest4 = new Thread(() =>
	{
	    Console.WriteLine("线程4开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程4结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest4.Start();
	Thread threadTest5 = new Thread(() =>
	{
	    Console.WriteLine("线程5开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程5结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest5.Start();
	Thread threadTest6 = new Thread(() =>
	{
	    Console.WriteLine("线程6开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程6结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest6.Start();
	Thread threadTest7 = new Thread(() =>
	{
	    Console.WriteLine("线程7开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程7结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest7.Start();
	Thread threadTest8 = new Thread(() =>
	{
	    Console.WriteLine("线程8开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程8结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest8.Start();
	Thread threadTest9 = new Thread(() =>
	{
	    Console.WriteLine("线程9开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程9结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest9.Start();
	Thread threadTest10 = new Thread(() =>
	{
	    Console.WriteLine("线程10开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程10结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest10.Start();
	Thread threadTest11 = new Thread(() =>
	{
	    Console.WriteLine("线程11开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程11结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest11.Start();
	Thread threadTest12 = new Thread(() =>
	{
	    Console.WriteLine("线程12开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程12结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest12.Start();
	Thread threadTest13 = new Thread(() =>
	{
	    Console.WriteLine("线程13开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程13结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest13.Start();
	Thread threadTest14 = new Thread(() =>
	{
	    Console.WriteLine("线程14开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程14结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest14.Start();
	Thread threadTest15 = new Thread(() =>
	{
	    Console.WriteLine("线程15开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程15结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	threadTest15.Start();
#else
	var Task1 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程1开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2500);
	    Console.WriteLine("线程1结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	var Task2 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程2开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2700);
	    Console.WriteLine("线程2结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task3 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程3开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程3结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task4 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程4开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程4结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task5 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程5开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程5结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task6 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程6开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程6结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task7 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程7开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程7结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task8 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程8开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程8结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task9 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程9开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程9结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task10 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程10开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程10结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	var Task11 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程11开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程11结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task12 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程12开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程12结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task13 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程13开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程13结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task14 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程14开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程14结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	var Task15 = Task.Factory.StartNew(() =>
	{
	    Console.WriteLine("线程15开始消耗时间:{0}", watch.ElapsedMilliseconds);
	    Thread.Sleep(2900);
	    Console.WriteLine("线程15结束消耗时间:{0}", watch.ElapsedMilliseconds);
	});
	
	while (watch.ElapsedMilliseconds <= 3000)
	{
	    //if (!threadTest.IsAlive && !threadTest1.IsAlive)
	    if (Task1.IsCompleted && Task2.IsCompleted && Task3.IsCompleted)
	    {
	        Console.WriteLine("监控结束消耗时间:{0}", watch.ElapsedMilliseconds);
	        break;
	    }
	    else
	    {
	        Thread.Sleep(1);
	    }                    
	}
#endif
}

5 IsBackground作用

  要点:
  1、当在主线程中创建了一个线程,那么该线程的IsBackground默认是设置为FALSE的。
  2、当主线程退出的时候,IsBackground=FALSE的线程还会继续执行下去,直到线程执行结束。
  3、只有IsBackground=TRUE的线程才会随着主线程的退出而退出。
  4、当初始化一个线程,把Thread.IsBackground=true的时候,指示该线程为后台线程。后台线程将会随着主线程的退出而退出。
  5、原理:只要所有前台线程都终止后,CLR就会对每一个活在的后台线程调用Abort()来彻底终止应用程序。
  Net的公用语言运行时(Common Language Runtime,CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
  既然前台线程和后台线程有这种差别,那么我们怎么知道该如何设置一个线程的 IsBackground 属性呢?下面是一些基本的原则:对于一些在后台运行的线程,当程序结束时这些线程没有必要继续运行了,那么这些线程就应该设置为后台线程。比如一个程序启动了一个进行大量运算的线程,可是只要程序一旦结束,那个线程就失去了继续存在的意义,那么那个线程就该是作为后台线程的。而对于一些服务于用户界面的线程往往是要设置为前台线程的,因为即使程序的主线程结束了,其他的用户界面的线程很可能要继续存在来显示相关的信息,所以不能立即终止它们。这里我只是给出了一些原则,具体到实际的运用往往需要编程者的进一步仔细斟酌。
  一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。

四 threadpoll和task的结构图

  threadpool:
在这里插入图片描述
   task:
在这里插入图片描述

五 Async/Await

  本节内容来自这里
  async/await 特性是与Task紧密相关的,所以在了解 async/await 前必须充分了解 Task 的使用。

1 示例

在这里插入图片描述
在这里插入图片描述
  1、从 Main 方法执行到CountCharactersAsync(1, url1)方法时,该方法会立即返回,然后才会调用它内部的方法开始下载内容。该方法返回的是一个Task<int>类型的占位符对象,表示计划进行的工作。这个占位符最终会返回 int 类型的值。
  2、这样就可以不必等 CountCharactersAsync(1, url1) 方法执行完成就可以继续进行下一步操作。到执行CountCharactersAsync(2, url2)方法时,一样返回Task<int>对象。
  3、然后,Main方法继续执行三次 ExtraOperation 方法,同时两次 CountCharactersAsync方法依然在持续工作 。
  4、t1.Resultt2.Result 是指从 CountCharactersAsync 方法调用的 Task<int> 对象取结果,如果还没有结果的话,将阻塞,直有结果返回为止。

2 async/await 结构

  async/await 结构可分成三部分:
  (1)调用方法:该方法调用异步方法,然后在异步方法执行其任务的时候继续执行;
  (2)异步方法:该方法异步执行工作,然后立刻返回到调用方法;
  (3)await 表达式:用于异步方法内部,指出需要异步执行的任务。一个异步方法可以包含多个 await 表达式(不存在 await 表达式的话 IDE 会发出警告)。

3 What’s 异步方法

  异步方法:在执行完成前立即返回调用方法,在调用方法继续执行的过程中完成任务。
  语法分析:
  (1)关键字:方法头使用 async 修饰。
  (2)要求:包含 N(N>0) 个 await 表达式(不存在 await 表达式的话 IDE 会发出警告),表示需要异步执行的任务,没有的话,就和普通方法一样执行了。
  (3)返回类型:只能返回 3 种类型(voidTaskTask\<T>)。TaskTask\<T>标识返回的对象会在将来完成工作,表示调用方法和异步方法可以继续执行。
  (4)参数:数量不限,但不能使用 out 和 ref 关键字。
  (5)命名约定:方法后缀名应以 Async 结尾。
  (6)其它:匿名方法和 Lambda 表达式也可以作为异步对象;async 是一个上下文关键字;关键字 async 必须在返回类型前。
  关于 async 关键字:
  1、在返回类型之前包含 async 关键字;
  2、它只是标识该方法包含一个或多个 await 表达式,即,它本身不创建异步操作;
  3、它是上下文关键字,即可作为变量名。

4 返回类型

  1、Task<T>:调用方法要从调用中获取一个 T 类型的值,异步方法的返回类型就必须是Task。调用方法从 Task 的 Result 属性获取的就是 T 类型的值。

internal class Calculator
{
   private static int Add(int n, int m)
   {
       return n + m;
   }

   public static async Task<int> AddAsync(int n, int m)
   {
       int val = await Task.Run(() => Add(n, m));
       return val;
   }
}
private static void Main(string[] args)
{
    Task<int> t = Calculator.AddAsync(1, 2);
    //一直在干活
    Console.WriteLine($"result: {t.Result}");
    Console.Read();
}

  2、Task:调用方法不需要从异步方法中取返回值,但是希望检查异步方法的状态,那么可以选择可以返回 Task 类型的对象。不过,就算异步方法中包含 return 语句,也不会返回任何东西。

internal class Calculator
{
	private static int Add(int n, int m)
	{
	    return n + m;
	}
	
	public static async Task AddAsync(int n, int m)
	{
	    int val = await Task.Run(() => Add(n, m));
	    Console.WriteLine($"Result: {val}");
	}
}
private static void Main(string[] args)
{
    Task t = Calculator.AddAsync(1, 2);

    //一直在干活

   t.Wait();
   Console.WriteLine("AddAsync 方法执行完成");

   Console.Read();
}

  (3)void:调用方法执行异步方法,但又不需要做进一步的交互。

internal class Calculator
{
    private static int Add(int n, int m)
    {
        return n + m;
    }
    public static async void AddAsync(int n, int m)
    {
        int val = await Task.Run(() => Add(n, m));
        Console.WriteLine($"Result: {val}");
    }
}
private static void Main(string[] args)
{
    Calculator.AddAsync(1, 2);
    //一直在干活
    Thread.Sleep(1000); //挂起1秒钟
    Console.WriteLine("AddAsync 方法执行完成");
    Console.Read();
}

六 Task.Run 和Task.Factory.StartNew 区别

  在线程里,.Net Framework 4.0开始支持Task.Factory.StartNew,.Net Framework 4.5后开始支持Task.Run
  Task.Factory.StartNew经过简化成了Task.Run,注意的是Factory.StartNew的方法参数种类更丰富,可以完成多样的需求。
  在选择上,如果创建的线程需要长时间运行的话那就选择Task.Factory.StartNew
  长时间运行
  两个函数最大的不同在于 Task.Factory.StartNew 可以设置线程是长时间运行,这时线程池就不会等待这个线程回收。

Task.Factory.StartNew(() =>
{
    for (int i = 0; i < 100; i++)
    {
        var foo = 2;
    }
    Console.WriteLine("进行 线程" + Thread.CurrentThread.ManagedThreadId);
}, TaskCreationOptions.LongRunning);

  所以在需要设置线程是长时间运行的才需要使用 Task.Factory.StartNew,不然就使用 Task.Run,调用 Task.Run(foo) 就和使用下面代码一样:

Task.Factory.StartNew(foo, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

七 进程

1 Process获取当前进程信息

Process cur = Process.GetCurrentProcess();
//当前进程的id
Console.WriteLine(cur.Id);
//获取关联的进程的终端服务会话标识符。
Console.WriteLine(cur.SessionId);
//当前进程的名称
Console.WriteLine(cur.ProcessName);
//当前进程的启动时间
Console.WriteLine(cur.StartTime);
//获取关联进程终止时指定的值,在退出事件中使用
//Console.WriteLine(cur.ExitCode);
//获取进程的当前机器名称
Console.WriteLine(cur.MachineName); //.代表本地
//获取进程的主窗口标题。
Console.WriteLine(cur.MainWindowTitle);

2 进程中启动其他程序

  本文将简单介绍C#调用exe的方法。在C#中,通过 Process 类来进行进程操作。 Process类在 System.Diagnostics 包中。
  示例一

using System.Diagnostics;

Process p = Process.Start("notepad.exe");
p.WaitForExit();//等待外部程序退出后才能往下执行

  通过上述代码可以调用记事本程序,注意如果不是调用系统程序,则需要输入全路径。
  示例二
  当需要调用cmd程序时,使用上述调用方法会弹出令人讨厌的黑窗。如果要消除,则需要进行更详细的设置。
  Process 类的 StartInfo 属性包含了一些进程启动信息,其中比较重要的几个
  FileName:可执行程序文件名,Arguments:程序参数,已字符串形式输入;CreateNoWindow:是否不需要创建窗口;UseShellExecute:是否需要系统shell调用程序。
  通过上述几个参数可以让讨厌的黑屏消失

System.Diagnostics.Process exep = new System.Diagnostics.Process();
exep.StartInfo.FileName = binStr;
exep.StartInfo.Arguments = cmdStr;
exep.StartInfo.CreateNoWindow = true;
exep.StartInfo.UseShellExecute = false;
exep.Start();
exep.WaitForExit();//等待外部程序退出后才能往下执行

或者

System.Diagnostics.Process exep = new System.Diagnostics.Process();
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo();
startInfo.FileName = binStr;
startInfo.Arguments = cmdStr;
startInfo.CreateNoWindow = true;
startInfo.UseShellExecute = false;
exep.Start(startInfo);
exep.WaitForExit();//等待外部程序退出后才能往下执行

3 打开一个cmd命令行执行的程序

在这里插入图片描述

八 句柄

1 什么是句柄

  Windows 编程中,程序需要访问各种各样的资源,如文件、网络、窗口、图标和线程等。不同类型的资源被系统封装成不同的数据结构,当需要使用这些资源时,程序需要依据这些数据结构创建出不同的对象,当操作完毕并不再需要这些对象时,程序应当及时释放它们。在Windows中,应用程序不能直接在内存中操作这些对象,而是通过一系列公开的 Windows API 由对象管理器(Object Manager)来创建、访问、跟踪和销毁这些对象。当调用这些 API 创建对象时,它们并不直接返回指向对象的指针,而是会返回一个32位或64位的整数值,这个在进程或系统范围内唯一的整数值就是句柄(Handle)。随后程序再次访问对象,或者删除对象,都将句柄作为 Windows API 的参数来间接对这些对象进行操作。在这个过程中,句柄作为系统中对象的标识来使用。
  对象管理器是系统提供的用来统一管理所有 Windows 内部对象的系统组件。这里所说的内部对象,不同于高级编程语言如 C# 中“对象”的概念,而是由 Windows 内核或各个组件实现和使用的对象。这些对象及其结构,要么不对用户代码公开,要么只能使用句柄由封装好的 Windows API 进行操作。C# 编程中,多数情况下,我们并不需要与这些 Windows API 打交道,这是因为 .NET 类库对这些 API 又进行了封装,但我们的托管程序仍然会间接创建出很多 Windows 内部对象,并持有它们的句柄。
  如上所说,句柄是一个32位或64位的整数值(取决于操作系统),所以在32位系统中,C# 完全可以用int来表示一个句柄。但.NET提供了一个结构体 System.IntPtr 专门用来代表句柄或指针,在需要表示句柄,或者要在 unsafe 代码中使用指针时,应当使用 IntPtr 类型。

2 C#中创建文件句柄的过程

  举例来说,文件属于一种非托管的系统资源。在 C# 中,可以用 File 类的静态方法 Open 来得到一个 FileStream 对象,来对磁盘文件进行读写操作。FileStream 对象本身是托管对象,它是如何与文件这个非托管资源产生联系的呢?
在这里插入图片描述
  大致说来,C# 中打开文件的操作会经过下列步骤:
  1、调用 .NET 静态方法 System.IO.File.Open 时,File类会创建一个 FileStream 对象并传入必要的参数,如文件路径,FileModeFileAccess 选项。FileMode 枚举表明是希望创建新文件,打开已有文件,覆盖原有文件或是在原文件上追加新内容;FileAccess 枚举表明是希望读文件、写文件或两者都有。
  2、接着 FileStream 调用自己的 Init 方法进行初始化,在这个过程中,有更多细节需要考虑。为了创建一个文件,初始化方法需要更多额外的信息和检查,比如本进程在使用文件时是否允许其它进程读写文件,文件路径是否有效,是否有足够的权限,目标文件是否是允许被访问的文件类型,是否正确设置了 FileModeFileAccess 选项的组合等。
  3、完成这些必要的检查后,FileStream.Init 调用 Win32Native.SafeCreateFile 方法。
  4、Win32Native 类封闭了大量的 Windows APISafeCreateFile 方法以 P/Invoke 的方式调用 kernel32.dll 中的 CreateFile API ,并返回 SafeFileHandleSafeFileHandle 是一个有趣的类型,继承自 SafeHandle,包含了真正的 IntPtr 类型的文件句柄。.NET的设计者有意让这个句柄字段对外不可见,但如果你非要拿到这个句柄值,SafeFileHandle 也提供了 DangerousGetHandle() 方法满足你的要求:都告诉你 Dangerous 了,你自己看着办。
  5、包含着文件句柄的 SafeFileHandle 会被返回并存放在 FileStream 对象中。随后的读取和写入操作,FileStream 都会使用这个句柄与 Windows API 进行交互,直到最终关闭句柄。至始至终,我们的代码都无需直接关心句柄的存在,FileStream 负责了绝大部分工作。

3 通过句柄操作对象的好处

  Windows 不允许应用程序直接访问内存中更底层的对象,而是由对象管理器统一管理,总的来说,至少有以下好处:
  1、在操作系统层面上,为所有程序使用系统资源提供了统一的接口和机制。如果没有对象管理器,不同程序会有各种各样的实现方式来访问资源,并且这些代码散落在各种,难以规范,也无从协调解决资源的争用。
  2、将需要在系统级别保护的对象隔离起来,提供更高安全性。
  3、所有对系统关键资源的访问都经由对象管理器,使得系统可以方便的追踪和限制资源的使用,进行权限控制。

4 查看进程的句柄数量

  到现在为止,本文讨论的全是看不见的概念,有必要来直观的看一下系统中的句柄使用情况。有多种方式可以查看进程的句柄使用情况,先从两个工具开始,Windows 任务管理器和 Process Explorer。
  任务管理器默认不显示句柄数,需要在“查看”-“选择列”中勾选“句柄数”后,才会显示进程中当前打开的句柄数量。如下图所示,可以看到记事本进程当前打开59个句柄。
在这里插入图片描述
  系统自带的任务管理器查看句柄数量很方便,但如果想知道这些句柄具体是什么,可以使用 Process Explorer。Process Explorer 是 Windows Sysinternals 工具包中的一个进程查看器,可以从这里下载。如果你看到的视图跟下图不同,可以点击 View,选中 Show Lower Pane,并在 Lower Pane View 中选择 Handles。在列表中选择进程后,下方面板中会显示该进程中句柄的详细列表。
在这里插入图片描述

5 为什么关注句柄数

  句柄指向的是诸如窗口、线程、文件、菜单、进程和定时器之类的系统资源,和所有被称为“资源”的事物一样,稀缺性是它们共同的特点。对于计算机和操作系统来讲,内存是一种稀缺资源,而所有的句柄和对象都存储在内存中。基于这个事实,操作系统不允许进程无限制的创建对象和句柄。对于任务管理器中的“句柄数”来讲,每一进程允许打开的句柄数理论上来讲可达 2 24 2^{24} 224 个,但由于内存的限制,实际数字大打折扣。在我的测试中,32位的 .NET 进程“句柄数”在达到 1500万 以上后,程序开始出现各种各样的问题。事实上绝大多数程序不会使用到这么多句柄,除非特殊需要,在软件编程中,如果自己的程序“句柄数”上千甚至是几千时,就需要引起特别注意,这一般说明程序中已经存在句柄泄露的情况。
  你可能已经留意到,本文前面任务管理器中,除了显示进程的“句柄数”之外,还显示了“用户对象”和“GDI对象”的数量,它们属于另外两种句柄。具体的区别我们将在后面介绍,现在我们需要清楚的是,系统对于这两种对象同样设置了数量限制。对于“用户对象”和“GDI对象”来说,每个进程允许创建的数量上限是在注册表中设定的,分别是 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows 中的USERProcessHandleQuota 项和 GDIProcessHandleQuota 项,在Windows 7的32位操作系统上,两个项都被默认设置为10000。你可以更改这个设置,用户对象最多只能设定为18000个,GDI对象最多为65536个。但是改变这个设置是不被推荐的,一般情况下当你的应用程序需要用到超过10000个用户对象或GDI对象时,应该首先检查哪里出现了句柄泄露,而不是更改上限数量;另一方面,更改上限并不意味着应用程序就真的可以创建和使用这么多对象句柄,实际可用的数量同时受制于当前系统可用内存。
  windows 之所以要设立句柄,根本上源于内存管理机制的问题—虚拟地址,简而言之数据的地址需要变动,变动以后就需要有人来记录管理变动,(就好像户籍管理一样),因此系统用句柄来记载数据地址的变更。

DateTime lastTime1 = DateTime.Now.AddHours(1);
while (true)
{
    var a = lastTime1 - DateTime.Now;
    if (a.TotalHours <= 0)
    {
        break;
    }
    //DoSomething!
    Thread.Sleep(1000);
}

  在 DoSomeThing 里,如果用一个委托,去更新窗体控件,会发现任务管理器里的“句柄数”一栏,值会一点点的增加。而且通常都不怎么降。比如:

richTextBox1.Invoke(new EventHandler(delegate
{
    label3.Text ="xxxx";
}));

  不管这个匿名委托里有没有代码,句柄数都会增加,但是如果使用上下文同步对象给控件发送消息,这个问题就解决了。

_syncContext.Post(ReFreshUI, a);
private void ReFreshUI(object state)
{
    try
    {
        label3.Text = state.ToString();
    }
    catch
    {
    }
}

  而且我还发现一个有意思的问题。
  那就是如果在循环里,添上对 StatusStripLabel 控件进行更新的代码,句柄数也会增加,也是没有要停的意思。几分钟时间里居然升到了1000多。如果将这个控件换成Label控件,那么句柄数会保持不变。这算不算是 StatusStripLabel 这个控件的bug呢。
  另外,还有一个可能导致句柄数增加的。就是 很多人都会和我一样,写上这样的代码:

socket = new Socket(AddressFamily.InterNetwork, SocketType, SocketType == SocketType.Dgram ? ProtocolType.Udp : ProtocolType.Tcp);
IAsyncResult result = socket.BeginConnect(remoteEP, null, null);
if (result.AsyncWaitHandle.WaitOne(timeout, false) && socket.Connected)
{
    result.AsyncWaitHandle.Close();
    RemoteEndPoint = remoteEP;
    return true;
}

  经过测试发现。只要跑一轮这个代码,句柄数也会增加几个。而且跑完了也不会降下来。后面我想了下,result.AsyncWaitHandle 就是一个 WaitHandle 对象,而我们知道这个对象是需要显式地释放掉才行的。
  如果加上 result.AsyncWaitHandle.Close(); 问题就可以完美的解决了,句柄数一般都不会上升,甚至可能降一点。

八 ThreadPool.QueueUserWorkItem引发的血案,线程池异步非正确姿势导致程序闪退的问题

  本节内容来自这里

ThreadPool是.net System.Threading命名空间下的线程池对象。使用QueueUserWorkItem实现对异步委托的先进先出有序的回调。如果在回调的方法里面发生异常则应用程序会出现闪退。当然是指不处理那个异常的情况下。这不公司的CMS在生产环境频频出现闪退的情况。该死的是,原来用老机器配置不高的情况下没有出现过。换了更好的新机器后出现的。

//
// 摘要:
//     将方法排入队列以便执行,并指定包含该方法所用数据的对象。此方法在有线程池线程变得可用时执行。
//
// 参数:
//   callBack:
//     System.Threading.WaitCallback,它表示要执行的方法。
//
//   state:
//     包含方法所用数据的对象。
//
// 返回结果:
//     如果此方法成功排队,则为 true;如果未能将该工作项排队,则引发 System.NotSupportedException。
//
// 异常:
//   T:System.NotSupportedException:
//     承载公共语言运行时 (CLR) 的宿主不支持此操作。
//
//   T:System.ArgumentNullException:
//     callBack 为 null。
[SecuritySafeCritical]
public static bool QueueUserWorkItem(WaitCallback callBack, object state);

  经过一番测试重新了故障现象,但由于是生产环境代码不好大动,看来解决方案就是吞掉异常,让程序不再闪退一种解决办法了。
  编码测试过程:

using System;
using System.Threading;
namespace ConsoleShell3
{
    //164-184
    class Program
    {
        static object queueObj = new object();
        static CoreThreadPool pool = new CoreThreadPool();
        static void Main(string[] args)
        {           
            Console.WriteLine("Main Thread OK...");
            pool.Exceute += Pool_Exceute;
            pool.Start();
            pool.Post(queueObj);
            Thread thread = new Thread(() =>
            {
                while (true)
                {
                    Thread.Sleep(1000);
                    queueObj = (object)DateTime.Now.Ticks;
                    Console.WriteLine(DateTime.Now);
                    pool.Post(queueObj);
                }              
            });
            thread.Start();
            Console.ReadLine();
        }
 
        private static void Pool_Exceute(object obj)
        {
            ThreadPool.QueueUserWorkItem(CallbackDemoViod, obj);                     
        }
 
        /// <summary>
        /// 我的方案就是在这里把这个回调的方法用try catch包裹起来,吞到出现的异常
        /// </summary>
        /// <param name="obj"></param>
        private static void CallbackDemoViod(object obj)
        {
            try
            {
                var inObj = obj;
                var ex = new Exception("!!!!");
                throw ex;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
            //以下不catch异常就会导致闪退
            //var inObj = obj;
            //var ex = new Exception("!!!!");
            //throw ex;
        }
    }
}

  处理前后对比。
  处理前:
在这里插入图片描述
  处理后:
在这里插入图片描述

九 参考文章

  c#之task与thread区别及其使用:https://blog.csdn.net/qq_40677590/article/details/102797838
  c#中任务Task:https://blog.csdn.net/liyazhen2011/article/details/81262582
  走进异步编程的世界 - 剖析异步方法(上):https://www.cnblogs.com/liqingwen/p/5844095.html
  走进异步编程的世界 - 剖析异步方法(下):cnblogs.com/liqingwen/p/5866241.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值