[读书笔记] Threading in C# - PART 1: GETTING STARTED

本篇同步发文在个人Blog: [读书笔记] Threading in C# - PART 1: GETTING STARTED

前言

  这阵子换了新工作环境,公司使用不少C# Thread相关的技术,而知名书籍C# in a Nutshell的作者Joseph Albahari,将C# Thread的技术教学都免费公开,因此会阅读他的教学文来撰写读书笔记,希望在工作专案或Side Project都有帮助到。

  作者有一些程式码并非完整,我会尽量写出实际可执行的範例,且有些功能Net Core以后不支援,也会加上注明。以下正式开始。


Introduction and Concepts

Thread是独立的执行路径, 也能同时和其他Thread工作

C# Client程式(Console, wpf, winform等), CLR都会起单一的Main thread执行

被赋予工作的Thread, 只要那工作(function)完成, 该Thread也就结束,也无法重新工作

每个Thread会分配到记忆体独立的Stack区块, 所以function的变数能有地方储存

Thread如果参考到同一物件, 该物件的资料会共享. 如果用static的资料也一样是共享

但资料共享容易造成_Thread-Safe_的问题, 要特别处理, 像下面範例, 因为两个取到done的值都是false, 所以都会执行

    using System;    using System.Threading;        class ThreadTestWithSharedData    {        private bool done = false;        static void Main()        {            ThreadTestWithSharedData test = new ThreadTestWithSharedData();            Thread t = new Thread(test.GoMaybeNotSafe);            t.Start();            test.GoMaybeNotSafe();            Console.Read();        }            public void Go()        {            if(!done){                done = true;                Console.WriteLine("done");            }        }            public void GoMaybeNotSafe()        {            if(!done){                Console.WriteLine("done");                done = true;            }        }    }

使用Exclusive lock, 只允许一个thread运算

当Thread被Blocked, 不会消耗CPU资源

Join and Sleep

使用Join可等待Thread完成

使用Sleep让当前Thread暂停指定的时间

不管是Join或Sleep, 都是_Blocked_

Thread.Sleep(0) 将目前Thread的运算时间放其, 将CPU时间交给别的Thread, 等同功能是 Thread.Yield()

用Sleep(0)或Yield, 可以用来找thread safety的问题, 假如把Yield填入程式任何地方且出现问题, 代表这程式码有Bug

How Threading Works

在CLR里有个Thread Scheduler, 代表作业系统, 由它Thread的执行时间

单一处理器的系统, 切的time slice时间比switch context的时间还长

多处理器的系统, 切的time slice有concurrency, 可以同时执行多个thread

Thread如果被preempted(抢占), 代表它是被interrupted, 比如time-slicing

Threads vs Processes

多个Thread可以执行在1个Process

Process之间是互相隔离

Thread之间互相分享Heap记忆体的资料

Threading's Uses and Misuses(误用)

Maintaining a responsive user interface: 其他的Worker Thread可背后执行消耗的任务, 而Main(UI) Thread与User操作互动

Making efficient use of an otherwise blocked CPU:

Parallel programming: 在多核心/多处理器的环境, 多个执行绪能平行分担工作

Speculative(投机性) execution: 有些任务可以用多个演算法同时运算, 最终结果取最快运算完的.

Allowing requests to be processed simultaneously: .NET的Server功能(WCF、ASP.NET等) 收到Request, 会自动建立多执行绪来处理. Client也是可同样的作法.

强调多执行绪之间共用资料时, 都会有Bug的产生. 建议把多执行绪的逻辑能封装在独立的library, 也比较好测试

有些功能用太多执行绪不见得更快, 比如Disk IO, 只要几个thread读取 比 10几个thread还快

Creating and Starting Threads

Thread建立时会带入委託TheadStart, 但可以省略直接带functionc或匿名function

Passing Data to a Thread

可以在Thread.Start(someArgs)代入该function的参数

也可以用ParameterizedThreadStart, 但是function的参数必须用object, 再另外转型

Lambda expressions and captured variables: 传参数要注意共用性的问题, 下面的输出可能是0223557799, 而不是0~9各出现一次, 原因是有时多个Thread对i会存取到一样的

    for (int i = 0; i < 10; i++)      new Thread (() => Console.Write (i)).Start();

解决Captured variable的方法是指定变数:

    for (int i = 0; i < 10; i++)    {      int temp = i;      new Thread (() => Console.Write (temp)).Start();    }

Naming Threads

可以指定Thread的名字, 比较容易做Debug

用Thread.CurrentThread.Name = XXXX 指定名字

Foreground and Background Threads

Thread预设建立是Foreground, 代表它执行完才会让App结束

指定Thread.IsBackground = true, App终止时并不会理会Background的thread而强制终止

如果在程式要结束且有finally的background thread, 这thread也会被忽略掉, 解决方法有2

用Join

如果是Pooled thread, 可用event wait handler

如果有程式被任务管理员中止, 所有程式内的thread都会像background的直接中止

Thread Priority

Priority决定thread的执行时间长度

小心使用Priority, 否则可能造成对其他thread取资源的starvation

如果Process的Priority很低, 即使调高Thread的Priority也是会被限制资源

Process有个RealTime的Priority, 这会几乎抢佔所有作业系统的资源, 小心使用, 一般用High就好

如果要做RealTime的应用程式且包含使用者介面, 通常会拆开来, 使用者介面一个程式、后端运算是另一个程式, 彼此沟通用Remoting(WCF, Web Api之类)或memory-mapped files (C# in a Nutshell 有提到!! 没用过~~)

Exception Handling

建立Thread的try/catch/finally的scope, 无法捕捉到thread抛出的exception, 以下範例会直接抛出Exception, 而程式直接中止, 不会进到catch
    using System;    using System.Threading;        class ThreadThrowException    {        static void Main(string[] args){            try{                Thread t = new Thread(Go);                t.Start();            }            catch(Exception ex){                Console.WriteLine("Hi i am here" + ex.Message);            }                        Console.Read();        }            static void Go()        {            throw new Exception("Null");        }    }
将try/catch写在被Thread执行的function
    using System;    using System.Threading;        class ThreadThrowException2    {        static void Main(string[] args){            Thread t = new Thread(Go);            t.Start();                        Console.Read();        }            static void Go()        {            try{                throw new Exception("Null");            }            catch(Exception ex){                Console.WriteLine("Hi i am here" + ex.Message);            }                    }    }

Global的异常事件处理(WPF和Winform的Application.DispatcherUnhandledException和 Application.ThreadException), 只有Main UI thread抛出的异常才会处理, 其他Worker thread的异常要自己处理

AppDomain.CurrentDomain.UnhandledException会被任何异常触发, 但无法阻止后续程式的中止, 以下範例两个exception都会被UnhandledException捕捉, 但程式仍直接中止

    using System;    using System.Threading;        class ThreadThrowExceptionWithAppDomainHandler    {        static void Main(string[] args){            AppDomain currentDomain = AppDomain.CurrentDomain;            currentDomain.UnhandledException += new UnhandledExceptionEventHandler(MyHandler);            try{                Thread t = new Thread(Go);                t.Start();            }            catch(Exception ex){                Console.WriteLine("Hi i am here" + ex.Message);            }                        throw new Exception("TEST");                Console.Read();        }            static void Go()        {            throw new Exception("Null");        }            static void MyHandler(object s, UnhandledExceptionEventArgs args)        {            Exception e = (Exception) args.ExceptionObject;            Console.WriteLine("runtime terminating: {0} ", args.IsTerminating);        }    }

Threading Pool

使用Threading Pool的4种方式

Task Parallel Library

ThreadPool.QueueUserWorkItem

asynchronous delegates (BeginXXXXX...)

BackgroundWorker

以下是间接会用到Threading pool:

WCF, Remoting, ASP.NET, ASMX Web service等的应用程式Server

System.Timers.Timer和System.Threading.Timer

Net有用Async结尾的函式, 比如WebClient(使用event-based asynchronous pattern)和BeginXXXX开头的函式(asynchronous programming model pattern)

PLINQ

使用Threading Pool的注意事项

不能对Thread pool设定Name

thread pool都是background thread

block thread pool可能会造成一些潜在问题, 有一些优化的手法(比如ThreadPool.SetMinThreads)

Thread pool设过priority后, 任务执行完回收到pool会赋归成normal priority

可以用Thread.CurrentThread.IsThreadPoolThread 查看目前Thread是不是从pool来的

Entering the Thread Pool via TPL

新的Task类别使用Thread pool更简单

非泛型的Task类别取代ThreadPool.QueueUserWorkItem

泛型的Task类别取代asynchronous delegate (BeginXXXXX...)

非泛型的Task类别用Task.Factory.StartNew

会回传一个Task物件, 可以用Wait()等待, 而Task指定的函式发生Exception时, 会捕捉到

如果不对Task物件做Wait, 而中间发生的Exception会造成程式中止 ( 这个用Console程式无法成功, 主程式没被中止)

Task的结果可用.Result取得该Task回传的结果

在Task取Result有Exception时, 会包装在AggregateException, 没处理的话会让程式中止

Entering the Thread Pool Without TPL

ThreadPool.QueueUserWorkItem 和 asynchronous delegates都是不使用TPL而用Thread pool的方法, 差异在于asynchronous delegates可从thread回传资料、回传exception给caller

QueueUserWorkItem

像是new Thread一样, 代入void的function, 也能代入参数, 都包装在object

如果function有未处理的exception, 将造成程式中止

    using System;    using System.Threading;        class QueueUserWorkItem    {        static void Main(string[] args){                ThreadPool.QueueUserWorkItem(Go);            ThreadPool.QueueUserWorkItem(Go, 12345);            Console.Read();        }            static void Go(object data)        {            Console.WriteLine("Hello " + data);        }    }

Asynchronous delegates

能够回传值, 基于IAsyncResult

Asynchronous delegate和asynchronous methods不一样, 有些函式库也是用BeginXXX/EndXXX开头

使用Asynchronous delegates的流程:

建立要被委託的函式, 必需指定成Func类别

用Func的BeginInvoke呼叫该函式, 会回传IAsyncResult

用Func的EndInvoke代入IAsyncResult变数, 将取得结果

    using System;    using System.Threading;        class AsynchronousDelegate    {        static void Main(string[] args){            Func<string, int, string> task = Go;            IAsyncResult cookie = task.BeginInvoke("test", 123, null, null);            string result = task.EndInvoke(cookie);            Console.WriteLine("Result is " + result);            Console.Read();        }            static string Go(string name, int n)        {            return name + " and " + n.ToString();        }    }
EndInvoke会做3件事:

如果事情还未完成, 会等它完成

接收回传值

将Exception抛回至Caller

技术上来讲, 如果函式没有要回传值, 可以不呼叫EndInvoke, 但内部造成的Exception要小心. 所以建议都呼叫EndInvoke

另一种用法是把处理运算结果写在另一个委託函式, 该函式接收IAsyncResult的参数. 而不是在Caller呼叫 EndInvoke

    using System;    using System.Threading;        class AsynchronousDelegate2    {        static void Main(string[] args){            Func<string, int, string> task = Go;            task.BeginInvoke("test", 123, Done, task);            Console.Read();        }            static void Done(IAsyncResult cookie)        {            var target = (Func<string, int, string>) cookie.AsyncState;            string result = target.EndInvoke(cookie);            Console.WriteLine("Result is " + result);        }            static string Go(string name, int n)        {            return name + " and " + n.ToString();        }    }

Optimizing the Thread Pool

ThreadPool.SetMaxThreads可以设置Thread pool最多的Thread数量

每个环境有预设的上限

Framework 4.0 & 32-bit 可设1023个

Framework 4.0 & 64-bit 可设32768个

Framework 3.5 可设每个核心250个

Framework 2.0 可设每个核心25个

ThreadPool.SetMinThreads能设置最小的Thread数量, 预设是每个core会有1个

SetMinThreads能优化的状况是, 因为建立Thread会有延迟, 但如果SetMinThreads指定X个, 这X个Thread不要有延迟.

参考资料

Threading in C#, PART 1: GETTING STARTED, Joseph Albahari.C# 8.0 in a Nutshell: The Definitive Reference, Joseph Albahari (Amazon)

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章