C# 线程入门 00

内容预告:

  • 线程入门(线程概念,创建线程)
  • 同步基础(同步本质,线程安全,线程中断,线程状态,同步上下文)
  • 使用线程(后台任务,线程池,读写锁,异步代理,定时器,本地存储)
  • 高级话题(非阻塞线程,扶起和恢复)


概览:

C#支持通过多线程并行地执行代码,一个线程是独立的执行个体,可以和其他线程同时运行。

CLR和操作系统会给C#程序开启一个线程(主线程),可以被用来作为创建多线程的起点,例子:

class ThreadTest {
static void Main() {
Thread t = new Thread (WriteY);
t.Start(); // Run WriteY on the new thread
while (true) Console.Write ("x"); // Write ‘x‘ forever
}
static void WriteY() {
while (true) Console.Write ("y"); // Write ‘y‘ forever
}
}

运行结果将是:

xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

主线程创建了一个线程 t ,执行了重复输出y的操作,主要线程执行了重复输出x的操作。
CLR给每个线程分配了单独的线程栈,所以本地变量是每个线程单独保存的,下面的例子,我们用一个本地变量定义一个函数,然后在main函数和新的线程里同时执行这个函数

static void Main() {
new Thread (Go).Start(); // Call Go() on a new thread
Go(); // Call Go() on the main thread
}
static void Go() {
// Declare and use a local variable - ‘cycles‘
for (int cycles = 0; cycles < 5; cycles++) Console.Write (‘?‘);
}

执行结果:

??????????

每个线程的内存栈里都创建了一个单独的变量cycle,所以输出是10个?
如果是引用同一个对象的话,线程则共享这个数据:

按 Ctrl+C 复制代码

按 Ctrl+C 复制代码

因为两个线程都调用Go(),它们共享done这个变量,所以done只输出一次:

Done

static变量提供一种不同的方式在线程中共享变量,这里是一个例子:

class ThreadTest {
static bool done; // Static fields are shared between all threads
static void Main() {
new Thread (Go).Start();
Go();
}
static void Go() {
if (!done) { done = true; Console.WriteLine ("Done"); }
}
}

这里输出就不太确定了,看起来要输出两次done,其实不可能。我们交换一下Go的次序,

class ThreadTest {
static bool done; // Static fields are shared between all threads
static void Main() {
new Thread (Go).Start();
Go();
}
static void Go() {
if (!done) { Console.WriteLine ("Done"); done = true; }
}
}

done就有可能输出两次。因为在一个线程计算if表达式然后执行Console.WriteLine ("Done");的时候,另一个线程可能有机会在done的值改变之前先输出done。
其实在C#中可以用lock来达到这个目的:

class ThreadSafe {
static bool done;
static object locker = new object();
static void Main() {
new Thread (Go).Start();
Go();
}
static void Go() {
lock (locker) {
if (!done) { Console.WriteLine ("Done"); done = true; }
}
}
}

当两个线程同时竞争一个锁时,一个线程等待,或者说阻塞,直到锁空出来。这主要是保证同时只能有一个线程可以进入临界代码区域,"Done"只会被输出一次。
代码是以这样的方式被保护的,来自于多线程上下文的不确定性,叫做线程安全。
临时地暂停,或阻塞,是线程同步的基本功能。
如果一个线程想要暂停,或者休眠一段时间,可以用:

Thread.Sleep (TimeSpan.FromSeconds (30)); // 阻塞30秒

一个线程可以通过调用Join等待另一个线程结束:

Thread t = new Thread (Go); // 假设Go是静态函数。
t.Start();
Thread.Join (t); // 阻塞,只到线程t结束。


线程如何工作:
在内部,多线程是被线程调度器管理的,是CLR代替操作系统干的活。线程调度器要保证所有活跃线程合理分配执行时间,以及在等待中的线程(这些线程是不消耗CPU时间的)。
在单核机器上,线程调度是以在活跃线程间快速切换时间片的方式工作的。就像就第一个例子,重复输出x或y的线程轮换得到时间片。在Windows XP下,一个时间片就是几十毫秒,这还是要比CPU在线程间切换能干更多事,一次线程切换也就几毫秒的事。
在多核机器上,多线程的实现是结合了时间片轮换和并发,并发是不同的线程同时运行在不同的CPU上,因为机器要运行的线程数远远大于CPU的数量,所以还需要时间片切换。
线程不能控制自己什么时候执行,完全由操作系统的时间片切换机制来控制。

线程和进程:

总是有面试官喜欢把线程和进程做比较,其实两者根本不是一个级别的东西。一个单独的应用程序内所有的线程都在逻辑或属于一个进程的。进程:一个运行应用程序的操作系统单元。线程与进程有些相似之处,比如:对于实例,进程和线程都是典型的时间片轮换的执行机制。关键的不同点在于进程间是相互独立的,而同一个应用程序里的线程间是共享堆内存的,这也是性能的用武之地:一个线程可以在后台运行,另一个线程可以显示得到的数据。

什么时候应该用多线程:

  • 一个普通的多线程程序在后台运行耗时的任务时。主线程保持运行状态,工作线程干后台的活。在Windows Form程序里,如果主线程被长时间占用,键盘和鼠标的操作就不能处理了,然后程序就变成“无响应”了。所以,需要把耗时的任务放在后台运行,让主线程保证响应用户输入。
  • 在非UI程序中,比如Windows服务,多线程就特别有用了,当等待另一台机器(例如一个应用服务器,数据库服务器,客户端)的响应时,用一个工作线程来等待,让主线程保持畅通。
  • 多线程的另一个用处是在函数中有大量计算时,函数划成多个线程可以在多核的机器上执行更快(可以用Environment.ProcessorCount得到CPU核心数量)。
  • 一个C#程序可以通过两种方式成为多线程:显示地创建线程,或者使用.NET显示创建线程的功能(比如BackgroundWorker,线程池,定时器,远程服务器,WebSerivce或ASP.NET程序),在后面这些情况下,只能是多线程。单线程的web服务器肯定不行。在无状态的web服务器里,多线程是相当简单的。主要的问题是如果处理缓存数据的锁机制。

什么时候不应该用多线程:

多线程也有缺点,最大的问题是会让程序变得复杂,多线程本身并不复杂,复杂在于线程间的交互。能让开发周期变长,以及Bug变多。所以需要把多个线程间的交互设计的尽量简单,或者就别用多线程,除非你可以保证的很好。

过多地在线程间切换和分配内存栈,也会带来CPU资源的消耗,通常,当硬盘IO很多时,只有一两个线程依次执行任务的程序性能要更好,而多个性能同时执行一个任务的性能不怎么样。后面会讨论生产者/消费者模型。



创建和启动线程:

可以用Thread类的构造函数创建线程,传递一个ThreadStart的代理作为参数,这个代理指向将要执行的函数,以下是这个代理的定义:

public delegate void ThreadStart();

执行Start()函数,线程即开始运行,在函数结束后线程会返回,下面是创建ThreadStart的C#语法:

按 Ctrl+C 复制代码

按 Ctrl+C 复制代码

线程t执行Go函数,同时主线程也调用Go,执行结果是:

hello!
hello!

也可以用C#的语法糖:编译器会自动创建一个ThreadStart的代理。

static void Main() {
Thread t = new Thread (Go); // No need to explicitly use ThreadStart
t.Start();
...
}
static void Go() { ... }

还有更简单的匿名函数语法:

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

给ThreadStart传递参数:这种形式只能传递一个参数

public delegate void ParameterizedThreadStart (object obj);

class ThreadTest {
static void Main() {
Thread t = new Thread (Go);
t.Start (true); // == Go (true)
Go (false);
}
static void Go (object upperCase) {
bool upper = (bool) upperCase;
Console.WriteLine (upper ? "HELLO!" : "hello!");
}

结果:

hello!
HELLO!

如果用匿名函数方式:可以传递多个参数,且也不需要类型转换,

static void Main() {
Thread t = new Thread (delegate() { WriteText ("Hello"); });
t.Start();
}
static void WriteText (string text) { Console.WriteLine (text); }

还有一种传参的方式是传一个实例过去,而不是传一个静态函数:

class ThreadTest {
bool upper;
static void Main() {
ThreadTest instance1 = new ThreadTest();
instance1.upper = true;
Thread t = new Thread (instance1.Go);
t.Start();
ThreadTest instance2 = new ThreadTest();
instance2.Go(); // Main thread – runs with upper=false
}
void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }



线程命名:线程有一个Name属性,在调试时很有用。

class ThreadNaming {
static void Main() {
Thread.CurrentThread.Name = "main";
Thread worker = new Thread (Go);
worker.Name = "worker";
worker.Start();
Go();
}
static void Go() {
Console.WriteLine ("Hello from " + Thread.CurrentThread.Name);
}
}

输出:

Hello from main
Hello from worker


前台线程和后台线程:

默认情况下,线程都是前台线程,意味着任何一个前台线程正在运行,程序就是运行的。而后台线程在所有前台线程终止时也会立即终止。

把线程从前台改为后台,线程在CPU调度器的优先级和状态是不会改变的。

class PriorityTest {
static void Main (string[] args) {
Thread worker = new Thread (delegate() { Console.ReadLine(); });
if (args.Length > 0) worker.IsBackground = true;
worker.Start();
}
}

如果这个程序执行时不带参数,worker线程默认是前台线程,并且会在ReadLine这一行等着用户输入。同时,主线程退出,但是程序会继续运行,因为ReadLine也是前台线程。如果传了一个参数给Main函数,worker线程的状态则被设置成后台状态,程序几乎会在主线程结束时立即退出--终于ReadLine。当后台线程以这种方式终止时,任何代码都不再执行了,这种代码是不推荐的,所以最好在程序退出前等待所有后台线程,可以用超时时间(Thread.Join)来做。如果因为某些原因worker线程一直不结束,也能终止这个线程,这种情况下最好记录一下日志来分析什么情况导致的。

在Windows Form中被抛弃的前台线程是个潜在的危险,因为程序在主线程结束将要退出时,它还在继续运行。在Windows的任务管理器里,它在应用程序Tab里会消失,但在进程Tab里还在。除非用户显式地结束它。

常见的程序退出失败的可能性就是忘记了前台线程。



线程的优先级:线程的优先级决定了线程的执行时间。

enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

线程的优先级为Highest时,并不意味着线程会实时运行,要想实时运行,进程的优先级也得是High。当你的进程的优先级是High时,如果程序进入了死循环,系统会死锁。这个时候就只有按电源键了。所以,慎用。

最好将实时线程和UI分开在两个线程,并设置成不同的优先级,通过远程或共享内存通信,共享内存需要P/Invoking Win32 API(CreateFileMapping和MapViewOfFile)。



线程的异常处理:线程一旦启动,任何在try/catch/finally范围内创建线程的代码块与try/catch/finally就没有什么关系了。

public static void Main() {
try {
new Thread (Go).Start();
}
catch (Exception ex) {
// We‘ll never get here!
Console.WriteLine ("Exception!");
}
static void Go() { throw null; }
}

上例中的try/catch基本没用了,新创建的线程可能是未处理的空引用异常,最好在线程要执行的代码里加异常捕获:

public static void Main() {
new Thread (Go).Start();
}
static void Go() {
try {
...
throw null; // this exception will get caught below
...
}
catch (Exception ex) {
Typically log the exception, and/or signal another thread
that we‘ve come unstuck
...
}

从.NET2.0开始,线程上任何未处理的异常会导致整个程序挂掉,意味着千万别忽略异常,在线程要执行的函数里,给每个可能异常的代码加上try/catch。这可能有点麻烦,所以,很多人这样处理,用全局的异常处理:

using System;
using System.Threading;
using System.Windows.Forms;
static class Program {
static void Main() {
Application.ThreadException += HandleError;
Application.Run (new MainForm());
}
static void HandleError (object sender, ThreadExceptionEventArgs e) {
Log exception, then either exit the app or continue...
}
}

Application.ThreadException事件会在代码抛出异常时被触发,这样看起来很完美--可以捕获所有异常,但在worker线程上的异常可能捕获不了,在main函数里的窗体的构造函数,在Windows的消息循环之前就执行了。.NET提供了一个低层的事件捕获全局异常:AppDomain.UnhandledException,它才可以捕获所有异常(UI和非UI的)。虽然它提供了一个很好的方式捕获所有异常并记录异常日志,但是它没有办法阻止程序关系,也没有办法阻止.NET的异常对话框

时间: 2024-08-24 22:41:57

C# 线程入门 00的相关文章

UML建模快速入门00 Outline

Preface UML建模,其重要性不言而喻,本人虽然大学期间就早已知其大名,无奈因各种因素总是拿起又放下,未能持续研究,几经断断续续,一直未持续深入读完一本书.最近越发觉得逆向工程(由代码生成UML)在日常整理中的重要性及方便性,便又捡起书本,觉得应该好好看看,边看边画,对很多概念又有了较深入的理解,对以前模糊的概念更加明晰些了.说实在的,这次拿起书本来看,主要有两个原因:一是觉得UML建模确实应该作为码工具备的一个技能,二是为了提升下逼格. 为了记录个人历经的路程,后续将推出系列快速入门读书

python线程入门

目录 python线程入门 线程与进程 线程 总结 参考 python线程入门 正常情况下,我们在启动一个程序的时候.这个程序会先启动一个进程,启动之后这个进程会启动起来一个线程.这个线程再去处理事务.也就是说真正干活的是线程,进程这玩意只负责向系统要内存,要资源但是进程自己是不干活的.默认情况下只有一个进程只会拉起来一个线程. 多线程顾名思义,就是同样在一个进程的情况同时拉起来多个线程.真正干活的是线程.进程与线程的关系就像是工厂和工人的关系, 要想一个工厂运行起来,至少有一个工,当然如果工人

C#线程入门(一)

入门 概述与概念 创建和开始使用多线程 线程同步基础 同步要领 锁和线程安全 Interrupt 和 Abort 线程状态 等待句柄 同步环境 使用多线程 单元模式和Windows Forms BackgroundWorker类 ReaderWriterLock类 线程池 异步委托 计时器 局部储存 高级话题 非阻止同步 Wait和Pulse Suspend和Resume 终止线程 一.入门 1.     概述与概念 C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程

c#之线程入门

C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程同时地运行.一个C#程序开始于一个单线程,这个单线程是被CLR和操作系统(也称为“主线程”)自动创建的,并具有多线程创建额外的线程.这里的一个简单的例子及其输出: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Thread

线程入门,浅谈Thread与Runnable

一 实现Runnable接口使用一个线程. 这里我们用RunnableTest实现了该接口,在Main函数中创建了该实现类的对象,并传递给了 Thread的构造函数,然后调用start方法.代码如下 public class RunnableTest implements Runnable {     @Override     public void run() {        System.out.println(Thread.currentThread().getName() + " i

多线程面试秒杀系列5--多线程入门

首先我们先来看一个程序,从这个程序来引出多线程的控制手段,怎么样使得每个线程相互是互斥的,下面这个程序大概就是创建10个线程,并输出线程的序号,但是因为没有控制手段导致序号输出的时候是混乱的,因为线程的执行时没有顺序的它是并行执行的所以如果变量不加以控制就会出现混乱的情况.程序使用codeblocks编译的. #include <iostream> #include <stdio.h> #include <process.h> #include <windows.

java线程入门一

线程优先级: 在JAVA线程中,通过一个int型变量priority来控制线程优先级,线程的有限机为1-10,默认为5,优先级高的线程获得的运行时间要高于优先级低的线程.但这只是一个提示,操作系统和JVM可能会根据自身情况忽略这个情况.请看下面代码: import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class Priority { private sta

iOS学习笔记11-多线程入门

一.iOS多线程 iOS多线程开发有三种方式: NSThread NSOperation GCD iOS在每个进程启动后都会创建一个主线程,更新UI要在主线程上,所以也称为UI线程,是其他线程的父线程. 线程和进程的区别傻傻分不清楚: 线程(thread):用于指代独立执行的代码段. 进程(process):用于指代一个正在运行的可执行程序,它可以包含多个线程. 二.NSThread NSThreadhi轻量级的多线程开发,需要自己管理线程生命周期 创建线程主要实现方法: /* 直接将操作添加到

《day14---多线程入门_进阶》

1 /* 2 多线程: 3 4 进程:正在执行中的程序,一个应用程序启动后在内存中运行的那片空间.进程具有动态性和并发性. 5 6 线程:进程中的一个执行单元.负责进程中的程序的运行的.一个进程中至少要有一个线程. 7 一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序. 8 9 程序启动了多线程,有什么应用呢? 10 可以实现多部分程序同时执行.专业术语称之为 并发. 11 12 多线程的使用可以合理使用cpu的资源,如果线程过多会导致降低性能. 13 14 cpu在处理程序时是