C# -- 多线程

本文是一篇随心所欲的文章,内容不全面也不严谨,是本人自己的知识理解与补充,包教不包会,大神绕行,新手见谅。

委托在多线程中应用比较广泛,所以现在了解一下委托的概念。

委托

internal delegate void Feedback(int value);

委托就是回掉在C#中的体现形式,在一个类型中通过委托来调用另一个类型的私有成员,只要委托对象的可访问性够,就可以访问,不受限于被访问成员是否私有所限。

将方法绑定到委托是,CLR允许引用类型的协变性和逆变性,所为协变性指方法能返回从委托的返回类型派生的一个类型,逆变性指方法获取的参数可以是委托的参数类型的基类。

1.  线程的利与弊

线程的前提是你需要有多个和多核CPU的支持,因为在同一时刻每个CPU只能执行一个线程,换句话说windows确保单个线程不会 通知被多个内核上调度。

多线程可以说是比较矛盾的产物,因为线程需要耗用大量内存,而且需要相当多的时间来创建,销毁和管理,还需要切换上下文环境,在内存回收的时候还需要挂起所有线程的耗时,以及带来的状态共享和死锁的问题,以上这些种种弊端是和你线程的多少成正比的。但是有很多时候我们又不得不使用线程,因为多线程能过是我们的程序变得更健壮,响应更灵敏,给用户带来好的用户体验。

线程的启动:

// Thread Class
public sealed class Thread: CriticalFinalizerObject{
    public Thread(ParameterizedThreadStart start);
}

delegate void ParameterizedThreadStart(Object obj);

// apply
Thread thread = new Thread(Test);
thread.Start(0);

private static void Test(object state){
    Console.WriteLine(state.ToString());
}

2. 前台线程与后台线程

CLR将每个线程区分为前台/后台线程。简单来说,如果所有前台线程结束,则会退出程序,强制终止后台线程并不抛出异常。主线程和通过Thread对象创建的任何线程都默认是主线程,相反线程池默认为后台线程,但是在线程的生存周期内是可以前台转后台,后台转前台的。

额外提一点,我们应用多线程的时候要尽量用后台线程,后台线程的生存期能够与应用程序保持一致,程序退出线程终止。前台不然,会出现退出程序但是实际上没真正退出因为有某个前台线程依然在运行,这种问题往往比较隐秘不易发现。同时线程池也会自动帮你创建和销毁线程,何乐而不为。

3. CLR线程池

每个CLR都有自己的线程池,如果一个AppDomain中加载了多个CLR就会有多个线程池。如果AppDomain中的CLR是共享的那么它们的线程池就是共享的,不能再多个程序集中线程的执行位置。

线程池中的线程是这样执行的,在CLR初始化的时候线程池中是没有线程的,在内部线程池维护了一个请求队列,应用程序执行一个异步操作时,调用方法将一个记录项追加到线程池的队列中,然后线程池的代码从这个队列中提取记录项,将这个记录项派发给一个线程池线程,如果线程池中没有线程,就创建一个新线程。在线程完成任务后,线程不会被销毁而是返回到线程池,在线程池中进入空闲状态,等待下一个请求。如果一段时间内(CLR中有对这个时间的定义)没有下一个请求,这时才会自己释放资源回收。

// Definition
static Boolean QueueUserWorkItem(WaitCallback callback);
static Boolean QueueUserWorkItem(WaitCallback callback);

delegate void WaitCallback(Object state);

// Apply
ThreadPool.QueueUserWorkItem(Test, 0);

private static void Test(Object state){
    Console.Write(state.ToString());
}

4. 任务Task

尽管ThreadPool很方便的发起一次异步操作,而且还可以添加取消异步操作的callback方法,但是它依然存在很多不足,例如不知道操作什么时候完成,获取操作的返回值等。为了解决诸如以上的问题引入了任务的概念,System.Threading.Tasks.

使用方式对比如下:

// ThreadPool
ThreadPool.QueueUserWorkItem(Test, 0);

// Task Method 1
new Task(Test, 0).Start();

// Task Method 2
Task.Run(()=> Test(0));

如何任务完成时自动启动新任务?

如果调用Task的Wait方法或者Result属性,会有额外问题不推荐使用,额外问题是如果当前任务未完成时调用Wait/Result,则会在线程池中创建新线程来处理这个被等待的线程,如果被等待的线程加锁了并且正在锁中就会产生死锁的现象。

Task的ContinueWith(newTask)推荐使用,一个任务完成之后执行新的任务,同时还可以传递TaskCreationOptions枚举来控制新任务的执行行为。

5. Parallel的使用

Parallel,System.Threading.Tasks.Parrallel为了简化编程,内部对Task进行了封装,前提是任务是并行执行,而不是顺利执行,在Parallel的基础上又提供了PLINQ的支持,不过个人不推荐LINQ相关操作,维护起来比较蛋疼,感兴趣自己查这里不多介绍了。Parallel常用的方法如下:

// 循环,并行处理
// For 更快
Parallel.For(0, 100, i => Test(i));

Parallel.ForEach(list, item => Test(item));

// 多个方法并行执行
Parallel.Invoke(() => Method1(), () => Method2(), () => Method3());

6. Timer定时任务*****

System.Threading命名空间定义了一个Timer类,它让一个线程池线程定时调用一个方法,然后再某个时间点(自己设置)回掉你指定的回掉方法。

// Timer构造器
public sealed class Timer: MarshalByRefObject, IDisposable {
    public Timer(TimerCallback callback, Object state, Int32 dueTime, Int32 period);
    public Timer(TimerCallback callback, Object state, Int64 dueTime, UInt32 period);
    public Timer(TimerCallback callback, Object state, Int64 dueTime, Int64 period);
    public Timer(TimerCallback callback, Object state, Timespan dueTime, TimeSpan period);
}

// callback委托
delegate void TimerCallback(Object state);

其实Timer有很多个,WinForm,WPF,WindowStore,TimeService中常用的Timer.Time等,以上都有自己想对应的Timer类,不过定时任务还是Threading.Timer比较经典,其它根据具体场景选择即可。

7. 活锁和死锁

首先构造线程的时候分为用户构造和内核构造。

用户构造是指用特殊的指令操作CPU,可以理解为这个操作发生在硬件上,一个线程在某个CPU中快速执行,拿到可用资源的时候,操作资源,阻塞时间短,在没有资源的时候在内存当中自转,从而浪费CPU,这种现象被称为活锁。

内核构造是指由Windows操作系统自身提供的,应用程序线程实际上调用的操作系统的内核函数,优点当内核模式构造的线程获取资源时,Windows会组合线程以避免浪费CPU,当资源可用时再恢复线程执行,如果资源一直不可用就会出现死锁。

8. 内核构造

事件和信号量是两种基元内核模式线程同步构造了。而我们常用的是WaitHandle抽象基类,它包装了一个Windows内核对象的句柄(也就是SafeWaitHandle字段)。层次结构如下:

WaitHandle => EventWaitHandle => AutoResetEvent,ManualResetEvent

                    => Semaphore

                    => Mutex

接下来着重了解一下Event构造,其实event知识由内核维护的Boolean变量,false时在时间上等待的线程阻塞,true时接触阻塞。有两种事件,自动重置事件和手动重置事件了,区别在于当事件为true时,自动的会唤醒一个阻塞线程,之后内核自动重置为false,手动的则唤醒所有阻塞的线程,需要手动将事件设置为false。

public class EventWaitHandle: WaitHandle{
    public Boolean Set();
    public Boolean Reset();
}

public sealed class AutoResetEvent: EventWaitHandle{
    public AutoResetEvent(Boolean state);
}

public sealed class ManualResetEvent: EventWaitHandle{
    public ManulResetEvent(Boolean state);
}

internal sealed class SimpleWaitLock: IDisposable{
    private readonly AutoResetEvent available;
    public SimpleWaitLock(){
        this.available = new AutoResetEvent(true);
    }
    public void Enter(){
        this.available.WaitOne();
    }
    public void Leave(){
        this.available.Set();
    }
    public void Dispose(){
        this.available.Dispose();
    }
}

信号量,信号量其实时内核维护的Int32变量,0时在信号量上等待的线程会阻塞,>0时解除阻塞,接触一个阻塞线程信号量计数减1。

9. 双检锁

双检锁,是利用单例对象的构造推迟到首次被请求的时候构造,也就是延迟初始化,但当多个线程同时请求这个单例对象时有可能会出问题,所以需要线程同步机制保证单例对象只被创建一次。

public sealed class Singleton{
    private static Object locker = new Object();
    private static Singleton s_value = null;
    private Singleton(){
    
    }

    public static Singleton GetSingleton(){
        if(s_value != null){
            return s_value;
        }
        Monitor.Enter(locker);
        if(s_value == null){
            Singleton temp = new Singleton();
            Volatile.Write(ref s_value, temp);
        }
        Monitor.Exit(locker);
        return s_value;
    }
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值