• C#多线程


    C#多线程简单示例

    Thread类构造函数可以传入一个委托,作为线程调用的方法。

     1 using System;
     2 using System.Threading;
     3 
     4 namespace Test
     5 {
     6     public class Thread1
     7     {
     8         public static void ThreadFunc1()
     9         {
    10             while (true)
    11             {
    12                 Console.WriteLine("Thread 1!");
    13                 Thread.Sleep(1000);
    14             }
    15         }
    16 
    17         private int num = 5;
    18 
    19         public Thread1()
    20         {
    21             // 使用静态方法作为线程调用的方法,不带参数
    22             Thread thread1 = new Thread(ThreadFunc1);
    23             thread1.Start();
    24 
    25             // 使用成员方法作为线程调用的方法,可带一个 object 类型的参数
    26             Thread thread2 = new Thread(ThreadFunc2);
    27             thread2.Start(2);
    28         }
    29 
    30         private void ThreadFunc2(object obj)
    31         {
    32             int count = num * (int)obj;
    33             while (count > 0)
    34             {
    35                 Console.WriteLine("Thread 2!");
    36                 Thread.Sleep(1000);
    37                 --count;
    38             }
    39         }
    40     }
    41 }
    View Code

    Thread类的第二个参数可以控制堆栈大小,堆栈大小的简介如下:

    每个线程独立拥有一个可配置大小的堆栈,一个线程内所有函数使用到的堆栈都依赖于这个栈,如果太多的变量、参数需要使用栈,则可能导致栈溢出。目前基础平台子系统通过配置环境变量,将默认堆栈大小设置为128K,可以减少这个问题的出现,但业务系统在编码时仍然 需要注意栈的使用,避免出现问题。

        包括:
        1、不要在函数内部定义过大的局部变量,如过大的结构体变量,联合变量,过大的字符串,数组等;
        2、函数调用的深度也需要注意,如果函数 A 调用 B, B 再调用 C,而A/B/C每个函数定义了 10 K的局部变量,则总的栈空间需求将超过 30K;
        3、不要直接将大的结构变量通过函数参数传递,这样也会消耗栈空间,可以通过指针或者引用的方式传递;
        4、建议每个函数内部定义的变量大小控制在4-8K以下;
        5、如果在运行中 COREDUMP,并且通过 GDB 的 WHERE 命令时看到刚进入某个函数就报错,连函数内的第一条调试语句都无法指向,则基本可以认为是栈空间不够导致的,可以尝试将栈空间配置大一点,如果问题不再出现,则可以确定问题。这时需要按照前面几点的要求修改代码,减少栈的使用。 

    前台线程和后台线程

    所有前台线程关闭后,还有后台线程在运行的话,后台线程会全部关闭。

    主线程和通过Thread构造函数创建的线程默认都是前台线程,线程池获取的则默认是后台线程,通过 IsBackground 属性可以设置和获取当前线程是前台线程还是后台线程。

    执行优先级

    Priority属性(ThreadPriority枚举)可以控制线程执行的优先级,高优先级的线程会优先执行。

    同步

    当多个线程同时对一个数据进行修改时,就会因为无法控制其访问顺序导致的无法预知的错误,我们看看下面的代码:

     1 using System.Collections.Generic;
     2 using System;
     3 using System.Threading;
     4 
     5 namespace Test
     6 {
     7     public class Thread2
     8     {
     9         private List<int> _nums;
    10 
    11         public Thread2()
    12         {
    13             _nums = new List<int>();
    14 
    15             Thread thread1 = new Thread(ThreadFunc1);
    16             thread1.Start();
    17 
    18             Thread thread2 = new Thread(ThreadFunc2);
    19             thread2.Start();
    20 
    21             Console.WriteLine("线程已启动");
    22 
    23             Thread.Sleep(3000);
    24 
    25             string str = "";
    26             foreach (var item in _nums)
    27             {
    28                 str += item + ", ";
    29             }
    30             Console.WriteLine(str);
    31         }
    32 
    33         private void ThreadFunc1()
    34         {
    35             for (int i = 0; i < 10; i++)
    36             {
    37                 AddNum(i);
    38             }
    39         }
    40 
    41         private void ThreadFunc2()
    42         {
    43             for (int i = 10; i < 20; i++)
    44             {
    45                 AddNum(i);
    46             }
    47         }
    48 
    49         private void AddNum(int num)
    50         {
    51             _nums.Add(num);
    52         }
    53     }
    54 }
    View Code

    输出如下:

    0, 1, 2, 3, 14, 5, 6, 7, 8, 16, 17, 18, 19, 

    我们发现,由于可能同时调用AddNum方法,会导致_nums中的顺序和数量都出现问题。

    可以通过给AddNum方法加锁来解决两个线程同时访问同一块数据,加了lock代码块的代码,可以保证同一时刻只有一个线程会对其进行访问,如下:

     1 using System.Collections.Generic;
     2 using System;
     3 using System.Threading;
     4 
     5 namespace Test
     6 {
     7     public class Thread2
     8     {
     9         private List<int> _nums;
    10 
    11         public Thread2()
    12         {
    13             _nums = new List<int>();
    14 
    15             Thread thread1 = new Thread(ThreadFunc1);
    16             thread1.Start();
    17 
    18             Thread thread2 = new Thread(ThreadFunc2);
    19             thread2.Start();
    20 
    21             Console.WriteLine("线程已启动");
    22 
    23             Thread.Sleep(3000);
    24 
    25             string str = "";
    26             foreach (var item in _nums)
    27             {
    28                 str += item + ", ";
    29             }
    30             Console.WriteLine(str);
    31         }
    32 
    33         private void ThreadFunc1()
    34         {
    35             for (int i = 0; i < 10; i++)
    36             {
    37                 AddNum(i);
    38             }
    39         }
    40 
    41         private void ThreadFunc2()
    42         {
    43             for (int i = 10; i < 20; i++)
    44             {
    45                 AddNum(i);
    46             }
    47         }
    48 
    49         private void AddNum(int num)
    50         {
    51             lock (this)
    52             {
    53                 _nums.Add(num);
    54             }
    55         }
    56     }
    57 }
    View Code

    看起来输出正常了,但是其实是每个线程的代码执行速度很快,所以看不出来线程的切换,如下:

    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,

    下面我们在添加数字的地方加入一段比较耗时的方法,就会触发线程的切换了:

     1 using System.Collections.Generic;
     2 using System;
     3 using System.Threading;
     4 
     5 namespace Test
     6 {
     7     public class Thread2
     8     {
     9         private List<int> _nums;
    10 
    11         public Thread2()
    12         {
    13             _nums = new List<int>();
    14 
    15             Thread thread1 = new Thread(ThreadFunc1);
    16             thread1.Start();
    17 
    18             Thread thread2 = new Thread(ThreadFunc2);
    19             thread2.Start();
    20 
    21             Console.WriteLine("线程已启动");
    22 
    23             Thread.Sleep(3000);
    24 
    25             string str = "";
    26             foreach (var item in _nums)
    27             {
    28                 str += item + ", ";
    29             }
    30             Console.WriteLine(str);
    31         }
    32 
    33         private void ThreadFunc1()
    34         {
    35             for (int i = 0; i < 10; i++)
    36             {
    37                 AddNum(i);
    38             }
    39         }
    40 
    41         private void ThreadFunc2()
    42         {
    43             for (int i = 10; i < 20; i++)
    44             {
    45                 AddNum(i);
    46             }
    47         }
    48 
    49         private void AddNum(int num)
    50         {
    51             lock (this)
    52             {
    53                 _nums.Add(num);
    54                 largeComputationalCost();
    55             }
    56         }
    57 
    58         private void largeComputationalCost()
    59         {
    60             for (int i = 0; i < 10000000; i++)
    61             {
    62             }
    63         }
    64     }
    65 }
    View Code

    可以从结果看出来,数字的插入顺序是乱序的:

    0, 1, 10, 2, 3, 11, 12, 4, 5, 13, 14, 15, 6, 7, 8, 16, 9, 17, 18, 19, 

    lock

    被lock标记的代码块,会被加锁,指定同一时刻,只有一个线程可以执行代码块中的代码,需要注意的是,lock可以带一个参数,该参数用于标记代码块的加锁状态。

    下面我们简单的理解一下加锁参数的用法:

    1 lock (this)
    2 {
    3     // ...
    4 }
    • 代码运行到lock时,会先判断下this对象是否已经被标记已经被某个线程运行(注意只针对lock当前的代码块);
    • 如果没有任何线程在执行该代码块,则当前线程开始运行,并且标记this对象已经被当前线程运行;
    • 如果正在某线程运行中,则阻塞等待那个线程执行完毕,其它线程执行完毕后,则当前线程开始运行,并且标记this对象已经被当前线程运行;
    • 当前线程代码块执行完毕,标记this对象没有被任何线程运行;

    对lock的参数加深理解

    1. lock参数只能是引用类型,如果是值类型会怎样,我们看下面的例子:

    1 int a = 1;
    2 lock (a)
    3 {
    4     // ...
    5 }

    因为每次运行到这里,都会是一个新的值类型a,所以其它的线程给这个值类型a打了加锁标记后,下一个线程运行到这里会发现值类型a仍然是没有加锁的,lock代码块就变得毫无意义,多个线程仍然可以同时访问。

    2. 大部分的情况下,lock参数都是使用的this:

    当然这是因为,大部分情况下,我们多线程操作的都是当前对象实例的成员变量,多个对象的实例相互之间不需要加锁。

    我们也可以传递其它的引用实例来打加锁标记,但是需要注意只有相同的引用,才会保证只有一个线程访问lock代码块,我们看看下面比较极端的情况:

    1 MyClass a = new MyClass();
    2 lock (a)
    3 {
    4     // ...
    5 }

    这种情况和使用值类型一样,因为每次执行都会产生一个新对象,所以加lock代码是没有意义的,多个线程仍然可以同时访问。

    Monitor

    lock代码块可以看做是Monitor的语法糖,在IL代码中lock会被翻译成Monitor,也就是Monitor.Enter(obj)和Monitor.Exit(obj),如下:

     1 lock (this)
     2 {
     3     // ...
     4 }
     5 // 等同于下面这样
     6 try
     7 {
     8     Monitor.Enter(this);
     9     // ...
    10 }
    11 finally
    12 {
    13     Monitor.Exit(this);
    14 }

    Monitor还额外提供了一些功能:

    1. Monitor.TryEnter(obj, timespan),超过timespan的时间之后,就不执行这段代码了,而lock会一直等待从而出现死锁。

    2. Monitor.Wait()、Monitor.Pulse()和Monitor.PulseAll(),要弄清楚这3个方法的含义,需要先理解lock的下面的流程:

    对于同一个被lock的对象,会有下面3个属性:

    • 拥有锁的线程:当前正在执行的线程;
    • 就绪队列(ready queue):执行了lock、Monitor.Enter或Monitor.TryEnter的线程会放入该队列中,当拥有锁的线程释放锁之后,会让该队列中的下一个线程拥有锁并执行;
    • 等待队列(wait queue):放入该队列中的线程,不会在当拥有锁的线程释放锁之后让下一个执行,也不会加入到就绪队列中,会等待明确的指令来确定怎么处理队列中的线程;

    明白了上面的3个属性后,就可以具体看这3个方法了:

    • Monitor.Wait:将当前拥有锁的线程释放锁且阻塞,并将当前的线程添加到等待队列中;
    • Monitor.Pulse:将等待队列中一个线程移到就绪队列中;
    • Monitor.PulseAll:将等待队列中的所有线程都移到就绪队列中;

    其它3种同步方式

    下面说的3种同步方式都属于内核对象,利用内核对象进行进程或线程之间的同步,线程必须要在用户模式和内核模式间切换,所以一般效率较lock会低一些。

    不同于Monitor,这3种同步方法都可以在任意的地方对线程进行等待或者运行的控制。

    EventWaitHandler

    EventWaitHandle 类允许线程通过发信号互相通信。通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。

    Semaphore

    类似互斥锁,但它可以允许多个线程同时访问一个共享资源,通过使用一个计数器来控制对共享资源的访问,如果计数器大于0,就允许访问,如果等于0,就拒绝访问。计数器累计的是“许可证”的数目,为了访问某个资源。线程必须从信号量获取一个许可证。

    Mutex

    Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热闹。

    死锁

    当一个或多个进程等待系统资源,而资源又被进程本身或其它进程占用时,就形成了死锁。总的来说,就是两个线程,都需要获取对方锁占有的锁,才能够接着往下执行,但是这两个线程互不相让,你等我先释放,我也等你先释放,但谁都不肯先放,就一直在这僵持住了。

    我们看一个简单的示例:

     1 using System;
     2 using System.Threading;
     3 
     4 namespace Test
     5 {
     6     public class Thread3
     7     {
     8         private Object obj1 = new object();
     9         private Object obj2 = new object();
    10 
    11         public Thread3()
    12         {
    13             Thread thread1 = new Thread(ThreadFunc1);
    14             thread1.Start();
    15 
    16             Thread thread2 = new Thread(ThreadFunc2);
    17             thread2.Start();
    18         }
    19 
    20         private void ThreadFunc1()
    21         {
    22             lock (obj1)
    23             {
    24                 Console.WriteLine("开始执行方法一");
    25                 Thread.Sleep(1000);
    26                 lock (obj2)
    27                 {
    28                     Console.WriteLine("方法一执行完毕");
    29                 }
    30             }
    31         }
    32 
    33         private void ThreadFunc2()
    34         {
    35             lock (obj2)
    36             {
    37                 Console.WriteLine("开始执行方法二");
    38                 Thread.Sleep(1000);
    39                 lock (obj1)
    40                 {
    41                     Console.WriteLine("方法二执行完毕");
    42                 }
    43             }
    44         }
    45     }
    46 }
    View Code

    输出如下:

    开始执行方法一
    开始执行方法二

    避免死锁可以有下面几个方法:

    1. 应该尽量避免大量嵌套的锁的使用;
    2. 可以使用锁的超时机制来避免对资源的长时间占用;
    3. 通过逻辑上的检查来避免死锁;

    线程池

    线程池(ThreadPool)有下面几个特点:

    • 线程池中所有线程都是后台线程,如果进程的所有前台线程都结束了,所有的后台线程就会停止。不能把入池的线程改为前台线程。
    • 不能给入池的线程设置优先级或名称。
    • 入池的线程只能用于时间较短的任务。如果线程要一直运行(如Word的拼写检查器线程),就应使用Thread类创建一个线程。
    • 一个进程有且只能管理一个线程池。
    • 当进程启动时,线程池并不会自动创建。当第一次将回调方法排入队列(比如调用ThreadPool.QueueUserWorkItem方法)时才会创建线程池。
    • 在对一个工作项进行排队之后将无法取消它。
    • 线程池中线程在完成任务后并不会自动销毁,它会以挂起的状态返回线程池,如果应用程序再次向线程池发出请求,那么这个挂起的线程将激活并执行任务,而不会创建新线程,这将节约了很多开销。
    • 只有线程达到最大线程数量,系统才会以一定的算法销毁回收线程。

    不适合使用线程池的情形包括:

    • 如果需要使一个任务具有特定的优先级。
    • 如果具有可能会长时间运行(并因此阻塞其他任务)的任务。
    • 如果需要将线程放置到单线程单元中(线程池中的线程均处于多线程单元中)。
    • 如果需要用永久标识来标识和控制线程,比如想使用专用线程来中止该线程,将其挂起或按名称发现它。
    • 如果您需要运行与用户界面交互的后台线程,.NET Framework 2.0 版提供了 BackgroundWorker 组件,该组件可以使用事件与用户界面线程的跨线程封送进行通信。

    线程池的优势:

    • 可以避免创建和销毁消除的开支,从而可以实现更好的性能和系统稳定性。
    • 把线程交给系统进行管理,程序员不需要费力于线程管理,可以集中精力处理应用程序任务。

    我们看一个简单的示例:

     1 using System;
     2 using System.Threading;
     3 
     4 namespace Test
     5 {
     6     public class Thread4
     7     {
     8         public Thread4()
     9         {
    10             ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadFunc), 10);
    11             ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadFunc), 15);
    12 
    13             // 避免程序退出
    14             Thread.Sleep(5000);
    15         }
    16 
    17         private void ThreadFunc(object o)
    18         {
    19             for (int i = 0; i < (int)o; i++)
    20             {
    21                 Thread.Sleep(100);
    22             }
    23             Console.WriteLine("线程已执行完毕 " + o);
    24         }
    25     }
    26 }
    View Code

    输出如下:

    线程已执行完毕 10
    线程已执行完毕 15

    Task

    ThreadPool存在一些使用上的不便,比如:

    • ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
    • ThreadPool不支持线程执行的先后次序;

    而Task在线程池的基础上进行了优化,并提供了更多的API。

    我们看一个简单的例子:

     1 using System;
     2 using System.Threading;
     3 using System.Threading.Tasks;
     4 
     5 namespace Test
     6 {
     7     public class Thread5
     8     {
     9         public Thread5()
    10         {
    11             Task t = new Task(() =>
    12             {
    13                 Console.WriteLine("任务开始工作……");
    14                 // 模拟工作过程
    15                 Thread.Sleep(5000);
    16             });
    17             t.Start();
    18             t.ContinueWith((task) =>
    19             {
    20                 Console.WriteLine("任务完成,完成时候的状态为:");
    21                 Console.WriteLine("IsCanceled={0}	IsCompleted={1}	IsFaulted={2}", task.IsCanceled, task.IsCompleted, task.IsFaulted);
    22             });
    23 
    24             // 避免程序退出
    25             Thread.Sleep(6000);
    26         }
    27     }
    28 }
    View Code

    输出如下:

    任务开始工作……
    任务完成,完成时候的状态为:
    IsCanceled=False    IsCompleted=True    IsFaulted=False

    Parallel

    Parallel类提供了数据和任务的并行性;

    我们主要看下其For方法的使用,类似于C#的for循环语句,也是多次执行一个任务。使用Paraller.For()方法,可以并行运行迭代,迭代的顺序是乱序的。

    我们直接看一个例子:

     1 using System;
     2 using System.Threading;
     3 using System.Threading.Tasks;
     4 
     5 namespace Test
     6 {
     7     public class Thread6
     8     {
     9         public Thread6()
    10         {
    11             ParallelLoopResult result = Parallel.For(0, 10, new ParallelOptions() { MaxDegreeOfParallelism = 10 }, i =>
    12             {
    13                 Console.WriteLine("迭代次数:{0}, 任务ID:{1}, 线程ID:{2}", i, Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
    14                 Thread.Sleep(10);
    15             });
    16             Console.WriteLine("是否完成:{0}", result.IsCompleted);
    17         }
    18     }
    19 }
    View Code

    输出如下:

    迭代次数:2, 任务ID:2, 线程ID:6
    迭代次数:0, 任务ID:5, 线程ID:1
    迭代次数:1, 任务ID:1, 线程ID:4
    迭代次数:3, 任务ID:3, 线程ID:5
    迭代次数:4, 任务ID:4, 线程ID:7
    迭代次数:7, 任务ID:5, 线程ID:1
    迭代次数:6, 任务ID:4, 线程ID:7
    迭代次数:5, 任务ID:1, 线程ID:4
    迭代次数:8, 任务ID:2, 线程ID:6
    迭代次数:9, 任务ID:3, 线程ID:5
    是否完成:True

    Unity中使用多线程

    和C#中使用完全一致,需要注意的是,子线程不能操作和访问Unity的任何对象,需要通过发送消息到主线程来实现控制。

    天道酬勤,功不唐捐!
  • 相关阅读:
    视差插件parallarx
    经典幻灯片插件Swiper
    经典全屏滚动插件fullPage.js
    Dialog插件artDialog
    html5 canvas 做的一个时钟效果
    CSS3 Transitions, Transforms和Animation使用简介与应用展示
    微软官方下载地址
    Windows 7 下配置IIS,并且局域网内可访问(转载)
    C# 使用HttpWebRequest 实现文件的上传
    小图标网站
  • 原文地址:https://www.cnblogs.com/hammerc/p/14374934.html
Copyright © 2020-2023  润新知