[读书笔记] 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

  1. 用Join

  2. 如果是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种方式
  1. Task Parallel Library

  2. ThreadPool.QueueUserWorkItem

  3. asynchronous delegates (BeginXXXXX...)

  4. BackgroundWorker

  • 以下是间接会用到Threading pool:
  1. WCF, Remoting, ASP.NET, ASMX Web service等的应用程序Server

  2. System.Timers.Timer和System.Threading.Timer

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

  4. PLINQ

  • 使用Threading Pool的注意事项
  1. 不能对Thread pool设定Name

  2. thread pool都是background thread

  3. 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的流程:

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

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

  3. 用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件事:
  1. 如果事情还未完成, 会等它完成

  2. 接收回传值

  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数量

  • 每个环境有预设的上限

  1. Framework 4.0 & 32-bit 可设1023个

  2. Framework 4.0 & 64-bit 可设32768个

  3. Framework 3.5 可设每个核心250个

  4. Framework 2.0 可设每个核心25个

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

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

参考资料

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

<<:  Day 8 - 目前(传统)的机器学习三步骤(3)-训练

>>:  PHP & MySQL 连结资料库进行增、删、改、查

DAY 16- DHKE、ECDH、ElGamal

有趣的简写越来越多了,而且越来越长了... 我个人认为这个章节应该摆在对称加密和非对称密码的中间来介...

Day 1:开始前的准备

缘起 各位好~我是一个软件工程师,追求每年都要有不一样的进步,今年追求的是把自己的基底在打的更稳,之...

Day37. 原型模式

本文同步更新於blog Prototype Pattern 当创建实例的过程很昂贵或复杂时,透过拷...

[NestJS 带你飞!] DAY25 - Authorization & RBAC

现在的企业会使用一些管理系统来管理人力等资源,而这些管理系统通常都会有所谓的 权限设计 (Permi...

【Day 18】Complexity & Graphs

接下来我们要针对复杂度做介绍,首先要说的就是高手们常常说的「Big O」! 但是到底什麽是 big ...