.NET进阶篇07-async异步、thread多线程2

知识需要不断积累、总结和沉淀,思考和写作是成长的催化剂

一、线程Thread

.NET中线程操作封装为了Thread类,可以让开发者对线程进行直观操作。Thread提供了实例方法用于管理线程的生命周期和静态方法用于控制线程的一些访问存储等一些外在的属性,相当于工作空间环境变量了

1、生命周期

线程的生命周期有创建、启动、可能挂起、等待、恢复、异常、然后结束。用Thread类可以容易控制一个线程的全生命周期

Thread类的构造函数重载可以接受ThreadStart无参数和ParameterizedThreadStart有参数的委托,然后调用实例的Start()方法启动线程。Thread的构造函数的带有参数的委托,参数是一个object类型,因为我们可以传入任何信息

Thread t1 = new Thread(() => {
    Console.WriteLine($"新线程  {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
});
t1.Start();
Thread t2 = new Thread((obj) => {
    Console.WriteLine($"新线程  {Thread.CurrentThread.ManagedThreadId.ToString("00")},参数 {obj.ToString()}");
});
t2.Start("hello kitty");

线程启动后,可以调用线程的Suspend()挂起线程,线程就会处于休眠状态(不继续执行线程内代码),调用Resume()唤醒线程,还有一个不太建议使用的Abort()通过抛出异常的方式来销毁线程,随后线程的状态就会变为AbortRequested

常用的还有线程的等待,在主线程上启用工作线程后,有时需要等待工作线程的完成后,主线程才继续工作。可以调用实例方法Join(),当然我们可以传入时间参数来表明我主线程最多等你多久

2、后台线程

上一章我们知道Thread默认创建的是前台线程,前台线程会阻止系统进程的退出,就是启动之后一定要完成任务的后台线程会伴随着进程的退出而退出。通过设置属性IsBackground=true改为后台线程。另外还可以通过设置Priority指定线程的优先级。但这个并不总会如你所想设置了高优先级就一定最先执行。操作系统会优化调度,这也是线程不太好控制的原因之一

3、静态方法

上面介绍的都是Tread的实例方法,Thread还有一些常用静态方法。有时线程设置不当,会有些意想不到的的bug

1.线程本地存储

AllocateDataSlot和AllocateNamedDataSlot用于给所有线程分配一个数据槽。像下面例子所示,如果不在子线程中给数据槽中放入数据,是获取不到其他线程往里面放的数据。

var slot= Thread.AllocateNamedDataSlot("testSlot");
//Thread.FreeNamedDataSlot("testSlot");
Thread.SetData(slot, "hello kitty");
Thread t1 = new Thread(() => {
    //Thread.SetData(slot, "hello kitty");
    var obj = Thread.GetData(slot);
    Console.WriteLine($"子线程:{obj}");//obj没有值
});
t1.Start();

var obj2 = Thread.GetData(slot);
Console.WriteLine($"主线程:{obj2}");

在声明数据槽的时候.NET提醒我们如果要更好的性能,请使用ThreadStaticAttribute标记字段。什么意思?我们来看下面这个例子

//
// 摘要:
//     在所有线程上分配未命名的数据槽。 为了获得更好的性能,请改用以 System.ThreadStaticAttribute 特性标记的字段。
//
// 返回结果:
//     所有线程上已分配的命名数据槽。
public static LocalDataStoreSlot AllocateDataSlot();

例子中的如果不在静态字段上标记ThreadStatic输出结果就会一致。ThreadStatic标记指示各线程的静态字段值是否唯一

[ThreadStatic]
static string name = string.Empty;
public void Function()
{
    name = "kitty";
    Thread t1 = new Thread(() => {
        Console.WriteLine($"子线程:{name}");//输出空
    });
    t1.Start();
    Console.WriteLine($"主线程:{name}");//输出kitty
}

还有一个ThreadLocal提供线程数据的本地存储,用法和上面一样,在每个线程中声明数据仅供自己使用

ThreadLocal<string> local = new ThreadLocal<string>() { };
local.Value = "hello kitty";
Thread t = new Thread(() => {
    Console.WriteLine($"子线程:{local.Value}");
});
t.Start();
Console.WriteLine($"主线程:{local.Value}");

上面的静态方法用于线程的本地存储TLS(Thread Local Storage),Thread.Sleep方法在开发调试时也是经常用的,让线程挂起指定的时间来模拟耗时操作

2.内存栅栏

先说一个常识问题,为什么我们发布版本时候要用Release发布?Release更小更快,做了很多优化,但优化对我们是透明的(计算机里透明认为是个黑盒子,内部逻辑细节对我们不开放,和生活中透明意味着完全掌握了解不欺瞒刚好相反),一般优化不会影响程序的运行,我们先借用网上的一个例子

bool isStop = false;
Thread t = new Thread(() => {
    bool isSuccess = false;
    while (!isStop)
    {
        isSuccess = !isStop;
    }
});
t.Start();
Thread.Sleep(1000);
isStop = true;
t.Join();
Console.WriteLine("主线程执行结束");

上面例子如果在debug下能正确执行完直到输出“主程序执行结束”,然而在release下却一直会等待子线程的完成。这里子线程中isStop一直为false。首先这是一个由多线程共享变量引起的问题,所以我们建议最好的解决办法就是尽量不共享变量,其次可以使用Thread.MemoryBarrier和VolatileRead/Write以及其他锁机制牺牲一点性能来换取数据的安全。(上面例子测试如果在子线程while中进行Console.writeLine操作,奇怪的发现release下也能正常输出了,猜测应该是进行了内存数据的更新)

release优化会将t线程中的isStop变量的值加载到CPU Cache中,而主线程修改了isStop值在内存中,所以子线程拿不到更新后的值,造成数据不一致。那么解决办法就是取值时从内存中获取。Thread.MemoryBarrier()就可以让在此方法之前的内存写入都及时的从CPU Cache中更新到内存中,在此之后的内存读取都要从内存中获取,而不是CPU Cache。在例子中的while内增加Thread.MemoryBarrier()就能避免数据不一致问题。VolatileRead/Write是对MemoryBarrier的分开解释,从处理器读取,从处理器写入。

4、返回值

前面声明线程时,可以传递参数,那么想要有返回值该如何去做呢?Thread并没有提供返回值的操作,后面.NET给出的对Thead的高级封装给出了解决方案,直接使用即可。那目前我们使用thread类就要自己实现下带有返回值的线程操作,都是通过委托实现的,这里简单介绍一种,(共享外部变量也是可以,不建议)

private Func<T> ThreadWithReturn<T>(Func<T> func)
{
    T t = default(T);
    Thread thread = new Thread(() =>
    {
        t = func.Invoke();
    });
    thread.Start();
    return () =>
    {
        thread.Join();
        return t;
    };
}
//调用
Func<int> func = this.ThreadWithReturn<int>(() =>
{
    Thread.Sleep(2000);
    return DateTime.Now.Millisecond;
});
int iResult = func.Invoke();

二、线程池ThreadPool

.NET起初提供Thread线程类,功能很丰富,API也很多,所以使用起来比较困难,况且线程还不都是很像理想中运行,所以从2.0开始提供了ThreadPool线程池静态类,全是静态方法,隐藏了诸多Thread的接口,让线程使用起来更轻松。线程池可用于执行任务、发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器

1、工作队列

常用ThreadPool线程池静态方法QueueUserWorkItem用于将方法排入线程池队列中执行,如果线程池中有闲置线程就会执行,QueueUserWorkItem方法的参数可以指定一个回调函数委托并且传入参数,像下面这样

ThreadPool.QueueUserWorkItem((obj) => {
                Console.WriteLine($"线程池中线程  {Thread.CurrentThread.ManagedThreadId.ToString("00")} ,传入 {obj.ToString()}");
            },"hello kitty");

2、工作线程和IO线程

一般异步任务的执行,不涉及到网络文件等IO操作的,计算密集型,开发者来调用。而IO线程一般用在文件网络上,是CLR调用的,开发者无需管。工作线程发起文件访问调用,由驱动器完成后通知IO线程,IO线程则执行异步任务的回调函数

image

获取和设置最小最大的工作线程和IO线程

ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads);
ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads);
ThreadPool.SetMaxThreads(16, 16);
ThreadPool.SetMinThreads(8, 8);

3、和Thread区别

如果计算机只有8个核,同时可以有8个任务运行。现在我们有10个任务需要运行,用Thread就需要创建10个线程,用ThreadPool可能只需要利用8个线程就行,节约了空间和时间。线程池中的线程默认先启动最小线程数量的线程,然后根据需要增减数量。线程池使用起来简单,但也有一些限制,线程池中的线程都是后台线程,不能设置优先级,常用于耗时较短的任务。线程池中线程也可以阻塞等待,利用ManualResetEvent去通知,但一般不会使用。

4、定时器

.NET中有很多可以实现定时器的功能,在ThreadPool中,我们可以利用RegisterWaitForSingleObject来注册一个指定时间的委托等待。像下面这样,将每隔一秒就输出消息

ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(true), new WaitOrTimerCallback((obj, b) =>
{
    Console.WriteLine($"obj={obj},tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
}),"hello kitty",1000,false);

我们平常见过比较多的还是timer类,timer类在.net内是好几个地方都有的,在System.Threading、
System.Timer、System.Windows.Form、System.Web.UI等里面都有Timer,后面都是在第一个System.Threading里的Timer扩展

System.Threading.Timer timer = new System.Threading.Timer((obj) =>
{
    Console.WriteLine($"obj={obj},tid={Thread.CurrentThread.ManagedThreadId},datetime={DateTime.Now}");
},"hello kitty",1000,1000);

timer的底层有一个TimerQueue,利用ThreadPool.UnsafeQueueUserWorkItem来完成定时功能,和上面我们使用的ThreadPool定时器有一点区别

实际开发中,简单定时timer就够用,但一般业务场景比较复杂,需要定制个性化的定时器,比如每月几号执行,每月第几个星期几,几点执行,工作日执行等。因此我们使用Quarz.NET定时框架,后面框架整合时会用到,用起来也是很简单的

先就啰嗦这两点吧,下一篇应该是Task、Parallel以及Async/Await,然后总结介绍下C#的线程模式、线程同步锁机制、异常处理,线程取消,线程安全集合和常见的线程问题

天长水阔,见字如面,随缘更新,拜了个拜~

可关注主页公号获取更多哈

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容