C# 多线程异步编程笔记

本文转载连接: https://blog.youkuaiyun.com/AngOn823/article/details/78647693

为什么要异步编程
1、避免性能瓶颈
从磁盘里读取数据,并对数据处理。同步方式,速度有限,必须读完再处理。 异步方式,一个线程读,一个线程处理,有可能读完了就处理完了。
2、增强应用程序的总体响应能力
异步对可能引起阻塞的活动至关重要。 比如 web对资源的 访问,UI界面响应。如果在一个同步应用程序中有任何的线程被阻塞了,那么所有线程都将被阻塞。 应用程序停止响应,使用异步方法时,将可能会引起阻塞的操作放在一个独立的线程中,主线程继续做其它事情。
下面记录几种异步编程的方式

一、Thread
最简单,最直接,最原始的多线程编程
Thread thread = new Thread ( new ThreadStart(callback));
Thread thread = new Thread ( new ParameterizedThreadStart(callback))); //带一个参数
 
thread.start();
thread.start(object parameter);

除非指定是后台线程,否则都是前台线程。
后台线程当主线程自己执行完后,不会等待子线程结束再退出。除非手动指定join。

thread.IsBackground = true; // 设置为后台线程
thread.Join();
callback 的返回值必须为 void ,可以带一个object类型的参数
二、ThreadPool 线程池
创建和销毁线程是一个要耗费大量时间的过程,另外,太多的线程也会浪费内存资源,所以通过Thread类来创建过多的线程反而有损于性能,为了改善这样的问题 ,.net中就引入了线程池。

线程池形象的表示就是存放应用程序中使用的线程的一个集合(就是放线程的地方,这样线程都放在一个地方就好管理了)。CLR(公共语言运行库)初始化时,线程池中是没有线程的,在内部, 线程池维护了一个操作请求队列,当应用程序想执行一个异步操作时,就调用一个方法,就将一个任务放到线程池的队列中,线程池中代码从队列中提取任务,将这个任务委派给一个线程池线程去执行,当线程池线程完成任务时,线程不会被销毁,而是返回到线程池中,等待响应另一个请求。由于线程不被销毁, 这样就可以避免因为创建线程所产生的性能损失。


线程池中分且分为两种线程:

工作者线程(workerThreads)和I/O线程 (completionPortThreads)

I/O线程 顾名思义 是用于 与外部系统交换信息(input/output)。

除了与外部交换信息的线程,其余都可以看做工作者线程。

除了Thread 类之外,不管使用委托还是task,或者是一些异步的方法(异步I/O,异步Socket,异步WebRequest)创建的都线程都是在线程池里面,由线程池管理。

线程池里的线程都全部是后台线程

直观来说,ThreadPool是一个静态类,所有线程共享。

使用ThreadPool创建一个工作者线程,用户并不需要显式使用类似new Thread的语句。正如上面所说,CLR初始化时,线程池中是没有线程的,此时添加线程, 线程池才可能才需要new一个新的线程,但也是线程池内部自己管理,用户并不知道。而当线程池线程完成任务时,线程不会被销毁,而是返回到线程池中,等待响应另一个请求,所以当线程池有空闲线程时,就更不用new了。

本文所说的“创建一个工作者线程”,都是指上面含义,不是说每次都真的new了新的线程。

代码示例:


class Program
{
 
    static void Main(string[] args)
    {
        ThreadPool.SetMaxThreads(1000, 1000);
        ThreadPoolMessage("Main Thread");
        ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback2), "hello");
        ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback), "world");
        for (int n = 0; n < 5; n++)
            Console.WriteLine("  Main thread do work!");
        Console.WriteLine("");
        Console.ReadKey();
    }
 
    static void AsyncCallback(object result)
    {
        ThreadPoolMessage("AsyncCallback");
        Console.WriteLine(result as string);
    }
 
    static void AsyncCallback2(object result)
    {
        ThreadPoolMessage("AsyncCallback2");
        Console.WriteLine(result as string);
    }
 
    static void ThreadPoolMessage(string data)
    {
        int a, b;
        ThreadPool.GetAvailableThreads(out a, out b);
        string message = string.Format("{0}\n  CurrentThreadId is {1}\n  " +
                     "WorkerThreads is:{2}  CompletionPortThreads is :{3}",
                     data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
        Console.WriteLine(message);
    }    
}

QueueUserWorkItem创建一个线程,不用start()之类,callback函数中的内容将直接在另一个线程中被执行。

callback 的返回值必须为 void ,可以带一个object类型的参数

GetAvailableThreads(out int ,out int)方法可以获取当前线程池中还有多少可用的工作者和I/O线程。

运行上面的程序可以发现,调用一次QueueUserWorkItem之后,可用线程少了一个。

三、使用delegate(委托)
通过调用ThreadPool的QueueUserWorkItem方法来来启动工作者线程非常方便,但是WaitCallback只能带一个参数,并且返回值一定是void,如果我们实际操作中需要有返回值,或者需要带有多个参数,这时通过这样的方式实现就要多费周折。为了解决这样的问题,我们可以通过委托来建立工作者线程。

 

异步委托主要就是依靠两个函数:(如果你还不知道什么是Invoke先去了解一下C#的委托)


IAsyncResult  BeginInvoke (..., callback, object obj);
var          EndInvoke   (IAsyncResult  result);
讲到这里就不得不说一下 Beginxxx 和 Endxxx 类型的异步了……

Beginxxx 和 Endxxx 模式 都有一个共同的特点。那就是在 Beginxxx里面启动异步模式,即开始进入了另一个线程。而在Beginxxx函数里一定有一个参数是callback函数。然后再在这个callback函数里调用Endxxx,结束异步模式。.

还是说委托,先看一个代码


class Program
{
    delegate int MyDelegate(string name, int x);
 
    static void Main(string[] args)
    {
        ThreadPoolMessage("Main Thread");
 
        MyDelegate myDelegate = new MyDelegate(HelloInNewThread);
 
        IAsyncResult result = myDelegate.BeginInvoke("Leslie", 6, new AsyncCallback(AsyncCallback), "hello");
 
        for (int n = 0; n < 5; n++)
            Console.WriteLine("  Main thread do work!");
        Console.ReadKey();
    }
 
    static int HelloInNewThread(string name, int x)
    {
        Console.WriteLine(x);
        ThreadPoolMessage("Async Thread");
        Thread.Sleep(2000);            //虚拟异步工作
        return x;// "HelloInNewThread " + name;
    }
 
    static void AsyncCallback(IAsyncResult result)
    {
        ThreadPoolMessage("AsyncCallback");
        Console.WriteLine(result.AsyncState as string);
        AsyncResult _result = (AsyncResult)result;
        MyDelegate myDelegate = (MyDelegate)_result.AsyncDelegate;
        int data = myDelegate.EndInvoke(result);
        Console.WriteLine(data);
    }
 
    static void ThreadPoolMessage(string data)
    {
        int a, b;
        ThreadPool.GetAvailableThreads(out a, out b);
        string message = string.Format("{0}\n  CurrentThreadId is {1}\n  " +
                     "WorkerThreads is:{2}  CompletionPortThreads is :{3}",
                     data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
        Console.WriteLine(message);
    }
}
在这里,通过委托将HelloInNewThread这个函数放到了新的线程里异步执行。

具体:

BeginInvoke的前几个参数对应myDelegate绑定的函数的参数,

倒数第二个参数是callback,和之前的不同,这里的callback 带一个IAsyncResult 的参数,返回值还是void。

IAsyncResult 是一个结构体。

public interface IAsyncResult
{
    object AsyncState { get; }
    WaitHandle AsyncWaitHandle { get; }
    bool CompletedSynchronously { get; }
    bool IsCompleted { get; }
}
最后一个参数可以通过 IAsyncResult的AsyncState取得。

BeginInvoke回一个IAsyncResult对象

 

EndInvoke的参数是BeginInvoke返回的那个IAsyncResult对象

intdata = myDelegate.EndInvoke(result);

继续,AsyncResult 也是一个结构体,并且实现了接口IAsyncResult

public class AsyncResult : IAsyncResult, IMessageSink
{
    public virtual object AsyncDelegate { get; }
    public virtual object AsyncState { get; }
    public virtual WaitHandle AsyncWaitHandle { get; }
    public virtual bool CompletedSynchronously { get; }
    public bool EndInvokeCalled { get; set; }
}
Callback的参数为 IAsyncResult ,要先把它强制转换成AsyncResult ,AsyncResult 中的AsyncDelegate 记录了 BeginInvoke的那个委托,再调用这个委托的EndInvoke方法结束委托。

(这里测试 EndInvoke的参数为 IAsyncResult 或 AsyncResult 类型都可以)

最后, BeginInvoke是在原线程中被执行, hello 和EndInvoke都是在新的线程中被执行。

除了 BeginInvoke 与 EndInvoke之外,
还有诸如以下类似的Begin/End 异步操作。
BeginWrite 与 EndWrite // 异步写 (I/O线程)
BeginRead 与 EndRead // 异步读 (I/O线程)
BeginGetRequestStream 与  EndGetRequestStream  // 异步webRequest
BeginGetResponse 与 EndGetResponse 等

这种类型的异步 callback 的参数都必须指定为IAsyncResult类型

大同小异,都是在Begin之后进入异步。要执行的内容完毕之后,调用callback,

并且在callback中调用End方法,End方法的参数是Begin方法的返回值。

Callback函数也都是在另一线程中执行,不会影响到主线程。

 

使用Beginxxx/Endxxx 创建的都线程都是在线程池里面,由线程池管理。

四、Task
Thread 和 ThreadPool中的QueueUserWorkItem都不带返回值。

委托使我们可以使用带返回值,带多个参数的工作者线程。但是并不方便。

作为更高级的Task,可以带参数,也可以有返回值,并且使用简单。

 

System.Threading.Tasks中的类被统称为任务并行库(Task Parallel Library,TPL),TPL使用CLR线程池把工作分配到CPU,并能自动处理工作分区、线程调度、取消支持、状态管理以及其他低级别的细节操作,极大地简化了多线程的开发。

 

要使用Task创建工作者线程,最好对C# lambda表达有所了解。

 

用Task创建一个线程很简单。主要是两种方法:

Task.Run(run_thread);
Task.Factory.StartNew(run_thread);
两者似乎没有什么差别。都是在另一个线程里运行run_thread中的内容,run_thread可以是任何函数,最多带16个参数,可以有返回值。

看一个代码:


class Program
{
    static void Main(string[] args)
    {
        Task task1 = Task.Run(() => run_thread("hello") );
 
        Task task2 = Task.Factory.StartNew(() => run_thread("world") );
 
        Task<string> task3 = Task.Factory.StartNew<string>(() => run_thread("return") );
 
        task1.Wait(); // Join
        task2.Wait();
        task3.Wait();
        Console.WriteLine("Main: " + task3.Result);
 
        Console.ReadKey();
    }
 
    public static string run_thread(string str)
    {
        ThreadPoolMessage("AsyncCallback");
        Console.WriteLine(str);
        return str.ToUpper();
    }
    static void ThreadPoolMessage(string data)
    {
        int a, b;
        ThreadPool.GetAvailableThreads(out a, out b);
        string message = string.Format("{0}\n  CurrentThreadId is {1}\n  " +
                       "IsBackground: {2}\n  "+
                     "WorkerThreads is:{3}  CompletionPortThreads is :{4}",
                     data, Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsBackground.ToString(),a.ToString(), b.ToString());
        Console.WriteLine(message);
    }
}


直观来说,task1 和 task2 没什么差别。

他们都传递了一个string类型的参数,但是并没有捕获run_thread的返回值。

task3 后面跟了一个泛型参数,这个参数的类型要必须和run_thread的返回值匹配,参数的个数最多为16个。

通过task3.Result就可以取得run_thread的返回值了。

 

Task结合lambda表达式使用起来非常方便,也是msdn推荐使用的方法。


总结:
从Thread 到 Task,并行编程越来越方便。上文所提到的也仅仅只是异步编程的几种方式,还有很多细节完全没有理会,实际编程中肯定还会碰到各种各样的问题。

还有一些异步编程模式没有提到:

异步I/O(I/0线程)

异步SqlCommand

数据并行

await和 async

 

后面三者个人感觉实际上用上的时候并不多,特别是2,3两点,一般人可能很难遇到要使用它的场景。

await和 async 是为了用同步的代码逻辑编写异步代码,是比较新的东西,好用但是理解起来有点麻烦。

而异步I/O大多也是开一个线程读数据一个线程处理数据。用同步机制保证安全。

 

对于我来说……目前来线程池都还没用过,日常工作中Thread完全就能满足我的需求了。

 

对C# 接触不久,对于它的核心 delegate 理解还不是很深,这篇文章我隔段时间就会读一遍,然后按照新的理解再修改。

 

参考:

http://www.cnblogs.com/leslies2/archive/2012/02/08/2320914.html#t6

https://msdn.microsoft.com/zh-cn/library/system.threading.tasks.task(v=vs.110).aspx
--------------------- 
作者:姜团长 
来源:优快云 
原文:https://blog.youkuaiyun.com/AngOn823/article/details/78647693 
版权声明:本文为博主原创文章,转载请附上博文链接!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值