大家应该掌握的多线程编程

毫无疑问,多线程在各种编程语言中都占有比较重要的一个席位。不管你是初学者,还是资深的老司机,多线程是在学习,面试和工作中都要经常被提及的一个话题,下面我们就来看一看具体的相关内容。

1、多线程编程必备知识

1.1 进程与线程的概念

当我们打开一个应用程序后,操作系统就会为该应用程序分配一个进程ID,例如打开QQ,你将在任务管理器的进程选项卡看到QQ.exe进程,如下图:

进程可以理解为一块包含了某些资源的内存区域,操作系统通过进程这一方式把它的工作划分为不同的单元。一个应用程序可以对应于多个进程。

线程是进程中的独立执行单元,对于操作系统而言,它通过调度线程来使应用程序工作,一个进程中至少包含一个线程,我们把该线程成为主线程。线程与进程之间的关系可以理解为:线程是进程的执行单元,操作系统通过调度线程来使应用程序工作;而进程则是线程的容器,它由操作系统创建,又在具体的执行过程中创建了线程。

1.2线程的调度

在操作系统的书中貌似有提过,“Windows是抢占式多线程操作系统”。之所以这么说它是抢占式的,是因为线程可以在任意时间里被抢占,来调度另一个线程。操作系统为每个线程分配了0-31中的某一级优先级,而且会把优先级高的线程优先分配给CPU执行。

Windows支持7个相对线程优先级:Idle、Lowest、BelowNormal、Normal、AboveNormal、Highest和Time-Critical。其中,Normal是默认的线程优先级。程序可以通过设置Thread的Priority属性来改变线程的优先级,该属性的类型为ThreadPriority枚举类型,其成员包括Lowest、BelowNormal、Normal、AboveNormal和Highest。CLR为自己保留了Idle和Time-Critical两个优先级。

1.3线程也分前后台

线程有前台线程和后台线程之分。在一个进程中,当所有前台线程停止运行后,CLR会强制结束所有仍在运行的后台线程,这些后台线程被直接终止,却不会抛出任何异常。主线程将一直是前台线程。我们可以使用Tread类来创建前台线程。

using System;
using System.Threading;

namespace 多线程1
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      var backThread = new Thread(Worker);
      backThread.IsBackground = true;
      backThread.Start();
      Console.WriteLine("从主线程退出");
      Console.ReadKey();
    }

    private static void Worker()
    {
      Thread.Sleep(1000);
      Console.WriteLine("从后台线程退出");
    }
  }
}

以上代码先通过Thread类创建了一个线程对象,然后通过设置IsBackground属性来指明该线程为后台线程。如果不设置这个属性,则默认为前台线程。接着调用了Start的方法,此时后台线程会执行Worker函数的代码。所以在这个程序中有两个线程,一个是运行Main函数的主线程,一个是运行Worker线程的后台线程。由于前台线程执行完毕后CLR会无条件地终止后台线程的运行,所以在前面的代码中,若启动了后台线程,则主线程将会继续运行。主线程执行完后,CLR发现主线程结束,会终止后台线程,然后使整个应用程序结束运行,所以Worker函数中的Console语句将不会执行。所以上面代码的结果是不会运行Worker函数中的Console语句的。

可以使用Join函数的方法,确保主线程会在后台线程执行结束后才开始运行。

using System;
using System.Threading;

namespace 多线程1
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      var backThread = new Thread(Worker);
      backThread.IsBackground = true;
      backThread.Start();
      backThread.Join();
      Console.WriteLine("从主线程退出");
      Console.ReadKey();
    }

    private static void Worker()
    {
      Thread.Sleep(1000);
      Console.WriteLine("从后台线程退出");
    }
  }
}

以上代码调用Join函数来确保主线程会在后台线程结束后再运行。

如果你线程执行的方法需要参数,则就需要使用new Thread的重载构造函数Thread(ParameterizedThreadStart).

using System;
using System.Threading;

namespace 多线程1
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      var backThread = new Thread(new ParameterizedThreadStart(Worker));
      backThread.IsBackground = true;
      backThread.Start("Helius");
      backThread.Join();
      Console.WriteLine("从主线程退出");
      Console.ReadKey();
    }

    private static void Worker(object data)
    {
      Thread.Sleep(1000);
      Console.WriteLine($"传入的参数为{data.ToString()}");
    }
  }
}

执行结果为:

2、线程的容器——线程池

前面我们都是通过Thead类来手动创建线程的,然而线程的创建和销毁会耗费大量时间,这样的手动操作将造成性能损失。因此,为了避免因通过Thread手动创建线程而造成的损失,.NET引入了线程池机制。

2.1 线程池

线程池是指用来存放应用程序中要使用的线程集合,可以将它理解为一个存放线程的地方,这种集中存放的方式有利于对线程进行管理。

CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列,当应用程序想要执行一个异步操作时,需要调用QueueUserWorkItem方法来将对应的任务添加到线程池的请求队列中。线程池实现的代码会从队列中提取,并将其委派给线程池中的线程去执行。如果线程池没有空闲的线程,则线程池也会创建一个新线程去执行提取的任务。而当线程池线程完成某个任务时,线程不会被销毁,而是返回到线程池中,等待响应另一个请求。由于线程不会被销毁,所以也就避免了性能损失。记住,线程池里的线程都是后台线程,默认级别是Normal。

2.2 通过线程池来实现多线程

要使用线程池的线程,需要调用静态方法ThreadPool.QueueUserWorkItem,以指定线程要调用的方法,该静态方法有两个重载版本:

public static bool QueueUserWorkItem(WaitCallback callBack);

public static bool QueueUserWorkItem(WaitCallback callback,Object state)

这两个方法用于向线程池队列添加一个工作先以及一个可选的状态数据。然后,这两个方法就会立即返回。下面通过实例来演示如何使用线程池来实现多线程编程。

using System;
using System.Threading;

namespace 多线程2
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine($"主线程ID={Thread.CurrentThread.ManagedThreadId}");
      ThreadPool.QueueUserWorkItem(CallBackWorkItem);
      ThreadPool.QueueUserWorkItem(CallBackWorkItem,"work");
      Thread.Sleep(3000);
      Console.WriteLine("主线程退出");
      Console.ReadKey();
    }

    private static void CallBackWorkItem(object state)
    {
      Console.WriteLine("线程池线程开始执行");
      if (state != null)
      {
        Console.WriteLine($"线程池线程ID={Thread.CurrentThread.ManagedThreadId},传入的参数为{state.ToString()}");
      }
      else
      {
        Console.WriteLine($"线程池线程ID={Thread.CurrentThread.ManagedThreadId}");
      }
    }
  }
}

结果为:

2.3 协作式取消线程池线程

.NET Framework提供了取消操作的模式,这个模式是协作式的。为了取消一个操作,必须创建一个System.Threading.CancellationTokenSource对象。下面还是使用代码来演示一下:

using System;
using System.Threading;

namespace 多线程3
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      Console.WriteLine("主线程运行");
      var cts = new CancellationTokenSource();
      ThreadPool.QueueUserWorkItem(Callback, cts.Token);
      Console.WriteLine("按下回车键来取消操作");
      Console.Read();
      cts.Cancel();
      Console.ReadKey();
    }

    private static void Callback(object state)
    {
      var token = (CancellationToken) state;
      Console.WriteLine("开始计数");
      Count(token, 1000);
    }

    private static void Count(CancellationToken token, int count)
    {
      for (var i = 0; i < count; i++)
      {
        if (token.IsCancellationRequested)
        {
          Console.WriteLine("计数取消");
          return;
        }
        Console.WriteLine($"计数为:{i}");
        Thread.Sleep(300);
      }
      Console.WriteLine("计数完成");
    }
  }
}

结果为:

3、线程同步

线程同步计数是指多线程程序中,为了保证后者线程,只有等待前者线程完成之后才能继续执行。这就好比生活中排队买票,在前面的人没买到票之前,后面的人必须等待。

3.1 多线程程序中存在的隐患

多线程可能同时去访问一个共享资源,这将损坏资源中所保存的数据。这种情况下,只能采用线程同步技术。

3.2 使用监视器对象实现线程同步

监视器对象(Monitor)能够确保线程拥有对共享资源的互斥访问权,C#通过lock关键字来提供简化的语法。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace 线程同步
{
  class Program
  {
    private static int tickets = 100;
    static object globalObj=new object();
    static void Main(string[] args)
    {
      Thread thread1=new Thread(SaleTicketThread1);
      Thread thread2=new Thread(SaleTicketThread2);
      thread1.Start();
      thread2.Start();
      Console.ReadKey();
    }

    private static void SaleTicketThread2()
    {
      while (true)
      {
        try
        {
          Monitor.Enter(globalObj);
          Thread.Sleep(1);
          if (tickets > 0)
          {
            Console.WriteLine($"线程2出票:{tickets--}");
          }
          else
          {
            break;
          }
        }
        catch (Exception)
        {
          throw;
        }
        finally
        {
          Monitor.Exit(globalObj);
        }
      }
    }

    private static void SaleTicketThread1()
    {
      while (true)
      {
        try
        {
          Monitor.Enter(globalObj);
          Thread.Sleep(1);
          if (tickets > 0)
          {
            Console.WriteLine($"线程1出票:{tickets--}");
          }
          else
          {
            break;
          }
        }
        catch (Exception)
        {
          throw;
        }
        finally
        {
          Monitor.Exit(globalObj);
        }
      }
    }
  }
}

在以上代码中,首先额外定义了一个静态全局变量globalObj,并将其作为参数传递给Enter方法。使用了Monitor锁定的对象需要为引用类型,而不能为值类型。因为在将值类型传递给Enter时,它将被先装箱为一个单独的毒香,之后再传递给Enter方法;而在将变量传递给Exit方法时,也会创建一个单独的引用对象。此时,传递给Enter方法的对象和传递给Exit方法的对象不同,Monitor将会引发SynchronizationLockException异常。

3.3线程同步技术存在的问题

(1)使用比较繁琐。要用额外的代码把多个线程同时访问的数据包围起来,还并不能遗漏。

(2)使用线程同步会影响程序性能。因为获取和释放同步锁是需要时间的;并且决定那个线程先获得锁的时候,CPU也要进行协调。这些额外的工作都会对性能造成影响。

(3)线程同步每次只允许一个线程访问资源,这会导致线程堵塞。继而系统会创建更多的线程,CPU也就要负担更繁重的调度工作。这个过程会对性能造成影响。

下面就由代码来解释一下性能的差距:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace 线程同步2
{
  class Program
  {
    static void Main(string[] args)
    {
      int x = 0;
      const int iterationNumber = 5000000;
      Stopwatch stopwatch=Stopwatch.StartNew();
      for (int i = 0; i < iterationNumber; i++)
      {
        x++;
      }
      Console.WriteLine($"不使用锁的情况下花费的时间:{stopwatch.ElapsedMilliseconds}ms");
      stopwatch.Restart();
      for (int i = 0; i < iterationNumber; i++)
      {
        Interlocked.Increment(ref x);
      }
      Console.WriteLine($"使用锁的情况下花费的时间:{stopwatch.ElapsedMilliseconds}ms");
      Console.ReadKey();
    }
  }
}

执行结果:

总结

以上就是本文关于大家应该掌握的多线程编程的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

(0)

相关推荐

  • C#多线程数组模拟socket

    本文实例为大家分享了C#多线程数组模拟socket的具体代码,供大家参考,具体内容如下 代码如下 //实例化线程组 Thread[] clientThreads = new Thread[numThread]; for (int i = 0; i < numThread; i++) { clientThreads[i] = new Thread(new ParameterizedThreadStart(SocketClient)); clientThreads[i].Start(i); } 多线

  • C#多线程之线程控制详解

    本文为大家分享了C#多线程之线程控制,供大家参考,具体内容如下 方案一: 调用线程控制方法.启动:Thread.Start();停止:Thread.Abort();暂停:Thread.Suspend();继续:Thread.Resume(); private void btn_Start_Click(object sender, EventArgs e) { mThread.Start(); // 开始 } private void btn_Stop_Click(object sender, E

  • C#多线程及同步示例简析

    60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建.撤消与切换存在较大的时空开销,因此需要引入轻型进程:二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大. 因此在80年代,出现了能独立运行的基本单位--线程(Threads).        线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元.一个标准的线程由线程ID,当前指令指针(P

  • C#多线程ThreadPool线程池详解

    简单说明一下: 线程池可以看做容纳线程的容器:一个应用程序最多只能有一个线程池:ThreadPool静态类通过QueueUserWorkItem()方法将工作函数排入线程池: 每排入一个工作函数,就相当于请求创建一个线程: 线程池的作用: 1.线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率. 2.如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜.),况且我们还不能控制线程池中线程的开始.挂起.和

  • C# 文件上传下载(Excel导入,多线程下载)功能的实现代码

    废话不多说了,直接给大家贴代码,具体代码如下所示: //打开Excel文件,转换为DataTable DataTable dtExcel; private void OpenFile() { OpenFileDialog dialog = new OpenFileDialog(); dialog.Filter = "Microsoft Excel files(*.xls)|*.xls;*.xlsx"; //筛选打开文件类型 :图片 *.jpg|*.jpg|*.bmp|*.bmp ;&q

  • C#多线程爬虫抓取免费代理IP的示例代码

    这里用到一个HTML解析辅助类:HtmlAgilityPack,如果没有网上找一个增加到库里,这个插件有很多版本,如果你开发环境是使用VS2005就2.0的类库,VS2010就使用4.0,以此类推..........然后直接创建一个控制台应用,将我下面的代码COPY替换就可以运行,下面就来讲讲我两年前做爬虫经历,当时是给一家公司做,也是用的C#,不过当时遇到一个头痛的问题就是抓的图片有病毒,然后系统挂了几次.所以抓网站图片要注意安全,虽然我这里没涉及到图片,但是还是提醒下看文章的朋友. clas

  • C#多线程之Thread类详解

    使用System.Threading.Thread类可以创建和控制线程. 常用的构造函数有: // 摘要: // 初始化 System.Threading.Thread 类的新实例,指定允许对象在线程启动时传递给线程的委托. // // 参数: // start: // System.Threading.ParameterizedThreadStart 委托,它表示此线程开始执行时要调用的方法. // // 异常: // System.ArgumentNullException: // star

  • C#多线程之Semaphore用法详解

    Semaphore:可理解为允许线程执行信号的池子,池子中放入多少个信号就允许多少线程同时执行. private static void MultiThreadSynergicWithSemaphore() { //0表示创建Semaphore时,拥有可用信号量数值 //1表示Semaphore中,最多容纳信号量数值 Semaphore semaphore = new Semaphore(0, 1); Thread thread1 = new Thread(() => { //线程首先WaitO

  • linux下c语言的多线程编程

    我们在写linux的服务的时候,经常会用到linux的多线程技术以提高程序性能 多线程的一些小知识: 一个应用程序可以启动若干个线程. 线程(Lightweight Process,LWP),是程序执行的最小单元. 一般一个最简单的程序最少会有一个线程,就是程序本身,也就是主函数(单线程的进程可以简单的认为只有一个线程的进程) 一个线程阻塞并不会影响到另外一个线程. 多线程的进程可以尽可能的利用系统CPU资源. 1创建线程 先上一段在一个进程中创建一个线程的简单的代码,然后慢慢深入. #incl

  • linux下的C\C++多进程多线程编程实例详解

    linux下的C\C++多进程多线程编程实例详解 1.多进程编程 #include <stdlib.h> #include <sys/types.h> #include <unistd.h> int main() { pid_t child_pid; /* 创建一个子进程 */ child_pid = fork(); if(child_pid == 0) { printf("child pid\n"); exit(0); } else { print

  • Java多线程编程小实例模拟停车场系统

    下面分享的是一个Java多线程模拟停车场系统的小实例(Java的应用还是很广泛的,哈哈),具体代码如下: Park类 public class Park { boolean []park=new boolean[3]; public boolean equals() { return true; } } Car: public class Car { private String number; private int position=0; public Car(String number)

  • Java多线程编程安全退出线程方法介绍

    线程停止 Thread提供了一个stop()方法,但是stop()方法是一个被废弃的方法.为什么stop()方法被废弃而不被使用呢?原因是stop()方法太过于暴力,会强行把执行一半的线程终止.这样会就不会保证线程的资源正确释放,通常是没有给与线程完成资源释放工作的机会,因此会导致程序工作在不确定的状态下 那我们该使用什么来停止线程呢 Thread.interrupt(),我们可以用他来停止线程,他是安全的,可是使用他的时候并不会真的停止了线程,只是会给线程打上了一个记号,至于这个记号有什么用呢

  • Java多线程编程实现socket通信示例代码

    流传于网络上有关Java多线程通信的编程实例有很多,这一篇还算比较不错,代码可用.下面看看具体内容. TCP是Tranfer Control Protocol的 简称,是一种面向连接的保证可靠传输的协议.通过TCP协议传输,得到的是一个顺序的无差错的数据流.发送方和接收方的成对的两个socket之间必须建 立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以

  • java多线程编程学习(线程间通信)

    一.概要 线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程间的通信就是成为整体的必用方案之一.可以说,使线程进行通信后,系统之间的交互性会更强大,在大大提高cpu利用率的同时还会使程序员对各线程任务在处理过程中进行有效的把控和监督. 二.等待/通知机制 1."wait/notify"机制:等待/通知机制,wait使线程暂停运行,而notify 使暂停的线程继续运行.用一个厨师和服务员的交互来说明: (1) 服务员取到菜的时间取决于厨师,所以服务员就有&

  • IOS多线程编程NSThread的使用方法

    IOS多线程编程NSThread的使用方法 NSThread是多线程的一种,有两种方法创建子线程 (1)优点:NSThread 比GCD.NSOperation都轻量级 (2)缺点:需要自己管理线程的生命周期,线程同步.线程同步对数据的加锁会有一定的系统开销 第一种是隐藏创建,有以下几种方式: (1)多用于串行:- (id)performSelector:(SEL)aSelector withObject:(id)object; (2)后台执行,多用于并行:- (void)performSele

  • IOS多线程编程的3种实现方法

    前言 在多线程简介中,我已经说明过了,为了提高界面的流畅度以及用户体验.我们务必要把耗时的操作放到别的线程中去执行,千万不要阻塞主线程. iOS中有以下3种多线程编程方法: NSThread Grand Centeral Dispatch(GCD) NSOperation和NSOperationQueue 1.NSThread 这是最轻量级的多线程的方法,使用起来最直观的多线程编程方法.但是因为需要自己管理线程的生命周期,线程同步.经常使用NSThread进行调试,在实际项目中不推荐使用. //

  • Java多线程编程中synchronized线程同步的教程

    0.关于线程同步 (1)为什么需要同步多线程? 线程的同步是指让多个运行的线程在一起良好地协作,达到让多线程按要求合理地占用释放资源.我们采用Java中的同步代码块和同步方法达到这样的目的.比如这样的解决多线程无固定序执行的问题: public class TwoThreadTest { public static void main(String[] args) { Thread th1= new MyThread1(); Thread th2= new MyThread2(); th1.st

  • Java多线程编程中synchronized关键字的基础用法讲解

    多线程编程中,最关键.最关心的问题应该就是同步问题,这是一个难点,也是核心. 从jdk最早的版本的synchronized.volatile,到jdk 1.5中提供的java.util.concurrent.locks包中的Lock接口(实现有ReadLock,WriteLock,ReentrantLock),多线程的实现也是一步步走向成熟化.   同步,它是通过什么机制来控制的呢?第一反应就是锁,这个在学习操作系统与数据库的时候,应该都已经接触到了.在Java的多线程程序中,当多个程序竞争同一

随机推荐