C# 线程相关知识总结

初识线程

线程是一个独立的运行单元,每个进程内部都有多个线程,每个线程都可以各自同时执行指令。每个线程都有自己独立的栈,但是与进程内的其他线程共享内存。但是对于.NET的客户端程序(Console,WPF,WinForms)是由CLR创建的单线程(主线程,且只创建一个线程)来启动。在该线程上可以创建其他线程。

图:

线程工作方式

多线程由内部线程调度程序管理,线程调度器通常是CLR委派给操作系统的函数。线程调度程序确保所有活动线程都被分配到合适的执行时间,线程在等待或阻止时 (例如,在一个独占锁或用户输入) 不会消耗 CPU 时间。
在单处理器计算机上,线程调度程序是执行时间切片 — 迅速切换每个活动线程。在 Windows 中, 一个时间片是通常数十毫秒为单位的区域 — — 相比来说 线程间相互切换比CPU更消耗资源。在多处理器计算机上,多线程用一种混合的时间切片和真正的并发性来实现,不同的线程会在不同的cpu运行代码。

创建线程

如:

using System;
using System.Threading;

class ThreadTest
{
 static void Main()
 {
  Thread t = new Thread (Write2);     // 创建线程t
  t.Start();                // 执行 Write2()

  // 同时执行主线程上的该方法
  for (int i = 0; i < 1000; i++) Console.Write ("1");
 }

 static void Write2()
 {
  for (int i = 0; i < 1000; i++) Console.Write ("2");
 }
}

输出

111122221122221212122221212......

在主线程上创建了一个新的线程,该新线程执行WrWrite2方法,在调用t.Start()时,主线程并行,输出“1”。

图:

线程Start()之后,线程的IsAlive属性就为true,直到该线程结束(当线程传入的方法结束时,该线程就结束)。

CLR使每个线程都有自己独立的内存栈,所以每个线程的本地变量都相互独立。

如:

static void Main()
{
 new Thread (Go).Start();   // 创建一个新线程,并调用Go方法
 Go();             // 在主线程上调用Go方法
}

static void Go()
{
 // 声明一个本地局部变量 cycles
 for (int cycles = 0; cycles < 5; cycles++) Console.Write ('N');
}

输出

NNNNNNNNNN (共输出10个N)

在新线程和主线程上调用Go方法时分别创建了变量cycles,这时cycles在不同的线程栈上,所以相互独立不受影响。

图:

如果不同线程指向同一个实例的引用,那么不同的线程共享该实例。

如:

class ThreadTest
{
 //全局变量
 int i;

 static void Main()
 {
  ThreadTest tt = new ThreadTest();  // 创建一个ThreadTest类的实例
  new Thread (tt.Go).Start();
  tt.Go();
 }

 // Go方法属于ThreadTest的实例
 void Go()
 {
   if (i==1) { ++i; Console.WriteLine (i); }
 }
}

输出

2

新线程和主线程上调用了同一个实例的Go方法,所以变量i共享。

静态变量也可以被多线程共享

class ThreadTest
{
 static int i;  // 静态变量可以被线程共享

 static void Main()
 {
  new Thread (Go).Start();
  Go();
 }

 static void Go()
 {
  if (i==1) { ++i; Console.WriteLine (i); }
 }
}

输出

2

如果将Go方法的代码位置互换

 static void Go()
 {
  if (i==1) { Console.WriteLine (i);++i;}
 }

输出

1 1(有时输出一个,有时输出两个)

如果新线程在Write之后,done=true之前,主线程也执行到了write那么就会有两个done。

不同线程在读写共享字段时会出现不可控的输出,这就是多线程的线程安全问题。

解决方法: 使用排它锁来解决这个问题--lock

class ThreadSafe
{
 static bool done;
 static readonly object locker = new object();

 static void Main()
 {
  new Thread (Go).Start();
  Go();
 }

 static void Go()
 {
  //使用lock,确保一次只有一个线程执行该代码
  lock (locker)
  {
   if (!done) { Console.WriteLine ("Done"); done = true; }
  }
 }
}

当多个线程都在争取这个排它锁时,一个线程获取该锁,其他线程会处于blocked状态(该状态时不消耗cpu),等待另一个线程释放锁时,捕获该锁。这就保证了一次
只有一个线程执行该代码。

Join和Sleep

Join可以实现暂停另一个线程,直到调用Join方法的线程结束。

static void Main()
{
 Thread t = new Thread (Go);
 t.Start();
 t.Join();
 Console.WriteLine ("Thread t has ended!");
}

static void Go()
{
 for (int i = 0; i < 1000; i++) Console.Write ("y");
}

输出

yyyyyy..... Thread t has ended!

线程t调用Join方法,阻塞主线程,直到t线程执行结束,再执行主线程。

Sleep:暂停该线程一段时间

Thread.Sleep (TimeSpan.FromHours (1)); // 暂停一个小时
Thread.Sleep (500);           // 暂停500毫秒

Join是暂停别的线程,Sleep是暂停自己线程。

上面的例子是使用Thread类的构造函数,给构造函数传入一个ThreadStart委托。来实现的。

public delegate void ThreadStart();

然后调用Start方法,来执行该线程。委托执行完该线程也结束。

如:

class ThreadTest
{
 static void Main()
 {
  Thread t = new Thread (new ThreadStart (Go));

  t.Start();  // 执行Go方法
  Go();    // 同时在主线程上执行Go方法
 }

 static void Go()
 {
  Console.WriteLine ("hello!");
 }
}

多数情况下,可以不用new ThreadStart委托。直接在构造函数里传入void类型的方法。

Thread t = new Thread (Go); 

使用lambda表达式

static void Main()
{
 Thread t = new Thread ( () => Console.WriteLine ("Hello!") );
 t.Start();
}

Foreground线程和Background线程

默认情况下创建的线程都是Foreground,只要有一个Foregournd线程在执行,应用程序就不会关闭。
Background线程则不是。一旦Foreground线程执行完,应用程序结束,background就会强制结束。
可以用IsBackground来查看该线程是什么类型的线程。

线程异常捕获

public static void Main()
{
 try
 {
  new Thread (Go).Start();
 }
 catch (Exception ex)
 {
  // 不能捕获异常
  Console.WriteLine ("Exception!");
 }
}

static void Go() { throw null; }  //抛出 Null异常

此时并不能在Main方法里捕获线程Go方法的异常,如果是Thread自身的异常可以捕获。

正确捕获方式:

public static void Main()
{
  new Thread (Go).Start();
}

static void Go()
{
 try
 {
  // ...
  throw null;  // 这个异常会被下面捕获
  // ...
 }
 catch (Exception ex)
 {
   // ...
 }
}

线程池

当创建一个线程时,就会消耗几百毫秒cpu,创建一些新的私有局部变量栈。每个线程还消耗(默认)约1 MB的内存。线程池通过共享和回收线程,允许在不影响性能的情况下启用多线程。
每个.NET程序都有一个线程池,线程池维护着一定数量的工作线程,这些线程等待着执行分配下来的任务。

线程池线程注意点:

1 线程池的线程不能设置名字(导致线程调试困难)。

2 线程池的线程都是background线程

3 阻塞一个线程池的线程,会导致延迟。

4 可以随意设置线程池的优先级,在回到线程池时改线程就会被重置。
通过Thread.CurrentThread.IsThreadPoolThread.可以查看该线程是否是线程池的线程。

使用线程池创建线程的方法:

  • Task
  • ThreadPool.QueueUserWorkItem
  • Asynchronous delegates
  • BackgroundWorker

TPL

Framework4.0下可以使用Task来创建线程池线程。调用Task.Factory.StartNew(),传递一个委托

  • Task.Factory.StartNew
  • static void Main()
    {
     Task.Factory.StartNew (Go);
    }
    
    static void Go()
    {
     Console.WriteLine ("Hello from the thread pool!");
    }

Task.Factory.StartNew 返回一个Task对象。可以调用该Task对象的Wait来等待该线程结束,调用Wait时会阻塞调用者的线程。

  • Task构造函数   给Task构造函数传递Action委托,或对应的方法,调用start方法,启动任务
  • static void Main()
    {
     Task t=new Task(Go);
     t.Start();
    }
    
    static void Go()
    {
     Console.WriteLine ("Hello from the thread pool!");
    }

  • Task.Run    直接调用Task.Run传入方法,执行。
  • static void Main()
    {
     Task.Run(() => Go());
    }
    
    static void Go()
    {
     Console.WriteLine ("Hello from the thread pool!");
    }

QueueUserWorkItem

QueueUserWorkItem没有返回值。使用 QueueUserWorkItem,只需传递相应委托的方法就行。

static void Main()
{
 //Go方法的参数data此时为空
 ThreadPool.QueueUserWorkItem (Go);
 //Go方法的参数data此时为123
 ThreadPool.QueueUserWorkItem (Go, 123);
 Console.ReadLine();
}

static void Go (object data)
{
 Console.WriteLine ("Hello from the thread pool! " + data);
}

委托异步

委托异步可以返回任意类型个数的值。
使用委托异步的方式:

  1. 声明一个和方法匹配的委托
  2. 调用该委托的BeginInvoke方法,获取返回类型为IAsyncResult的值
  3. 调用EndInvoke方法传递IAsyncResulte类型的值获取最终结果

如:

static void Main()
{
 Func<string, int> method = Work;
 IAsyncResult cookie = method.BeginInvoke ("test", null, null);
 //
 // ... 此时可以同步处理其他事情
 //
 int result = method.EndInvoke (cookie);
 Console.WriteLine ("String length is: " + result);
}

static int Work (string s) { return s.Length; }

使用回调函数来简化委托的异步调用,回调函数参数为IAsyncResult类型

static void Main()
{
 Func<string, int> method = Work;
 method.BeginInvoke ("test", Done, method);
 // ...
 //并行其他事情
}

static int Work (string s) { return s.Length; }

static void Done (IAsyncResult cookie)
{
 var target = (Func<string, int>) cookie.AsyncState;
 int result = target.EndInvoke (cookie);
 Console.WriteLine ("String length is: " + result);
}

使用匿名方法

Func<string, int> f = s => { return s.Length; };
 f.BeginInvoke("hello", arg =>
 {
   var target = (Func<string, int>)arg.AsyncState;
   int result = target.EndInvoke(arg);
   Console.WriteLine("String length is: " + result);
 }, f);

线程传参和线程返回值

Thread

Thread构造函数传递方法有两种方式:

public delegate void ThreadStart();
public delegate void ParameterizedThreadStart (object obj);

所以Thread可以传递零个或一个参数,但是没有返回值。

  • 使用lambda表达式直接传入参数。
  • static void Main()
    {
     Thread t = new Thread ( () => Print ("Hello from t!") );
     t.Start();
    }
    
    static void Print (string message)
    {
     Console.WriteLine (message);
    }

  • 调用Start方法时传入参数
  • static void Main()
    {
     Thread t = new Thread (Print);
     t.Start ("Hello from t!");
    }
    
    static void Print (object messageObj)
    {
     string message = (string) messageObj;
     Console.WriteLine (message);
    }

Lambda简洁高效,但是在捕获变量的时候要注意,捕获的变量是否共享。

如:

for (int i = 0; i < 10; i++)
 new Thread (() => Console.Write (i)).Start();

输出

0223447899

因为每次循环中的i都是同一个i,是共享变量,在输出的过程中,i的值会发生变化。

解决方法-局部域变量

for (int i = 0; i < 10; i++)
{
 int temp = i;
 new Thread (() => Console.Write (temp)).Start();
}

这时每个线程都指向新的域变量temp(此时每个线程都有属于自己的花括号的域变量)在该线程中temp不受其他线程影响。

委托

委托可以有任意个传入和输出参数。以Action,Func来举例。

  • Action 有零个或多个传入参数,但是没有返回值。
  • Func 有零个或多个传入参数,和一个返回值。
  •  Func<string, int> method = Work;
     IAsyncResult cookie = method.BeginInvoke("test", null, null);
     //
     // ... 此时可以同步处理其他事情
     //
     int result = method.EndInvoke(cookie);
     Console.WriteLine("String length is: " + result);    
    
     int Work(string s) { return s.Length; }

使用回调函数获取返回值

static void Main()
{
 Func<string, int> method = Work;
 method.BeginInvoke ("test", Done, null);
 // ...
 //并行其他事情
}

static int Work (string s) { return s.Length; }

static void Done (IAsyncResult cookie)
{
 var target = (Func<string, int>) cookie.AsyncState;
 int result = target.EndInvoke (cookie);
 Console.WriteLine ("String length is: " + result);
}

EndInvoke做了三件事情:

  1. 等待委托异步的结束。
  2. 获取返回值。
  3. 抛出未处理异常给调用线程。

Task

Task泛型允许有返回值。

如:

static void Main()
{
 // 创建Task并执行
 Task<string> task = Task.Factory.StartNew<string>
  ( () => DownloadString ("http://www.baidu.com") );
 // 同时执行其他方法
 Console.WriteLine("begin");
 //等待获取返回值,并且不会阻塞主线程
 Console.WriteLine(task.Result);
 Console.WriteLine("end");
}
static string DownloadString (string uri)
{
 using (var wc = new System.Net.WebClient())
  return wc.DownloadString (uri);
}

参考:

http://www.albahari.com/threading/

以上就是C# 线程相关知识总结的详细内容,更多关于C# 线程的资料请关注我们其它相关文章!

(0)

相关推荐

  • C#使用Interlocked实现线程同步

    通过System.Threading命名空间的Interlocked类控制计数器,从而实现进程 的同步.Iterlocked类的部分方法如下表: 示例,同时开启两个线程,一个写入数据,一个读出数据 代码如下:(但是运行结果却不是我们想象的那样) using System; using System.Threading; namespace 线程同步 { class Program { static void Main(string[] args) { //缓冲区,只能容纳一个字符 char bu

  • C#中的多线程小试牛刀

    前言 昨天在上班时浏览博问,发现了一个问题,虽然自己在 C# 多线程上没有怎么尝试过,看了几遍 CLR 中关于 线程的概念和讲解(后面三章).也想拿来实践实践.问题定义是这样的: 对于多线程不是很懂,面试的时候遇到一个多线程的题,不会做,分享出来,懂的大佬指点一下,谢谢 建一个winform窗体,在窗体中放上一个开始按钮,一个停止按钮,一个文本框,在窗体中声明一个List类型的属性,点击开始按钮后开启10个线程,所有线程同时不间断的给List集合中添加1-10000之间的随机数,要求添加List

  • C#开启线程的四种方式示例详解

    一.异步委托开启线程 public static void Main(string[] args){ Action<int,int> a=add; a.BeginInvoke(3,4,null,null);//前两个是add方法的参数,后两个可以为空 Console.WriteLine("main()"); Console.ReadKey(); } static void add(int a,int b){ Console.WriteLine(a+b); } 运行结果: 如

  • C#多线程中的异常处理操作示例

    本文实例讲述了C#多线程中的异常处理操作.分享给大家供大家参考,具体如下: 常规Thread中处理异常 使用Thread创建的子线程,需要在委托中捕捉,无法在上下文线程中捕捉 static void Main(string[] args) { ThreadStart threadStart = DoWork; Thread thread = new Thread(threadStart); thread.Start(); thread.Join(); } static void DoWork()

  • C#使用后台线程BackgroundWorker处理任务的总结

    在一些耗时的操作过程中,在长时间运行时可能会导致用户界面 (UI) 处于停止响应状态,用户在这操作期间无法进行其他的操作,为了不使UI层处于停止响应状态,我们倾向推荐用户使用BackgroundWorker来进行处理,这个后台的线程处理,可以很好的实现常规操作的同时,还可以及时通知UI,包括当前处理信息和进度等,这个BackgroundWorker的处理在百度里面也是有很多使用的介绍,本篇随笔主要是做一些自己的使用总结,希望也能给读者提供一个参考. 在使用BackgroundWorker的过程中

  • C# 多线程编程技术基础知识入门

    什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源.而一个进程又是由多个线程所组成的. 什么是线程? 线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针.程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数. 什么是多线程? 多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务. 多线程是指程序中包含多个执行流,即在一个程序中

  • 基于C#实现的轻量级多线程队列图文详解

    前言 工作中我们经常会遇到一些一些功能需要实现造作日志,数据修改日志,对于这种业务需求如果我们以同步的方式实现,难免会影响到系统的性能.如下我列出集中解决方案. 使用Thread异步处理. 使用线程池或Task异步处理. 以上两种方案确实能解决我们此场景的需求,但是同时也带来了问题. 第一种方式,使用thread的情况下我们无法控制创建的线程数量,要知道创建线程是一个很耗性能的操作. 第二种方式,使用线程池或者Task我们虽然可以通过设置线程池的最大线程数量来限制线程最大数,但是这个设置由于是全

  • C#线程同步的几种方法总结

    我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一大堆数据,但还要使用户界面处于可操作状态:或者你的程序需要访问一些外部资源如数据库或网络文件等.这些情况你都可以创建一个子线程去处理,然而,多线程不可避免地会带来一个问题,就是线程同步的问题.如果这个问题处理不好,我们就会得到一些非预期的结果. 在网上也看过一些关于线程同步的文章,其实线程同步有好几种方法,下面我就简单的做一下归纳. 一.volatile关键字 volatile是最简单的一种同步方法,当然简单是要付出代价的

  • C# 线程相关知识总结

    初识线程 线程是一个独立的运行单元,每个进程内部都有多个线程,每个线程都可以各自同时执行指令.每个线程都有自己独立的栈,但是与进程内的其他线程共享内存.但是对于.NET的客户端程序(Console,WPF,WinForms)是由CLR创建的单线程(主线程,且只创建一个线程)来启动.在该线程上可以创建其他线程. 图: 线程工作方式 多线程由内部线程调度程序管理,线程调度器通常是CLR委派给操作系统的函数.线程调度程序确保所有活动线程都被分配到合适的执行时间,线程在等待或阻止时 (例如,在一个独占锁

  • JAVA 线程通信相关知识汇总

    两个线程之间的通信 多线程环境下CPU会随机的在线程之间进行切换,如果想让两个线程有规律的去执行,那就需要两个线程之间进行通信,在Object类中的两个方法wait和notify可以实现通信. wait方法可以使当前线程进入到等待状态,在没有被唤醒的情况下,线程会一直保持等待状态. notify方法可以随机唤醒单个在等待状态下的线程. 来实现这样的一个功能: 让两个线程交替在控制台输出一行文字 定义一个Print类,有两个方法print1和print2,分别打印一行不同的内容 package c

  • java并发编程专题(一)----线程基础知识

    在任何的生产环境中我们都不可逃避并发这个问题,多线程作为并发问题的技术支持让我们不得不去了解.这一块知识就像一个大蛋糕一样等着我们去分享,抱着学习的心态,记录下自己对并发的认识. 1.线程的状态: 线程状态图: 1.新建状态(New):新创建了一个线程对象. 2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权. 3.运行状态(Running):就绪状态的线程获取了CPU,执行程序代码. 4

  • Java 自旋锁(spinlock)相关知识总结

    一.前言 谈到『自旋锁』,可能大家会说,这有啥好讲的,不就是等待资源的线程"原地打转"嘛.嗯,字面理解的意思很到位,但能深入具体点吗?自旋锁的设计真就这么简单? 本文或者说本系列的目的,都是让大家不要停留在表面,而是深入分析,做到: 灵活使用 掌握原理 优缺点 二.锁的优化:自旋锁 当多个线程想同时访问同一个资源时,就存在资源冲突,这时,大家最直接想到的就是加锁来互斥访问,加锁会有这么几个问题: 等待资源的线程进入睡眠,发生用户态向内核态的切换,有一定的性能开销: 占用资源的线程很快就

  • c++ 队列相关知识总结

    预备 队列,是一种先进先出(FIFO)的线性表,一般来说会使用链表或者数组来实现它. 队列被允许从后端(rear)插入(insert)新元素,称作入列(push,enqueue):而从前端(front)弹出(pop)元素,称作出列(pop,dequeue). 学术上说它和堆栈常常被同时提起,因为堆栈与队列几乎一摸一样,除了出栈时也在后端弹出元素,从而构成了后进先出(LIFO)的数据结构. 古典的单向链表/单向队列 单向链表表示的队列,出列时必须遍历整个链表,直至链尾的前一位,然后摘除链尾来实现出

  • Android Insets相关知识总结

    最近工作中总会涉及到Insets相关的一些内容,网上对于Insets的分析以及介绍还是较少的,这里对Insets涉及到一些概念和方法做一个总结. 什么是Insets? WindowInsets 源码解释为 window content的一系列插值集合,(个人理解为 一个Activity相对于手机屏幕需要空出的地方以腾纳给statusbar.Ime.Navigationbar等系统窗口,具体表现为该区域需要的上下左右的宽高,比如输入法窗口的区域就是一个Inset) WindowInsets包括三类

  • Java基础之引用相关知识总结

    一.引用的定义 在JDK 1.2以前,Java中的引用定义很传统:如果reference类型的数据存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用. 二.问题 当描述这样的一类对象:当内存空间还足够时,则能保留在内存之中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象.此时的引用就显得过于狭隘.因此在JDK 1.2之后,Java堆引用的概念进行了扩充. 三.引用的分类 回收时机 强引用 Strong Reference 类似Object obj = new Ob

  • Java集合的总体框架相关知识总结

    一.集合概述 数组其实就是一个集合.集合实际上就是一个容器.可以来容纳其它的数据. 二.集合在开发中的应用 集合是一个容器,是一个载体,可以一次容纳多个对象.在实际开发中,假设连接数据库,数据库当中有10条记录,那么假设把这10条记录查询出来,在Java程序中会将10条数据封装成10个Java对象,然后将10个Java对象放到某一个集合当中,将集合传到前端,然后遍历集合,将一个数据一个数据展现出来 三.集合存储的数据 Java集合中实际存放的只是对象的引用,每个集合元素都是一个引用变量,实际内容

  • Python多线程与多进程相关知识总结

    一.什么是进程 进程是执行中的程序,是资源分配的最小单位:操作系统以进程为单位分配存储空间,进程拥有独立地址空间.内存.数据栈等 操作系统管理所有进程的执行,分配资源 可以通过fork或 spawn的方式派生新进程,新进程也有自己独立的内存空间 进程间通信方式(IPC,Inter-Process Communication)共享信息,实现数据共享,包括管道.信号.套接字.共享内存区等. 二.什么是线程 线程是CPU调度的的最小单位 一个进程可以有多个线程 同进程下执行,并共享相同的上下文 线程间

  • Java并发编程之死锁相关知识整理

    一.什么是死锁 所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进 二.死锁产生的条件 以下将介绍死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁 互斥条件 进程要求对所分配的资源(如打印机〉进行排他性控制,即在一段时间内某资源仅为一个进程所占有.此时若有其他进程请求该资源,则请求进程只能等待 不可剥夺条件 进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能

随机推荐