C# 多线程总结

C# 多线程总结

1 创建线程

 

1.1 异步委托方式

使用异步委托创建的线程,都是由.Net线程池维护的。
线程池中的线程总是后台线程。
为了方便起见,接下来使用的共通委托方法如下1

static int TakesAWhile(int data, int ms)
{
    Console.WriteLine("TakesAWhile started");
    Thread.Sleep(ms);
    Console.WriteLine("TakesAWhile completed");
    return ++data;
}

1.1.1 IAsyncResult.IsCompleted

根据IAsyncResult.IsCompleted判断异步委托是否执行完成。
EndInvoke获取返回值。

TakesAWhileDelegate dl = TakesAWhile;
IAsyncResult ar = dl.BeginInvoke(1, 3000, null, null);
while (!ar.IsCompleted)
{
    Console.Write(".");
    Thread.Sleep(50);
}
int result = dl.EndInvoke(ar);
Console.WriteLine("result: {0}", result);

1.1.2 IAsyncResult.AsyncWaitHandle

使用WaitHandle,可指定异步调用的超时时间进行后续处理。

TakesAWhileDelegate dl = TakesAWhile;
IAsyncResult ar = dl.BeginInvoke(1, 3000, null, null);
if (!ar.AsyncWaitHandle.WaitOne(200, false))
{
    Console.WriteLine("Thread not invoked.");
}
if (ar.AsyncWaitHandle.WaitOne(3000, false))
{
    int result = dl.EndInvoke(ar);
    Console.WriteLine("result: {0}", result);
}

1.1.3 AsyncCallBack

通过传入回调函数,进行后续处理

  • 分支一:单独定义回调方法
    static void Main(string[] args)
    {
        TakesAWhileDelegate dl = TakesAWhile;
        dl.BeginInvoke(1, 3000, TakesAWhileCompleted, dl);
        //必须程序主线程一直存在才会执行回调方法,所以使用了如下for循环(说明了异步委托所创建的线程是一个后台线程)
        for (int i = 0; i < 100; i++ )
        {
            Console.Write(".");
            Thread.Sleep(50);
        }
    }
    
    //定义回调方法
    static void TakesAWhileCompleted(IAsyncResult ar)
    {
        if (ar == null)
        {
            throw new ArgumentNullException("ar");
        }
        TakesAWhileDelegate dl = ar.AsyncState as TakesAWhileDelegate;
        Trace.Assert(dl != null, "Invalid object type");
        int result = dl.EndInvoke(ar);
        Console.WriteLine("result: {0}", result);
    }
    
  • 分支二:使用lambada表达式
    TakesAWhileDelegate dl = TakesAWhile;
    dl.BeginInvoke(1, 3000,
                   //这是个回调函数,使用lambada表达式的话,代码不够清晰。
                   ar =>
        {
            int result = dl.EndInvoke(ar);//lambda表达式可使用该作用域外部的变量dl
            Console.WriteLine("result: {0}", result);
        },
                   null);
    //必须程序主线程一直存在才会执行回调方法
    for (int i = 0; i < 100; i++)
    {
        Console.Write(".");
        Thread.Sleep(50);
    }
    

1.2 Thread类

 

1.2.1 无参数线程方法

var t1 = new Thread(() => Console.WriteLine("running in a thread, id {0}", Thread.CurrentThread.ManagedThreadId));
t1.Start();
Console.WriteLine("This is a main thread, id {0}", Thread.CurrentThread.ManagedThreadId);

1.2.2 有参数线程方法

public struct Data
{
    public string Message;
}

static int TakesAWhile(int data, int ms)
{
    var d = new Data { Message = "Info" };
    var t2 = new Thread((object obj) =>
        {
            Data data = (Data)obj;
            Console.WriteLine("running in a thread, id {0}, Data {1}", Thread.CurrentThread.ManagedThreadId, data.Message);
        });
    t2.Start(d);
    Console.WriteLine("This is a main thread, id {0}", Thread.CurrentThread.ManagedThreadId);
}

1.2.3 后台线程

Thread类默认创建的是前台线程,设定IsBackground属性可转为后台线程

var t1 = new Thread(
    () => 
    {
        Console.WriteLine("branch thread Start, id {0}", Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(3000);
        Console.WriteLine("branch thread End");
    }) 
{ Name = "NewBKThread", IsBackground = true };
t1.Start();
Thread.Sleep(50);//为了使后台线程的情况下,能打出branch thread Start, id
Console.WriteLine("This is a main thread, id {0}", Thread.CurrentThread.ManagedThreadId);
  • IsBackground = true 结果:

branch thread Start, id 3
This is a main thread, id 1

  • IsBackground = false 结果:

branch thread Start, id 3
This is a main thread, id 1
branch thread End

1.2.4 关于线程优先级

可以通过Thread.Priority属性调整线程的 基本 优先级。实际线程调度器会动态调整优先级
频繁使用CPU的线程的优先级会动态调低,等待资源(等待磁盘IO完成等)的线程会动态调高优先级。
以便在下次等待结束时获得CPU资源。2

1.2.5 线程状态

通过属性Thread.ThreadState获取当前线程状态

运行Thread.Start()后,状态为Unstarted。
系统线程调度器选择了运行该线程后,状态为Running。
调用Thread.Sleep(),状态为WaitSleepJoin。

停止另一个线程,调用Thread.Abort()。接到中止命令的线程中会抛出ThreadAbortException。3
涉及的状态有AbortRequested、Aborted。
继续停止的线程,调用Thread.ResetAbort()。线程将会在抛出ThreadAbortException后的语句后继续进行。

等待线程的结束,调用ThreadInstance.Join()。
该调用会停止 当前 线程,当前线程状态设为WaitSleepJoin。
等待加入的线程处理完成,再继续当前线程的处理。

1.3 线程池

超出最大线程数时,QueueUserWorkItem会等待获取线程资源时再调用。

static void Main(string[] args)
{
    ThreadPool.SetMinThreads(3, 3);//创建线程池时启动的最小线程数
    ThreadPool.SetMaxThreads(10, 10);//最大线程数
    for (int i = 0; i < 5; i++ )
    {
        ThreadPool.QueueUserWorkItem(JobForAThread);
    }
    Thread.Sleep(3000);//由于是后台线程,需要使主线程等一会,否则程序直接退出
}

static void JobForAThread(object state)
{
    for (int i = 0; i < 3; i++)
    {
        Console.WriteLine("loop {0}, running inside pooled thread {1}", i, Thread.CurrentThread.ManagedThreadId);
    }
}

使用线程池的限制:

  • 其中的所有线程只能是后台线程。
  • 无法设置线程的优先级或名称。
  • 关键点 适用于耗时较短的任务。长期运行的线程,应使用Thread类创建。

2 同步问题

 

2.1 lock关键字

只能锁定引用类型,锁定值类型等于锁定了一个副本,没有意义,编译器也不允许你这么做。
使用锁定需要时间,并不总是必须。可以创建类的两个版本,一个同步版本,一个异步版本。

2.1.1 将实例成员设为线程安全的

lock(this)
{
    //一次只有一个线程能访问相同实例的该语句块
}

因为该实例对象也可用于外部访问,这样做会导致外部访问时也得等待该同步语句块执行完成。正确的做法:

private object syncRoot = new object();
public void DoSomething()
{
    lock (object)
    {
        //Do something
    }
}

private static object syncRoot = new object();//可用于锁定类静态成员

2.1.2 lock关键字由编译器解析为Monitor类

lock (obj) {  };

等价于:

Monitor.Enter(obj);
try
{
}
finally
{
    Monitor.Exit(obj);
}

与lock关键字的区别:

  • 可添加一个等待解锁的超时时间,使用TryEnter传递超时值。
bool lockTaken = Monitor.TryEnter(obj, 500);
if (lockTaken)
{
    try
    {

    }
    finally
    {
        Monitor.Exit(obj);
    }
}
else
{
    //didn't get the lock, do something else
}

2.1.3 更快速的Interlocked类

仅用于简单的针对变量赋值的同步问题

lock(this)
{
    if (someState == null)
    {
        someState = newState;
    }
}

等价于(可用于单件模式的GetInstance):

Interlocked.CompareExchange<SomeState>(ref someState, newState, null);//第一个参数和第三个参数比较,如果相等,替换为第二个参数的值
public int State
{ 
    get 
    {
        lock (this)
        {
            return ++state;
        }
    }
}

等价于:

public int State
{
    get
    {
        return Interlocked.Increment(ref state);
    }
}

2.2 WaitHandle

WaitHandle是一个抽象基类。用于等待某个信号量。
Mutex、EventWaitHandle、Semaphore类都从WaitHandle派生。

2.3 Mutex类

提供进程之间的同步访问。创建一个进程之间能共享的以字符串命名的互斥锁。
构造函数的一种形式如下:

bool created;
Mutex mutex = new Mutex(false, "IFFileMutex", out created);

其中,第一个参数定义了该互斥体的所有权是否应属于调用线程。
第二个参数是互斥体名字,操作系统能识别该字符串,以此实现各进程之间的同步。
第三个参数,如果系统中已存在该命名的互斥体返回false,否则返回true。

Mutex mutex = Mutex.OpenExisting("IFFileMutex");//打开系统中已存在的互斥体
if(mutex.WaitOne(500))//500为等待超时时间
{
    try
    {
        //synchronized region
    }
    finally
    {
        mutex.ReleaseMutex();
    }
}

2.4 Semaphore类

信号量可以同时由多个线程使用,是计数的互斥体。一般用于受数量限制的访问资源(如DB连接资源)。

2.5 Event类

系统级的资源同步方式,比之Mutex,多了个Reset方法,
重置nonsignaled的状态(等同于互斥体的锁定状态),释放所有等待的线程。

Set方法:将事件设为signaled状态,使其他等待的线程得以继续,类似锁的Release方法。
Waitone方法:等待事件被设为signaled状态。
Reset方法:将事件设为nonsignaled状态,并且阻塞所有等待的线程。

2.5.1 AutoResetEvent

Reset方法会在某一线程Waitone成功后,自动重置为nonsignaled。
达到的效果:一次只能一个线程继续处理。

2.5.2 ManualResetEvent

需手动调用Reset方法重置为nonsignaled。
达到的效果:多个线程都能继续进行处理。

2.6 ReaderWriterLockSlim类(.Net 3.5引入)

如果没有Writer锁定资源,就允许多个Reader访问资源,但只能有一个
Writer锁定该资源(所有访问中的Reader都必须先释放锁)。
比之.Net 1.0版本 ReaderWriterLock类,重新设计为防止死锁,提供更好的性能。

  • EnterReadLock 进入锁定,另一个方法TryEnterReadLock允许指定一个超时时间。ExitReadLock释放锁定
  • EnterUpgradableReadLock 用于读取锁定需要改为写入锁定的情况。
  • EnterWriteLock 获得多资源的写入锁定。仅一个线程能获取锁定,在这之前还必须释放所有的读取锁定。

3 Timer类

.Net提供了几个Timer类,比较如下:

 
命名空间 说明
System.Threading 提供了Timer的核心功能,在构造函数中传入回调的委托。
System.Timer 继承Component,可在设计界面拖入,提供了基于事件的机制(非委托)。
System.Windows.Forms 为单线程环境设计的(创建和回调在同一个线程中执行),执行回调方法时UI会假死,不宜执行耗时较长的代码。该Timer时间精度55ms。
System.Web.UI 是一个AJAX扩展,可以用于Web页面

4 总结

 
目的 参考开销4 是否跨进程?
lock(Monitor) 保证单个进程内只有一个线程能够获取同步资源 20ns No
Mutex 保证只有一个线程能够获取同步资源 1000ns Yes
Semaphore 可指定可获取同步资源的线程数 1000ns Yes
ReaderWriterLock 允许多个Reader访问同步资源 100ns No
AutoResetEvent 当信号被设为signaled状态时,允许单个线程进入同步资源块 1000ns Yes
ManualResetEvent 当信号被设为signaled状态时,允许所有等待线程进入同步资源块 1000ns Yes
ReaderWriterLockSlim 可锁定多个Reader访问资源以及单个Writer修改资源 40ns No
  • 注:一些Slim类(如ManualResetEventSlim),比之旧版本,通常拥有更好的性能。参考 MSDN

几条规则:

  1. 尽量使同步要求最低,尝试避免共享状态。
  2. 类的静态成员应是线程安全的。
  3. 实例成员不需要是线程安全的。为了最佳性能,最好在类的外部处理同步问题。

完整代码示例:MultiThreadDemo.rar

5 推荐阅读

Threading in C#

Footnotes:

1 例子参照《C# 高级编程(第7版)》

2 给线程指定较高的基本优先级时,需注意。这有可能会降低其他线程的运行概率。

3 可以捕捉该异常完成线程的资源清理任务。

4 该时间测自CPU Intel Core i7 860的环境,参考Threading in C#

Last Updated 2013-05-17 14:41:00 中国标准时间

Author: ChrisChen([email protected])

 

你可能感兴趣的:(多线程)