.NET使用多线程编程

原外文地址可以点击此处查看

多线程简介

本文介绍了多线程的工作原理。您将了解操作系统如何管理线程执行,并向您展示如何在程序中操作Thread类来创建和启动托管线程。包含的知识点有线程创建,竞争条件,死锁,监视器,互斥锁,同步和信号量等。

多线程概述

线程是程序中的独立指令流。线程类似于串行执行程序。但是,单独一个线程本身不是一个程序,它不能独立运行,而是在程序的上下文中运行。

线程的实际用法不是关于单个串行线程,而是在单个程序中使用多个线程。同时运行并执行各种任务的多个线程称为多线程。线程被认为是一个轻量级进程,因为它在程序的上下文中运行并使用分配给该程序的资源。
.NET使用多线程编程_第1张图片
使用任务管理器,您可以打开“进程”(原文是线程感觉是手误写错?)列,查看每个进程的进程和线程数。在这里,您可以注意到只有cmd.exe具有单个线程,而所有其他应用程序使用多个线程。
.NET使用多线程编程_第2张图片
操作系统调度线程。线程具有优先级,并且每个线程都有自己的堆栈,但程序代码和堆的内存在单个进程中所有线程之间共享。

进程由一个或多个执行线程组成。进程始终由至少一个称为主线程的线程(C#程序中的Main()方法)组成。单线程进程只包含一个线程,而多线程进程包含多个执行线程。
.NET使用多线程编程_第3张图片
在计算机上,操作系统加载并启动应用程序。每个应用程序或服务在计算机上作为单独的进程运行。下图说明实际运行的进程比系统中运行的实际应用程序多。其中许多进程是后台操作系统进程,它们在操作系统加载时自动启动。
.NET使用多线程编程_第4张图片
System.Threading命名空间
与许多其他功能一样,在.NET中,System.Threading是提供各种类型的命名空间,以帮助构建多线程应用程序。

类型 描述
Thread 它表示在CLR中执行的线程。使用它,我们可以在应用程序域中生成其他线程。
Mutex 它用于应用程序域之间的同步。
Monitor 它使用Locks和Wait实现对象的同步。
Smaphore 它允许限制可以同时访问资源的线程数。
Interlock 它为多个线程共享的变量提供原子操作。
ThreadPool 它用于与CLR维护的线程池进行交互。
ThreadPriority 它表示线程优先级,例如高,正常,低。

System.Threading.Thread类

Thread类允许您在程序中创建和管理托管线程的执行。这些线程称为托管线程。

成员 类型 描述
CurrentThread Static 返回当前运行线程的引用。
Sleep Static 暂停当前​​线程特定的持续时间。
GetDoamin Static 返回当前应用程序域的引用。
CurrentContext Static 返回当前正在运行的线程的当前上下文的引用。
Priority Instance level 获取或设置线程优先级。
IsAlive Instance level 以True或False值的形式获取线程状态。
Start Instance level 指示CLR启动线程。
Suspend Instance level 挂起线程。
Resume Instance level 恢复以前挂起的线程。
Abort Instance level 指示CLR终止线程。
Name Instance level 允许建立线程名称。
IsBackground Instance level 指示线程是否在后台运行。

多线程实现

获取当前线程信息
为了说明Thread类型的基本用法,我们创建一个控制台应用程序,其中CurrentThread属性来获取当前正在执行的线程的Thread对象。

using System;  
using System.Threading;  
namespace threading  
{  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            Console.WriteLine("**********Current Thread Informations***************\n");  
            Thread t = Thread.CurrentThread;  
            t.Name = "Primary_Thread";  
            Console.WriteLine("Thread Name: {0}", t.Name);  
            Console.WriteLine("Thread Status: {0}", t.IsAlive);  
            Console.WriteLine("Priority: {0}", t.Priority);  
            Console.WriteLine("Context ID: {0}", Thread.CurrentContext.ContextID);  
            Console.WriteLine("Current application domain: {0}",Thread.GetDomain().FriendlyName);  
            Console.ReadKey();   
        }  
    }  
}  

编译完这个应用程序后,输出结果如下:
.NET使用多线程编程_第5张图片
简单线程创建
以下示例说明Thread类的实现,其中Thread类的构造函数接受委托参数。创建Thread类对象后,可以使用Start()方法启动该线程,如下所示;

using System;  
using System.Threading;  
namespace threading  
{  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            Thread t = new Thread(myFun);  
             t.Start();  
            Console.WriteLine("Main thread Running");  
            Console.ReadKey();   
        }  
        static void myFun()  
        {  
            Console.WriteLine("Running other Thread");   
        }  
    }  
}  

运行应用程序后,您将获得以下两个线程的输出:
在这里插入图片描述
这里要注意的重点是,不能保证首先出现输出的是什么,换句话说,不能保证哪个线程先出现。因为线程由操作系统调度。所以哪个线程先出现可能每次都不同。

后台线程
只要至少有一个前台线程正在运行,应用程序的进程就会一直运行。如果Main()方法结束但是还有多个正在运行的前台线程,则应用程序的进程将保持活动状态,直到所有前台线程完成其工作,并且通过前台线程终止,所有后台线程也将立即终止。

使用Thread类创建线程时,可以通过设置属性IsBackground将其定义为前台线程或后台线程。Main()方法将线程“t”的此属性设置为false。设置新线程后,主线程只是向控制台写入结束消息。新线程写入开始和结束消息,并在它之间休眠2秒。

using System;  
using System.Threading;  
namespace threading  
{  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            Thread t = new Thread(myFun);  
            t.Name = "Thread1";  
            t.IsBackground = false;   
            t.Start();  
            Console.WriteLine("Main thread Running");  
            Console.ReadKey();   
        }  
        static void myFun()  
        {  
            Console.WriteLine("Thread {0} started", Thread.CurrentThread.Name);  
            Thread.Sleep(2000);   
            Console.WriteLine("Thread {0} completed", Thread.CurrentThread.Name);   
        }  
    }  
}  

编译运行此程序时,会看到写入控制台的完成消息,因为新线程是前台线程。这里的输出如下:
在这里插入图片描述
如果将IsBackground属性更改为true,则控制台中显示的结果将如下所示:在这里插入图片描述
并发问题

当启动访问相同数据的多个线程时,程序需要确保共享数据不受其他线程更改其值。

竞争条件

如果两个或多个线程访问同一对象并且未同步对共享状态的访问,则会发生竞争条件。为了说明竞争条件的问题,让我们构建一个控制台应用程序。此应用程序使用Test类通过暂停当前线程来打印10个数字。

using System;  
using System.Threading;  
namespace threading  
{  
    public class Test  
    {  
        public void Calculate()  
        {    
            for (int i = 0; i < 10; i++)  
            {  
                Thread.Sleep(new Random().Next(5));  
                Console.Write(" {0},", i);  
            }  
            Console.WriteLine();  
        }  
    }  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            Test t = new Test();  
            Thread[] tr = new Thread[5];  
            for (int i = 0; i < 5; i++)  
            {  
                tr[i] = new Thread(new ThreadStart(t.Calculate));  
                tr[i].Name = String.Format("Working Thread: {0}", i);  
            }  
            //Start each thread  
            foreach (Thread x in tr)  
            {  
                x.Start();  
            }  
            Console.ReadKey();  
        }  
    }  
}  

编译运行此程序后,此应用程序中的主线程首先生成五个辅助线程。并告诉每个工作线程在同一个Test类实例上调用Calculate()方法。因此,所有五个线程同时开始访问Calculate()方法,因为我们没有采取任何预防措施来锁定此对象的共享资源; 这将导致竞争条件,应用程序产生不可预测的输出,如下所示
.NET使用多线程编程_第6张图片
死锁

应用程序中锁太多可能会导致程序出现问题。在死锁中,至少有两个线程互相等待释放锁。由于两个线程相互等待,发生死锁情况下线程无休止地互相等待导致程序停止响应。

下面两个方法都通过锁定它们来改变对象obj1和obj2的状态。方法DeadLock1()首先锁定obj1,接着锁定obj2,类似地,方法DeadLock2()首先锁定obj2,然后锁定obj1。因此释放对obj1的锁定,接下来发生线程切换,第二个方法启动并获取obj2的锁定。第二个线程现在等待obj1的锁定。两个线程现在都在等待,不会互相释放。这是一种典型的死锁。

using System;  
using System.Threading;  
namespace threading  
{  
    class Program  
    {  
        static object obj1 = new object();  
        static object obj2 = new object();  
        public static void DeadLock1()  
        {  
            lock (obj1)  
            {  
                Console.WriteLine("Thread 1 got locked");  
                Thread.Sleep(500);  
                lock (obj2)  
                {  
                    Console.WriteLine("Thread 2 got locked");  
                }  
            }  
        }  
        public static void DeadLock2()  
        {  
            lock (obj2)  
            {  
                Console.WriteLine("Thread 2 got locked");  
                Thread.Sleep(500);  
                lock (obj1)  
                {  
                    Console.WriteLine("Thread 1 got locked");  
                }  
            }  
        }  
        static void Main(string[] args)  
        {  
            Thread t1 = new Thread(new ThreadStart(DeadLock1));  
            Thread t2 = new Thread(new ThreadStart(DeadLock2));  
            t1.Start();  
            t2.Start();  
            Console.ReadKey();  
        }  
    }  
}  

同步

同步可以避免多个线程可能出现的问题(例如Race条件和死锁)。通常建议不在线程之间共享数据来避免并发问题。当然,这并非是总是可能的。如果数据共享是不可避免的,那么必须使用同步,以便一次只有一个线程访问和更改共享状态。

下面章节讨论各种同步技术。

我们可以使用lock关键字同步对共享资源的访问。通过这样做,传入的线程不能中断当前线程,阻止它完成其工作。lock关键字需要对象引用。

通过采用之前的竞争条件问题,我们可以通过对关键语句实施锁定来完善此程序,使其从竞争条件中输出不确定的数据变成可靠的数据,如下所示

public class Test  
{  
    public object tLock = new object();  
    public void Calculate()  
    {  
        lock (tLock)  
        {  
            Console.Write(" {0} is Executing",Thread.CurrentThread.Name);  
            for (int i = 0; i < 10; i++)  
            {  
                Thread.Sleep(new Random().Next(5));  
                Console.Write(" {0},", i);  
            }  
            Console.WriteLine();  
        }  
    }  
}  

在编译该程序之后,这次它产生了如下所示的期望结果。在这里,每个线程都有足够的机会完成其任务。
.NET使用多线程编程_第7张图片
监视器

lock语句由编译器解析为使用Monitor类。Monitor类几乎类似于锁,但它的优点是比lock语句更好地控制。可以显式地指示锁的进入和退出,如下面的代码所示。

object tLock = new object();  
public void Calculate()  
{  
    Monitor.Enter(tLock);  
    try  
    {  
      for (int i = 0; i < 10; i++)  
      {  
        Thread.Sleep(new Random().Next(5));  
        Console.Write(" {0},", i);  
      }  
    }  
    catch{}  
    finally  
    {  
      Monitor.Exit(tLock);  
    }  
    Console.WriteLine();  
}  

实际上,如果观察使用lock语句的任何应用程序的IL代码,您将在其中找到Monitor类引用,如下所示:
.NET使用多线程编程_第8张图片
使用[Synchronization]属性

[Synchronization]属性是System.Runtime.Remoting.Context命名空间的成员。为了线程安全,这个类级属性有效地锁定了对象的所有实例。

using System.Threading;  
  
using System.Runtime.Remoting.Contexts;   
[Synchronization]  
public class Test:ContextBoundObject  
{  
    public void Calculate()  
    {  
        for (int i = 0; i < 10; i++)  
        {  
            Thread.Sleep(new Random().Next(5));  
            Console.Write(" {0},", i);  
        }  
        Console.WriteLine();  
    }  
}  

互斥锁

Mutex代表互斥锁,它提供跨多个线程的同步。互斥锁是从WaitHandle派生而来的,您可以执行WaitOne()来获取互斥锁,并成为互斥锁的所有者。通过调用ReleaseMutex()方法释放互斥体,如下所示:

using System;  
using System.Threading;  
namespace threading  
{  
    class Program  
    {  
        private static Mutex mutex = new Mutex();  
        static void Main(string[] args)  
        {  
            for (int i = 0; i < 4; i++)  
            {  
                Thread t = new Thread(new ThreadStart(MutexDemo));  
                t.Name = string.Format("Thread {0} :", i+1);  
                t.Start();  
            }  
            Console.ReadKey();  
        }  
        static void MutexDemo()  
        {  
            try  
            {  
                mutex.WaitOne();   // Wait until it is safe to enter.  
                Console.WriteLine("{0} has entered in the Domain", Thread.CurrentThread.Name);  
                Thread.Sleep(1000);    // Wait until it is safe to enter.  
                Console.WriteLine("{0} is leaving the Domain\r\n", Thread.CurrentThread.Name);  
            }  
            finally  
            {  
                mutex.ReleaseMutex();  
            }  
        }  
    }  
}  

编译运行此程序后,它会显示每个新线程何时首次进入其应用程序域。一旦完成任务,它就会被释放,第二个线程就会启动,依此类推。
.NET使用多线程编程_第9张图片
信号量

信号量与互斥锁非常相似,但信号量可以由多个线程同时使用,而互斥锁则不能。对于信号量,可以定义允许多少线程同时访问信号量屏蔽的资源。

在下面的示例中,创建了5个线程和2个信号量。在信号量类的构造函数中,您可以定义可以使用信号量获取的锁的数量。

using System;  
using System.Threading;  
namespace threading  
{  
    class Program  
    {  
        static Semaphore obj = new Semaphore(2, 4);  
        static void Main(string[] args)  
        {  
            for (int i = 1; i <= 5; i++)  
            {  
                new Thread(SempStart).Start(i);  
            }  
            Console.ReadKey();  
        }  
        static void SempStart(object id)  
        {  
            Console.WriteLine(id + "-->>Wants to Get Enter");  
            try  
            {  
                obj.WaitOne();  
                Console.WriteLine(" Success: " + id + " is in!");     
                Thread.Sleep(2000);  
                Console.WriteLine(id + "<<-- is Evacuating");  
            }  
            finally  
            {  
                obj.Release();  
            }  
        }  
    }  
}  

在我们运行此应用程序时,会立即创建2个信号量,其他信号量等待,因为我们创建了5个线程。所以3个处于等待状态。任何一个线程释放一个信号量,其余的获得一个接一个地创建
.NET使用多线程编程_第10张图片

你可能感兴趣的:(.Net)