C # 多线程总结

基础概念

什么是线程

http://zh.wikipedia.org/zh-cn/%E7%BA%BF%E7%A8%8B
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

线程是独立调度和分派的基本单位。线程可以操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

一个进程可以有很多线程,每条线程并行执行不同的任务。

在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责IO处理、人机交互而常备阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

ps:以上摘自 Wiki 多个平台全面解释什么是线程,加深理解。

空间开销

线程的空间开销来自:

1)线程内核对象(Thread Kernel Object)。每个线程都会创建一个这样的对象,它主要包含线程上下文信息,在32位系统中,它所占用的内存在700字节左右。

2)线程环境块(Thread Environment Block)。TEB包括线程的异常处理链,32位系统中占用4KB内存。

3)用户模式栈(User Mode Stack),即线程栈。线程栈用于保存方法的参数、局部变量和返回值。每个线程栈占用1024KB的内存。要用完这些内存很简单,写一个不能结束的递归方法,让方法参数和返回值不停地消耗内存,很快就会发生OutOfMemoryException。

4)内核模式栈(Kernel Mode Stack)。当调用操作系统的内核模式函数时,系统会将函数参数从用户模式栈复制到内核模式栈。在32位系统中,内核模式栈会占用12KB内存。

时间开销

1)线程创建的时候,系统相继初始化以上这些内存空间。

2)接着CLR会调用所有加载DLL的DLLMain方法,并传递连接标志(线程终止的时候,也会调用DLL的DLLMain方法,并传递分离标志)。

3)线程上下文切换。一个系统中会加载很多的进程,而一个进程又包含若干个线程。但是一个CPU在任何时候都只能有一个线程在执行。为了让每个线程看上去都在运行,系统会不断地切换“线程上下文”:每个线程大概得到几十毫秒的执行时间片,然后就会切换到下一个线程了。这个过程大概又分为以下5个步骤:
步骤1: 进入内核模式。
步骤2: 将上下文信息(主要是一些CPU 寄存器信息)保存到正在执行的线程内核对象上。
步骤3: 系统获取一个 Spinlock,并确定下一个要执行的线程,然后释放 Spinlock。如果下一个线程不在同一个进程内,则需要进行虚拟地址交换。
步骤4: 从将被执行的线程内核对象上载入上下文信息。
步骤5: 离开内核模式。


C# 中使用线程的方法

总结了下,C# 下使用线程有一下几种方法,个人总结有可能不全或者有些错误,请大神指教。

  • Thread
  • Threadpool
  • Backgroundworker
  • Task
  • Asynchronous Delegates

随着微软的 .NET 技术不断推荐,这些的线程用法也有些变化,如在 Winform 中和 WPF 中使用时可能有些区别。本文示例主要以 WPF 为准。
在介绍每种用法前先交代一下公用函数,如果觉得代码代码被切割的看不懂了,可以下载工程代码:
https://github.com/AquariusCoder/WPF-Thread-Demon
https://github.com/AquariusCoder/WPF-Thread-Demon.git
http://download.csdn.net/detail/my___dream/8510525
代码很简单,一看就懂的,给我个鄙视的眼神,就可以离开了->_->
XAML

<Window x:Class="ThreadTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="595.773" Width="887.65">
    <Grid>
        <Button x:Name="btThread" Content="Thread" HorizontalAlignment="Left" Margin="2,0,0,531" VerticalAlignment="Bottom" Width="75" Click="btThread_Click"/>
        <ProgressBar x:Name="proThread" HorizontalAlignment="Left" Height="22" Margin="202,12,0,0" VerticalAlignment="Top" Width="668" TextOptions.TextFormattingMode="Display"/>
        <Button x:Name="btThreadpool" Content="Threadpool" HorizontalAlignment="Left" Margin="0,43,0,0" VerticalAlignment="Top" Width="75" Click="btThreadpool_Click"/>
        <ProgressBar x:Name="proThreadpool" HorizontalAlignment="Left" Height="22" Margin="202,43,0,0" VerticalAlignment="Top" Width="668"/>
        <Button x:Name="btBgWorker" Content="BgWorker" HorizontalAlignment="Left" Margin="0,75,0,0" VerticalAlignment="Top" Width="75" Click="btBgWorker_Click"/>
        <ProgressBar x:Name="proBgWorker" HorizontalAlignment="Left" Height="22" Margin="202,75,0,0" VerticalAlignment="Top" Width="668"/>
        <Button x:Name="btDelegate" Content="Delegate" HorizontalAlignment="Left" Margin="0,106,0,0" VerticalAlignment="Top" Width="75" Click="btDelegate_Click"/>
        <ProgressBar x:Name="proDelegate" HorizontalAlignment="Left" Height="22" Margin="202,106,0,0" VerticalAlignment="Top" Width="668"/>
        <Button x:Name="btTask" Content="Task" HorizontalAlignment="Left" Margin="0,137,0,0" VerticalAlignment="Top" Width="75" Click="btTask_Click"/>
        <ProgressBar x:Name="proTask" HorizontalAlignment="Left" Height="22" Margin="202,137,0,0" VerticalAlignment="Top" Width="668"/>
        <Label x:Name="lbThread" Content="线程 Id:" HorizontalAlignment="Left" Margin="96,13,0,0" VerticalAlignment="Top" Width="101"/>
        <Label x:Name="lbThreadpool" Content="线程 Id:" HorizontalAlignment="Left" Margin="96,44,0,0" VerticalAlignment="Top" Width="101"/>
        <Label x:Name="lbBgWorker" Content="线程 Id:" HorizontalAlignment="Left" Margin="96,75,0,0" VerticalAlignment="Top" Width="101"/>
        <Label x:Name="lbDelegate" Content="线程 Id:" HorizontalAlignment="Left" Margin="96,106,0,0" VerticalAlignment="Top" Width="101"/>
        <Label x:Name="lbTask" Content="线程 Id:" HorizontalAlignment="Left" Margin="96,135,0,0" VerticalAlignment="Top" Width="101"/>

    </Grid>
</Window>
private void AsyncProgressBar(ProgressBar proBar)
{
    // do some thing asynchrouse
    int val = 0;
    while (true)
    {
        // 线程内不能直接访问 UI 对象,需要使用 Invoke 或者 BeginInvoke
        object[] args = new object[2];
        args[0] = proBar;
        args[1] = val++;
        Dispatcher.BeginInvoke(new GoProgressHandle(GoProgress), args);
        if (val >= 100)
            val = 0;

        System.Threading.Thread.Sleep(100);
    }
}

private void GoProgress(ProgressBar proBar, int val)
{
    proBar.Value = val;
}

private void ShowThreadId(Label lb)
{
    object[] args = new object[2];
    args[0] = lb;
    args[1] = System.AppDomain.GetCurrentThreadId();
    Dispatcher.BeginInvoke(new ShowThreadIdHandle(ShowThreadId_Invoke), args);
}

private void ShowThreadId_Invoke(Label lb, int id)
{
    lb.Content = string.Format("线程 Id:{0}", id);
}

Thread

private void ThreadPro(object obj)
{
    ShowThreadId(this.lbThread);
    AsyncProgressBar((ProgressBar)obj);
}
System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(ThreadPro));
t.Start(this.proThread);

Thread 执行完成之后变成 DeathThread 等待垃圾回收。所以如果你的程序会产生很多线程还是用线程池吧,不然你这么虐待她,当心她死给你看。

Threadpool

private void ThreadpoolPro(object state)
{
    ShowThreadId(this.lbThreadpool);
    AsyncProgressBar(this.proThreadpool);
}
private void btThreadpool_Click(object sender, RoutedEventArgs e)
{
    // 排队任务,线程池有空线程时进入线程函数
    System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(ThreadpoolPro));
}

线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。注意 QueueUserWorkItem 函数调用后只是进入排队队列,等待线程池有空闲线程时接管。
所以,什么时候用 Thread 什么时候用 Threadpool 就很明了了。

Backgroundworker

System.ComponentModel.BackgroundWorker bgWorker = new System.ComponentModel.BackgroundWorker();
public MainWindow()
{
    InitializeComponent();
    this.bgWorker.DoWork += bgWorker_DoWork; // 不可以注册多次
}

private void bgWorker_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
    ShowThreadId(this.lbBgWorker);
  AsyncProgressBar(this.proBgWorker);
}
private void btBgWorker_Click(object sender, RoutedEventArgs e)
{
    if (this.bgWorker.IsBusy)
        return;

    this.bgWorker.RunWorkerAsync();
    // bgWorker 可以在线程函数中直接调用 ReportProgress 当然要先注册事件响应函数
    // this.bgWorker.ProgressChanged += bgWorker_ProgressChanged;
    // bgWorker.ReportProgress
}

内部使用 Threadpool 实现,实现了 IComponentModel 接口,封装了进度报告等 UI 相关事件,方便 UI 相关编程。

Task

private void ThreadpoolPro(object state)
{
    ShowThreadId(this.lbThreadpool);
    AsyncProgressBar(this.proThreadpool);
}
private void btTask_Click(object sender, RoutedEventArgs e)
{
    Task tk = new Task(new Action(TaskPro));
    tk.Start();
}

内部使用 Treadpool 实现。
thread是单核多线程,task是多核多线程。也就是说在多核的情况下使用 Task 会有一些效率的提升,具体提升情况,看具体情况了。所以使用 Thread 还是 Task 看情况而定了。注意: Task 是 .NET 4.0 新特性。

Asynchronous Delegates

delegate void AsyncDelegate(ProgressBar proBar);

private void DelegatePro(ProgressBar proBar)
{
    ShowThreadId(this.lbDelegate);
    AsyncProgressBar(proBar);
}

private void btDelegate_Click(object sender, RoutedEventArgs e)
{
    AsyncDelegate dele = new AsyncDelegate(DelegatePro);
    dele.BeginInvoke(this.proDelegate, null, null);
}

内部使用 Treadpool 实现。

前台线程 & 后台线程

在 CLR(公共语言运行时)中只要有一个前台线程在运行,应用程序就是激活的,也就是说 main 函数要是提前完成了,但有一个或多个前台线程在运行,那么应用程序就是激活的。
Thread 创建的线程默认都是前台线程,可以通过 IsBackground 修改线程前后台属性。Threadpool 创建的线程都是后台线程,且不能改变。

class program
{
    public static void Main()
    {
        Thread t = new Thread(new ThreadStart(ThreadPro));
        t.IsBackground = true; // 去掉这句,结果不一样

        Console.WriteLine("Begin Thread...");
        t.Start();
        Console.WriteLine("End Thread...");
    }

    private static void ThreadPro()
    {
        Console.WriteLine("ThreadPro...");
    }
}

线程状态

通过 TreadStat 属性可以读取线程状态。
1)Thread.Start() 函数调用后线程处于 Unstarted 状态,只有当线程调度器分配给线程 CUP 时间后线程才进入 Runing 状态
2)Thread.Sleep() 函数调用后会使线程进入 WaitSleepJoin 状态,等待函数定义的时间后线程自动被唤醒。
3)Thread.Abort() 函数调用后会先线程发送终止请求,线程收到请求后引发 ThreadAbortException 线程捕获异常后进行清理工作(当然前提是你写了 try catch 语句)。
4)Thread.Join() 函数能使当前线程阻塞以等待工作线程任务完成。

结论

1) 长时间执行繁重后台工作使用 Thread, 产生大量线程使用 Treadpool
2) UI 后台处理,并且需要反馈和控制使用 BackgroundWorker
3) Task 为 .NET 4.0 的新特性,注意版本问题,对于多核 CUP 有处理优势

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值