干货分享:详解线程的开始和创建

阅读目录

原文地址:C#多线程之旅(2)——创建和开始线程

C#多线程之旅目录:

C#多线程之旅(1)——介绍和基本概念

C#多线程之旅(2)——创建和开始线程

C#多线程之旅(3)——线程池

C#多线程之旅(4)——APM初探

C#多线程之旅(5)——同步机制介绍

C#多线程之旅(6)——详解多线程中的锁

更多文章正在更新中,敬请期待......

C#多线程之旅(2)——创建和开始线程

回到顶部

代码下载

Thread_博客园_cnblogs_jackson0714.zip

第一篇~第三篇的代码示例:

源码地址:https://github.com/Jackson0714/Threads

回到顶部

一、线程的创建和开始

在第一篇的介绍中,线程使用Thread 类的构造函数来创建,通过传给一个ThreadStart 委托来实现线程在哪里开始执行。下面是ThreadStart的定义:

// Summary:
//     Represents the method that executes on a System.Threading.Thread.
[ComVisible(true)]
public delegate void ThreadStart();

调用一个Start方法,然后设置它开始运行。线程会一直运行直到这个方法返回,然后这个线程结束。

下面是一个例子,使用扩展C#语法创建一个ThreadStart委托:2.1_ThreadStart

 1 class ThreadTest
 2 {
 3     static void Main()
 4     {
 5         Thread t = new Thread(new ThreadStart(Go));
 6         t.Start();
 7         Go();
 8         Console.ReadKey();
 9     }
10     static void Go()
11     {
12         Console.WriteLine("hello!");
13     }
14 }

在这个例子中,thread t执行Go(),基本上与主线同时程调用Go()方法,结果是打印出两个时间接近的hello。

一个线程可以被方便的创建通过指定一个方法组,然后由C#推断出ThreadStart委托:2.2_Thread

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         Thread t = new Thread(Go);
 6         t.Start();
 7         Go();
 8         Console.ReadKey();
 9     }
10
11     static void Go()
12     {
13         Console.WriteLine("Go");
14     }
15 }

另外一种更简单的方式是使用lambda表达式或者匿名方法:2.3_LambaExpression

static void Main(string[] args)
{
    Thread t = new Thread(()=>Console.WriteLine("Go"));
    t.Start();
    Console.ReadKey();
}

回到顶部

二、传递数据给一个线程

1.利用Lambda传递一个数据

传递参数给线程的目标方法的最简单的方法是执行一个lambda表达式,该表达式调用一个方法并传递期望的参数给这个方法。

2.4_PassingDataToAThread

static void Main(string[] args)
{
    Thread t = new Thread(() => Print("A"));
    t.Start();
    Console.ReadKey();
}

static void Print(string message)
{
    Console.WriteLine(message);
}

2.传递多个参数

通过这种方式,你可以传递任意数量的参数给这个方法。你甚至可以将整个实现包装在一个多语句的lambda中:

2.5_PassingDataToAThread

new Thread(() =>
{
    Console.WriteLine("a");
    Console.WriteLine("b");
}).Start();

你也可以简单的在C# 2.0里面那样使用匿名方法做同样的事:

new Thread(delegate()
{
    Console.WriteLine("a");
    Console.WriteLine("b");
}).Start();

3.利用Thread.Start传递参数

另外一种方式是传递一个参数给ThreadStart方法:

2.6_PassingDataToAThread_ThreadStart

static void Main(string[] args)
{
    Thread t = new Thread(Print);
    t.Start("A");
    Console.ReadKey();
}
static void Print(object messageObj)
{
    string message = (string)messageObj;//必须进行转换
    Console.WriteLine(message);
}

这种方式能够工作是因为Thread的构造函数是重载的,接受下面两种中的任意一种委托:

// Summary:
//     Represents the method that executes on a System.Threading.Thread.
[ComVisible(true)]
public delegate void ThreadStart();

// Summary:
//     Represents the method that executes on a System.Threading.Thread.
//
// Parameters:
//   obj:
//     An object that contains data for the thread procedure.
[ComVisible(false)]
public delegate void ParameterizedThreadStart(object obj);

这个ParameterizedThreadStart的只允许接收一个参数。而且因为它的类型是object,所以通常需要转换。

4.Lambda表达式和捕获变量

由我们上面看到的例子可以知道,一个lambda式在传递数据给线程是最用的。然而,你必须非常小心在开始线程后意外修改捕获变量,因为这些变量是共享的。比如下面的:

2.7_LbdaExpressionsAndCapturedVariables

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

这个输出是不确定的,下面是一种典型的情况:

这里的问题是变量i在for循环执行时指向同一个内存地址。因此,每一个线程调用Console.Write时,i的值有可能在这个线程运行时改变。

解决方案是使用一个临时变量:

2.8_LambdaExpressionsAndCapturedVariables_Solution

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

变量temp在每个循环迭代中位于不同的内存块。因此每一个线程捕获到了不同的内存位置,而且没有问题。我们可以解释在之前的代码中的问题:

2.9_PassingData_TemporaryVariable

string text = "A";
Thread a = new Thread(() => Console.WriteLine(text));

text = "B";
Thread b = new Thread(() => Console.WriteLine(text));

a.Start();
b.Start();

因为两个lambda表达式捕获同样的text的值,所以B被打印出两次。

回到顶部

三、命名线程

每一个线程有一个Name属性你可以方便用来debugging.当线程显示在Visual Statudio里面的Threads Window和Debug Loaction toolbar的时候,线程的Name属性是特别有用的。你可以只设置线程的名字一次;之后尝试改变它将会抛出异常信息。

静态的Thread.CurrentThread属性代表当前执行的线程。

在下面的例子2.10_NamingThread中,我们设置了主线程的名字:

static void Main(string[] args)
{
    Thread.CurrentThread.Name = "Main Thread";
    Thread t = new Thread(Go);
    t.Name = "Worker Thread";
    t.Start();
    Go();
    Console.ReadKey();
}
static void Go()
{
    Console.WriteLine("Go! The current thread is {0}", Thread.CurrentThread.Name);
}

回到顶部

四、前台线程和后台线程

默认情况下,你自己显示创建的线程是前台线程。前台线程保持这个应用程序一直存活只要其中任意一个正在运行,而后台线程不是这样的。一旦所有的前台线程完成,这个应用程序就结束了, 任何正在运行的后台线程立刻终止。

一个线程前台/后台的状态跟它的优先级和配置的执行时间没有关联。

你可以使用线程的IsBackgroud属性查询或改变一个线程的后台状态。

下面是例子:2.11_PriorityTest

static void Main(string[] args)
{
    Thread t = new Thread(() => Console.ReadKey());
    if (args.Length > 0)//如果Main方法没有传入参数
    {
        //设置线程为后台线程,等待用户输入。
        //因为主线程在t.Start()执行之后就会终止,
        //所以后台线程t会在主线程退出之后,立即终止,应用程序就会结束。
        t.IsBackground = true;
    }
    t.Start();
}

如果程序调用的时候传入了参数,则创建的线程为前台线程,然后等待用户输入。

同时,如果主线程退出,应用程序将不会退出,因为前台线程t没有退出。

另一方面,如果main方法传入了参数,则创建的线程设置为后台线程。当主线程退出时,应用程序立即退出。

当一个进程以这种方式终止,则任何后台线程执行栈里面的finally 语句块将会被规避。

如果你的线程使用finally(or using)语句块去执行如释放资源或者删除临时文件的清理工作,这将是一个问题。为了避免这个,你可以显示地等待后台线程退出应用程序。

这里有两种实现方式:

  1. 如果你自己创建了这个线程,可以在这个线程上调用Join方法。
  2. 如果你使用线程池,可以使用一个事件去等待处理这个线程。

在这两种情况下,你需要指定一个timeout,因此可以结束一个由于某些原因拒绝完成的线程。这是你的备选退出策略:在最后,你想要你的应用程序关闭,不需要用户从任务管理器中删除。

如果用户使用任务管理器强制结束一个.NET进程,所有的线程像是后台线程一样终止。这个是观察到的行为,所以会因为CLR和操作系统的版本而不同。

前台线程不需要这样对待,但是你必须小心避免可能造成线程不能结束的bugs。造成应用程序不能正确地退出的一个通常的原因是有激活的前台线程还存活在。

回到顶部

五、线程优先级

一个线程的优先级决定了在操作系统中它可以得到多少相对其他线程的执行时间,下面是线程优先级的等级:

// Summary:
//     Specifies the scheduling priority of a System.Threading.Thread.
[Serializable]
[ComVisible(true)]
public enum ThreadPriority
{
    Lowest = 0,
    BelowNormal = 1,
    Normal = 2,
    AboveNormal = 3,
    Highest = 4,
}

当多线程同时是激活的,线程优先级是很重要的。

注意:提高线程优先级时,需要非常小心,这将可能导致其他线程对资源访问的饥饿状态的问题。

当提升一个线程的优先级时,不会使它执行实时工作,因为它被应用程序的进程优先级限制了。为了执行实时工作,你也必须通过使用System.Diagnostices的Process类来提升进程的优先级:

using (Process p = Process.GetCurrentProcess())
{
    p.PriorityClass = ProcessPriorityClass.High;
}

ProcessPriorityClass.High事实上是优先级最高的一档:实时。设置一个进程优先级到实时状态将会导致其他线程无法获得CPU时间片。如果你的应用程序意外地进入一个无限循环的状态,你甚至会发现操作被锁住了,只有电源键能够拯救你了。针对这个原因,High通常对于实时应用程序是最好的选择。

如果你的实时应用程序有一个用户界面,提高程序的优先级将会使刷新界面占用昂贵的CPU的时间,且会使整个系统变得运行缓慢(尤其是UI很复杂的时候)。降低主线程优先级且提升进程的优先级来确保实时线程不会被界面重绘所抢占,但是不会解决其他进程对CPU访问缺乏的问题,因为操作系统整体上会一直分配不成比例的资源给进程。一个理想的解决方案是让实时线程和用户界面用不同的优先级运行在不同的进程中,通过远程和内存映射文件来通信。即使提高了进程优先级,在托管环境中处理硬实时系统需求还是对适用性有限制。此外,潜藏的问题会被自动垃圾回收引进,操作系统会遇到新的挑战,即使是非托管代码,使用专用硬件或者特殊的实时平台,那将被最好的解决。

回到顶部

六、异常处理

在任何try/catch/finally 语句块作用域内创建的线程,当这个线程开始时,这个线程和语句块是没有关联的。

思考下面的程序:

参考例子:2.12_ExceptionHandling

static void Main(string[] args)
{
    try
    {
        new Thread(Go).Start();
    }
    catch(Exception ex)
    {
        Console.WriteLine("Exception");
    }
    Console.ReadKey();
}
static void Go()
{
    throw null;
}

try/catch 声明在这个例子中是无效的,而且新创建的线程将会被一个未处理的NullReferenceException所阻断。当你考虑每一个线程有一个单独的执行路径这种行为是说得通的。

改进方法是将exception handler移到Go()的方法中:

参考例子:2.13_ExceptionHandling_Remedy

class Program
{
    static void Main(string[] args)
    {
        new Thread(Go).Start();
        Console.ReadKey();
    }

    static void Go()
    {
        try
        {
            throw null;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

你需要在应用程序中的所有线程入口方法中添加一个exception handler ,就像你在主线程中做的那样。一个未处理的线程会造成整个应用程序关闭,而且会弹出一个不好看的窗口。

在写这个exception handling 语句块时,你可能极少忽略这个问题,典型情况是,你可能会记录exception的详细信息,然后可能显示一个窗口让用户去自动去提交这些信息到你的web server上。然后你可能会关掉这个应用程序-因为这个error毁坏了程序的状态。然后,这样做的开销是用户可能会丢失他最近的工作,比如打开的文档。

对于WPF和WinForm应用程序来说,全局的exception handling 事件(Application.DispatcherUnhandlerException 和Application.ThreadException)只会检测到主UI线程上的抛出的异常。你还是必须手动处理线程的异常。

AppDomain.CurrentDomain.UnhandledException可以检测任何未处理的异常,但是无法阻止应用程序之后关闭。

然而,某些情形下你不需要在线程上处理异常,因为.NET Framework为你做了这个。下面是没有提及的内容:

Asynchronous delegates

BackgroudWorker

The Task Parallel Library(conditions apply)

时间: 2024-08-09 22:00:58

干货分享:详解线程的开始和创建的相关文章

【android】 微信分享详解(分享到朋友和朋友圈)+ PopupWindow的使用和分析

一. 微信分享的实现: 1.到微信开放平台https://open.weixin.qq.com创创建应用申请AppID 2.下载签名生成工具,对签名不了解的自行百度,这里不做说明. 下面是简单的微信分享代码: 首先看一下包结构图 MainActivity: public class MainActivity extends Activity { private static final String appid = "wx86b3d972e5ddd153"; private IWXAP

Java 多线程详解(二)------如何创建进程和线程

Java 多线程详解(一)------概念的引入:http://www.cnblogs.com/ysocean/p/6882988.html 在上一篇博客中,我们已经介绍了并发和并行的区别,以及进程和线程的理解,那么在Java 中如何创建进程和线程呢? 1.在 Windows 操作系统中创建进程 在 windows 操作系统中,我们创建一个进程通常就是打开某个应用软件,这便在电脑中创建了一个进程.更原始一点的,我们在命令提示符中来做(我们以打开记事本这个进程为例): 第一步:windows+R,

并发编程 — 详解线程池

本文将讲述如何通过JDK提供的API自定义定制的线程池 Java代码   //固定线程数 -- FixedThreadPool public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } //单

YUM源详解以及EPEL源的创建

YUM是什么? yum(全称为Yellowdog Updater Modified)是一个在CentOS.RedHat和Fedora操作系统中使用的Shell前端软件包管理器.yum主要管理基于rpm的软件包,Centos先将发布的软件放置到YUM服务器内,然后分析这些软件的依赖属性问题,将软件内的记录信息写下来(header).然后再将这些信息分析后记录成软件相关性的清单列表.这些列表数据与软件所在的位置可以称为仓库(repository).当客户端有软件安装的需求时,客户端主机会向网络上面的

微信js-sdk分享详解及demo实例

步骤一:绑定域名 先登录微信公众平台进入"公众号设置"的"功能设置"里填写"JS接口安全域名". 步骤二:引入JS文件 在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.2.0.js <script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script&

【Android 分享】ShareSDK微信分享详解

目前市面上大大小小的软件,几乎都支持分享的功能,前几天在做的项目中也有此功能,使用的ShareSDK来实现的微信分享功能,下面就跟着我来一步步实现微信分享吧! ShareSDK介绍:ShareSDK官网,Mob移动开发者服务平台,为全球移动开发者提供社会化分享SDK.手游视频录像SDK.免费手机短信验证码SDK,BigApp掌上社区等服务. 1.在ShareSDK注册个账户,进入创建应用的界面: 点击创建应用按钮 2.输入自己的应用名称,选择开发平台(以Android为例) 3.点击确定,进入后

并发编程学习总结(二) : 详解 线程的6种不同状态

(一) 线程状态: 我们先讨论一下线程的几种状态: java中Thrad.State总共有6中状态: (1)New (新创建) (2)Runnable (可运行) (3)Bolcked (被阻塞) (4)Waiting (等待) (5)Timed Waiting (计时等待) (6)Terminated (被终止) 这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析和代码实例. 下面我们分别看一下线程的这6中状态分别出现在什么情况下. (1)New (新创建) 当我们执行new T

详解线程的信号量和互斥锁

前言:有个问题感觉一直会被问道:进程和线程的区别?也许之前我会回答: 进程:资源分配最小单位 线程:轻量级的进程 是系统调度的最小单位 由进程创建 多个线程共享进程的资源 但是现在我觉得一个比喻回答的更好:程序就像静止的火车,进程是运行的火车,线程是运行火车的每节车厢. 个人感觉理解远比背些概念性东西更好. 一.线程 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义.线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而

HTML5干货整理详解canvas的学习方法及学习曲线(收藏保存)

还记得在过去的Web前端开发中,如果你需要绘图或者生成相关图形的话,使用Flash可能是你唯一或者说最强大的实现方式,而在近些年的技术热点HTML5标准中,(画布)能够更加方便的帮助你实现2D绘制图形图像及其各种动画效果功能. 首先我们先来了解一下什么是HTML Canvas? 我们可以在HTML中使用属性width和height来定义Canvas.但是实现Canvas的相关功能主要还依赖于Javascript实现,即HTML5 Canvas API.我们使用javascript来访问和控制Ca