.NET使用多线程编程

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

多线程简介

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

多线程概述

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

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

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

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

System.Threading.Thread类

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

成员类型描述
CurrentThreadStatic返回当前运行线程的引用。
SleepStatic暂停当前​​线程特定的持续时间。
GetDoaminStatic返回当前应用程序域的引用。
CurrentContextStatic返回当前正在运行的线程的当前上下文的引用。
PriorityInstance level获取或设置线程优先级。
IsAliveInstance level以True或False值的形式获取线程状态。
StartInstance level指示CLR启动线程。
SuspendInstance level挂起线程。
ResumeInstance level恢复以前挂起的线程。
AbortInstance level指示CLR终止线程。
NameInstance level允许建立线程名称。
IsBackgroundInstance 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();   
        }  
    }  
}  

编译完这个应用程序后,输出结果如下:
在这里插入图片描述
简单线程创建
以下示例说明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()方法,因为我们没有采取任何预防措施来锁定此对象的共享资源; 这将导致竞争条件,应用程序产生不可预测的输出,如下所示
在这里插入图片描述
死锁

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

下面两个方法都通过锁定它们来改变对象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();  
        }  
    }  
}  

在编译该程序之后,这次它产生了如下所示的期望结果。在这里,每个线程都有足够的机会完成其任务。
在这里插入图片描述
监视器

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类引用,如下所示:
在这里插入图片描述
使用[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();  
            }  
        }  
    }  
}  

编译运行此程序后,它会显示每个新线程何时首次进入其应用程序域。一旦完成任务,它就会被释放,第二个线程就会启动,依此类推。
在这里插入图片描述
信号量

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

在下面的示例中,创建了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个处于等待状态。任何一个线程释放一个信号量,其余的获得一个接一个地创建
在这里插入图片描述

  • 1
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值