C# 多线程(一)
2015-10-22 11:27
309 查看
(一)
通过System.Threading命名空间中提供的Thread类来创建和控制线程,ThreadPool类用于管理线程池等;
Thread类有几个至关重要的方法,描述如下:
Start(); 启动线程
Sleep(int); 静态方法,暂停当前线程制定的毫秒数
Abort(); 通常使用该方法来终止一个线程
Join(); 等待直到线程结束
Suspend(); 该方法并不终止未完成的线程,它仅仅挂起线程,以后还可以恢复
Resume(); 恢复被Suspend()方法挂起的线程执行。
(二)如何操纵一个线程
使用Thread类创建线程时,只需要提供线程入口即可。
在C#中,线程入口是通过ThreadStart代理(delegate)来提供的,当调用Thread.Start()方法后,线程就开始执行ThreadStart所指向的函数。
Thread oThread = new Thread(new
ThreadStart(oAlpha.Beta));
oThread.Start();
while
(!oThread.IsAlive) ;
{
Thread.Sleep(2);
oThread.Abort();
oThread.Join();
Console.WriteLine();
Console.WriteLine("Alpha.Beta
has finished");
try
{
Console.WriteLine("Try to restart the Alpha.Beta
thread");
oThread.Start();
}
catch
(ThreadStateException)
{
Console.WriteLine("ThreadStateException trying
to restart Alpha.Beta.");
Console.WriteLine("Exception since aborted
threads cannot be restarted.");
Console.ReadKey();
}
}
在创建线程oThread时,我们用指向oAlpha.Beta()方法初始化了ThreadStart代理(delegate)对象,当我们创建线程oThread调用oThread.Start()方法启动时,实际上程序运行的是oAlpha.Beta()方法。
在Main方法中的while循环中,使用静态方法Thread.Sleep()让主线程暂停一段时间,这段时间CPU转向执行线程oThread。然后我们使用Thread.Abort()方法终止了线程oThread,注意后面的oThread.Join(),Thread.Join()方法使主线程等待,直到oThread线程结束。也可以给Thread.Join()方法指定一个int型的参数作为最长的等待时间。
之后我们试图用Thread.Start方法重新启动线程oThread,但是,显然Abort()方法终止后的线程不可恢复,所以程序最后抛出ThreadStateException异常。
Thread.ThreadState 属性
这个属性代表了程序运行时的状态。
ThreadState属性的取值如下:
Aborted 线程已停止
AbortRequested 线程的Thread.Abort()方法已被调用,但是线程还未停止
Background 线程在后台执行,与属性Thread.IsBackground有关
Running 线程正在正常运行
Stopped 线程已经被停止
StopRequested 线程正在被要求停止
Suspended 线程已经被挂起(在此状态下,可以通过Resume()方法重新运行)
SuspendRequest 线程正在要求被挂起,但是还未来得及相应
Unstarted 未调用Thread.Start()开始线程的运行
WaitSleepJoin 线程因为调用了Wait(),Sleep()或Join()等方法处于封锁状态。
注:后台线程与前台线程只有一个区别,那就是后台线程不妨碍程序的终止。一旦一个进程所有的前台线程都终止后,CLR(通用语言运行环境)将调用任意一个存活中的后台进程的Abort()方法来彻底终止进程。
线程的优先级
当线程之间争夺CPU时间时,CPU是按照线程的优先级给予服务的。在C#程序中,用户可以设定5个不同的优先级,由高到低分别是Highest,AboveNormal,Normal,BelowNormal,Lowest,在创建线程时如果不指定优先级,那么系统默认为ThreadPriority.Normal。
(三)生产者和消费者
每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。几个线程同时执行一个函数,会导致数据混乱,产生不可预料的结果。必须避免这种情况的发生。
C#提供了一个关键字lock,可以把一段代码定义为互斥段(critical
section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义入下:
lock(expression) statement_block
expression代表你希望跟踪的对象,通常是对象引用。如果你想保护一个类的实例,一般的,可以使用this;保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。
statement_block就是互斥代码段,这段代码在一个时刻内只可能被一个线程执行。
lock语句(C#)MSDN http://msdn.microsoft.com/zh-cn/library/c5kehkcz(v=vs.100).aspx
下例使用线程和 lock。只要 lock 语句存在,语句块就是临界区并且 balance 永远不会是负数。
using System;
using System.Threading;
class Account
{
private
Object thisLock = new Object();
int
balance;
Random r
= new Random();
public
Account(int initial)
{
balance =
initial;
}
int
Withdraw(int amount)
{
// This condition will never
be true unless the lock statement
// is commented
out:
if (balance <
0)
{
throw new Exception("Negative
Balance");
}
// Comment out the next line
to see the effect of leaving
out
// the lock
keyword:
lock(thisLock)
{
if (balance >= amount)
{
Console.WriteLine("Balance before Withdrawal : "
+ balance);
Console.WriteLine("Amount to Withdraw
: -" +
amount);
balance =
balance - amount;
Console.WriteLine("Balance after Withdrawal :
" + balance);
return
amount;
}
else
{
return 0;
// transaction rejected
}
}
}
public
void DoTransactions()
{
for (int i = 0; i < 100;
i++)
{
Withdraw(r.Next(1, 100));
}
}
}
class Test
{
static
void Main()
{
Thread[] threads = new
Thread[10];
Account acc = new
Account(1000);
for (int i = 0; i < 10;
i++)
{
Thread t = new Thread(new
ThreadStart(acc.DoTransactions));
threads[i] = t;
}
for (int i = 0; i < 10;
i++)
{
threads[i].Start();
}
}
}
Monitor类锁定对象:
MSDN:http://msdn.microsoft.com/zh-cn/library/system.threading.monitor(v=vs.100).aspx
Monitor常用的方法:
Enter() 锁定对象
Exit() 释放对象
Pulse() 通知等待队列中的线程锁定对象状态的改变
Wait() 释放对象上的锁并组织当前线程,直到它重新获取该锁
注1:当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。
注2:lock 关键字在块的开始处调用 Enter,而在块的结尾处调用 Exit。(这就是lock与Enter及Exit的关系)
当多线程公用一个对象时,应使用Monitor类,其提供了使线程共享资源的方案。Monitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。保证了一个时刻只有一个线程可以访问这个对象。Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有办法都是静态的,不能使用对象来引用。下面的代码说明了使用Monitor锁定一个对象的情形:
。。。
Queue oQueue=new Queue();
....
Monitor.Enter(oQueue);
....//现在oQueue对象只能被当前线程操纵了
Monitor.Exit(oQueue);//释放锁
其他线程只有等待它使用Monitor.Exit()方法释放锁后才可以使用。为了保证线程最终都能释放锁,可以把Monitor.Exit()方法写在try-catch-finally结构的finally代码块里。
对于任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息:现在持有锁的线程的引用;预备队列,队列中保存了已经准备好获取锁的线程;等待队列,队列中保存着当前正在等待这个对象状态改变的线程的引用。
当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。
下面是一个展示如何使用lock关键字和Monitor类来实现线程的同步和通讯的例子,也是一个典型的生产者与消费者问题。这个历程中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示:
namespace Ex2
{
public class Cell
{
int cellContents;
//Cell对象中的内容
bool readerFlag = false;
//a状态标志,true可以读取,为false时则正在写入
public int ReadFromCell()
{
lock
(this) //lock关键字保证其他线程不能够访问该程序块
{
if (!readerFlag)
{
try
{
//等待WriteToCell方法中调用Monitor.Pulse()方法
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Consume:
{0}", cellContents);
readerFlag = false;
//重置readerFlag标志,表示消费行为已经完成
Monitor.Pulse(this);
//通知WriteToCell()方法,(该方法在另外一个线程中执行,等待中)
}
return
cellContents;
}
public void WriteToCell(int n)
{
lock
(this)
{
if (readerFlag)
{
try
{
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
//当同步方法(指Monitor类除Enter之外的方法)在非同步的代码区被调用
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
//当线程在等待状态的时候中止
Console.WriteLine(e);
}
}
cellContents = n;
Console.WriteLine("Produce:
{0}", cellContents);
readerFlag = true;
Monitor.Pulse(this);
//通知另外一个线程中正在等待的ReadFromCell()方法
}
}
}
public class
CellProd
{
Cell cell;
//被操作的Cell对象
int quantity = 1;
//生产者次数,初始化为1
public CellProd(Cell box, int request)
{
//构造函数
cell =
box;
quantity =
request;
}
public void ThreadRun()
{
for (int
looper = 1; looper <= quantity; looper++)
cell.WriteToCell(looper);
//生产者向操作对象写入信息
}
}
public class
CellCons
{
Cell cell;
int quantit
af95
y = 1;
public CellCons(Cell box, int request)
{
cell =
box;
quantity =
request;
}
public void ThreadRun()
{
int
valReturned;
for (int
looper = 1; looper <= quantity; looper++)
valReturned =
cell.ReadFromCell(); //消费者从操作对象中读取信息
}
}
class program
{
public static void Main(String[] args)
{
int result
= 0;
//一个标志位,如果是0表示程序没有出错,如果是1表明有错误发生
Cell cell
= new Cell();
//下面使用cell初始化CellProd和CellCons两个类,生产和消费次数均为20次
CellProd
prod = new CellProd(cell, 20);
CellCons
cons = new CellCons(cell, 20);
Thread
producer = new Thread(new ThreadStart(prod.ThreadRun));
Thread
consumer = new Thread(new ThreadStart(cons.ThreadRun));
//生产者线程和消费者线程都已经被创建,但是没有开始执行
try
{
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();
Console.ReadKey();
}
catch
(ThreadStateException e)
{
//当线程因为所处状态的原因而不能执行被请求的操作
Console.WriteLine(e);
result = 1;
}
catch
(ThreadInterruptedException e)
{
//当线程在等待状态的时候中止
Console.WriteLine(e);
result = 1;
}
//尽管Main()函数没有返回值,但下面这条语句可以向父进程返回执行结果
Environment.ExitCode = result;
}
}
}
在上面的例程中,同步是等待Monitor.Pulse()来完成的。首先生产者生产了一个值,而同一时刻消费者处于等待状态,直到收到生产者的“脉冲(Pulse)”通知它生产已经完成,此后消费者进入消费状态,而生产者开始等待消费者完成操作后将调用Monitor.Pulse()发送的“脉冲”。
程序执行过程:线程通过 lock(this)
即Enter方法获得对象cell的锁(或者等待获得),首先判断数据是否存在,即readFlag的值。
如果为false,即没有数据可以读取,producer线程执行写数据操作,即 cellContents =
n;Console.WriteLine("Produce: {0}", cellContents);
同时设置readerFlag为真,并通知等待队列中的consumer线程。readerFlag =
true; Monitor.Pulse(this);
consumer线程则执行Wait(),等待producer线程执行完后,将锁再次归还consumer线程。
如果为true,即有数据可以读取,producer线程执行Wait(),等待consumer线程执行完后,将锁再次归还producer线程;consumer线程执行读数据操作,即Console.WriteLine("Consume:
{0}", cellContents);
同时设置readerFlag为假,并通知等待队列中的producer线程。readerFlag =
false; Monitor.Pulse(this);
通过System.Threading命名空间中提供的Thread类来创建和控制线程,ThreadPool类用于管理线程池等;
Thread类有几个至关重要的方法,描述如下:
Start(); 启动线程
Sleep(int); 静态方法,暂停当前线程制定的毫秒数
Abort(); 通常使用该方法来终止一个线程
Join(); 等待直到线程结束
Suspend(); 该方法并不终止未完成的线程,它仅仅挂起线程,以后还可以恢复
Resume(); 恢复被Suspend()方法挂起的线程执行。
(二)如何操纵一个线程
使用Thread类创建线程时,只需要提供线程入口即可。
在C#中,线程入口是通过ThreadStart代理(delegate)来提供的,当调用Thread.Start()方法后,线程就开始执行ThreadStart所指向的函数。
Thread oThread = new Thread(new
ThreadStart(oAlpha.Beta));
oThread.Start();
while
(!oThread.IsAlive) ;
{
Thread.Sleep(2);
oThread.Abort();
oThread.Join();
Console.WriteLine();
Console.WriteLine("Alpha.Beta
has finished");
try
{
Console.WriteLine("Try to restart the Alpha.Beta
thread");
oThread.Start();
}
catch
(ThreadStateException)
{
Console.WriteLine("ThreadStateException trying
to restart Alpha.Beta.");
Console.WriteLine("Exception since aborted
threads cannot be restarted.");
Console.ReadKey();
}
}
在创建线程oThread时,我们用指向oAlpha.Beta()方法初始化了ThreadStart代理(delegate)对象,当我们创建线程oThread调用oThread.Start()方法启动时,实际上程序运行的是oAlpha.Beta()方法。
在Main方法中的while循环中,使用静态方法Thread.Sleep()让主线程暂停一段时间,这段时间CPU转向执行线程oThread。然后我们使用Thread.Abort()方法终止了线程oThread,注意后面的oThread.Join(),Thread.Join()方法使主线程等待,直到oThread线程结束。也可以给Thread.Join()方法指定一个int型的参数作为最长的等待时间。
之后我们试图用Thread.Start方法重新启动线程oThread,但是,显然Abort()方法终止后的线程不可恢复,所以程序最后抛出ThreadStateException异常。
Thread.ThreadState 属性
这个属性代表了程序运行时的状态。
ThreadState属性的取值如下:
Aborted 线程已停止
AbortRequested 线程的Thread.Abort()方法已被调用,但是线程还未停止
Background 线程在后台执行,与属性Thread.IsBackground有关
Running 线程正在正常运行
Stopped 线程已经被停止
StopRequested 线程正在被要求停止
Suspended 线程已经被挂起(在此状态下,可以通过Resume()方法重新运行)
SuspendRequest 线程正在要求被挂起,但是还未来得及相应
Unstarted 未调用Thread.Start()开始线程的运行
WaitSleepJoin 线程因为调用了Wait(),Sleep()或Join()等方法处于封锁状态。
注:后台线程与前台线程只有一个区别,那就是后台线程不妨碍程序的终止。一旦一个进程所有的前台线程都终止后,CLR(通用语言运行环境)将调用任意一个存活中的后台进程的Abort()方法来彻底终止进程。
线程的优先级
当线程之间争夺CPU时间时,CPU是按照线程的优先级给予服务的。在C#程序中,用户可以设定5个不同的优先级,由高到低分别是Highest,AboveNormal,Normal,BelowNormal,Lowest,在创建线程时如果不指定优先级,那么系统默认为ThreadPriority.Normal。
(三)生产者和消费者
每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。几个线程同时执行一个函数,会导致数据混乱,产生不可预料的结果。必须避免这种情况的发生。
C#提供了一个关键字lock,可以把一段代码定义为互斥段(critical
section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义入下:
lock(expression) statement_block
expression代表你希望跟踪的对象,通常是对象引用。如果你想保护一个类的实例,一般的,可以使用this;保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。
statement_block就是互斥代码段,这段代码在一个时刻内只可能被一个线程执行。
lock语句(C#)MSDN http://msdn.microsoft.com/zh-cn/library/c5kehkcz(v=vs.100).aspx
下例使用线程和 lock。只要 lock 语句存在,语句块就是临界区并且 balance 永远不会是负数。
using System;
using System.Threading;
class Account
{
private
Object thisLock = new Object();
int
balance;
Random r
= new Random();
public
Account(int initial)
{
balance =
initial;
}
int
Withdraw(int amount)
{
// This condition will never
be true unless the lock statement
// is commented
out:
if (balance <
0)
{
throw new Exception("Negative
Balance");
}
// Comment out the next line
to see the effect of leaving
out
// the lock
keyword:
lock(thisLock)
{
if (balance >= amount)
{
Console.WriteLine("Balance before Withdrawal : "
+ balance);
Console.WriteLine("Amount to Withdraw
: -" +
amount);
balance =
balance - amount;
Console.WriteLine("Balance after Withdrawal :
" + balance);
return
amount;
}
else
{
return 0;
// transaction rejected
}
}
}
public
void DoTransactions()
{
for (int i = 0; i < 100;
i++)
{
Withdraw(r.Next(1, 100));
}
}
}
class Test
{
static
void Main()
{
Thread[] threads = new
Thread[10];
Account acc = new
Account(1000);
for (int i = 0; i < 10;
i++)
{
Thread t = new Thread(new
ThreadStart(acc.DoTransactions));
threads[i] = t;
}
for (int i = 0; i < 10;
i++)
{
threads[i].Start();
}
}
}
Monitor类锁定对象:
MSDN:http://msdn.microsoft.com/zh-cn/library/system.threading.monitor(v=vs.100).aspx
Monitor常用的方法:
Enter() 锁定对象
Exit() 释放对象
Pulse() 通知等待队列中的线程锁定对象状态的改变
Wait() 释放对象上的锁并组织当前线程,直到它重新获取该锁
注1:当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。
注2:lock 关键字在块的开始处调用 Enter,而在块的结尾处调用 Exit。(这就是lock与Enter及Exit的关系)
当多线程公用一个对象时,应使用Monitor类,其提供了使线程共享资源的方案。Monitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。保证了一个时刻只有一个线程可以访问这个对象。Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有办法都是静态的,不能使用对象来引用。下面的代码说明了使用Monitor锁定一个对象的情形:
。。。
Queue oQueue=new Queue();
....
Monitor.Enter(oQueue);
....//现在oQueue对象只能被当前线程操纵了
Monitor.Exit(oQueue);//释放锁
其他线程只有等待它使用Monitor.Exit()方法释放锁后才可以使用。为了保证线程最终都能释放锁,可以把Monitor.Exit()方法写在try-catch-finally结构的finally代码块里。
对于任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息:现在持有锁的线程的引用;预备队列,队列中保存了已经准备好获取锁的线程;等待队列,队列中保存着当前正在等待这个对象状态改变的线程的引用。
当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。
下面是一个展示如何使用lock关键字和Monitor类来实现线程的同步和通讯的例子,也是一个典型的生产者与消费者问题。这个历程中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示:
namespace Ex2
{
public class Cell
{
int cellContents;
//Cell对象中的内容
bool readerFlag = false;
//a状态标志,true可以读取,为false时则正在写入
public int ReadFromCell()
{
lock
(this) //lock关键字保证其他线程不能够访问该程序块
{
if (!readerFlag)
{
try
{
//等待WriteToCell方法中调用Monitor.Pulse()方法
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Consume:
{0}", cellContents);
readerFlag = false;
//重置readerFlag标志,表示消费行为已经完成
Monitor.Pulse(this);
//通知WriteToCell()方法,(该方法在另外一个线程中执行,等待中)
}
return
cellContents;
}
public void WriteToCell(int n)
{
lock
(this)
{
if (readerFlag)
{
try
{
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
//当同步方法(指Monitor类除Enter之外的方法)在非同步的代码区被调用
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
//当线程在等待状态的时候中止
Console.WriteLine(e);
}
}
cellContents = n;
Console.WriteLine("Produce:
{0}", cellContents);
readerFlag = true;
Monitor.Pulse(this);
//通知另外一个线程中正在等待的ReadFromCell()方法
}
}
}
public class
CellProd
{
Cell cell;
//被操作的Cell对象
int quantity = 1;
//生产者次数,初始化为1
public CellProd(Cell box, int request)
{
//构造函数
cell =
box;
quantity =
request;
}
public void ThreadRun()
{
for (int
looper = 1; looper <= quantity; looper++)
cell.WriteToCell(looper);
//生产者向操作对象写入信息
}
}
public class
CellCons
{
Cell cell;
int quantit
af95
y = 1;
public CellCons(Cell box, int request)
{
cell =
box;
quantity =
request;
}
public void ThreadRun()
{
int
valReturned;
for (int
looper = 1; looper <= quantity; looper++)
valReturned =
cell.ReadFromCell(); //消费者从操作对象中读取信息
}
}
class program
{
public static void Main(String[] args)
{
int result
= 0;
//一个标志位,如果是0表示程序没有出错,如果是1表明有错误发生
Cell cell
= new Cell();
//下面使用cell初始化CellProd和CellCons两个类,生产和消费次数均为20次
CellProd
prod = new CellProd(cell, 20);
CellCons
cons = new CellCons(cell, 20);
Thread
producer = new Thread(new ThreadStart(prod.ThreadRun));
Thread
consumer = new Thread(new ThreadStart(cons.ThreadRun));
//生产者线程和消费者线程都已经被创建,但是没有开始执行
try
{
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();
Console.ReadKey();
}
catch
(ThreadStateException e)
{
//当线程因为所处状态的原因而不能执行被请求的操作
Console.WriteLine(e);
result = 1;
}
catch
(ThreadInterruptedException e)
{
//当线程在等待状态的时候中止
Console.WriteLine(e);
result = 1;
}
//尽管Main()函数没有返回值,但下面这条语句可以向父进程返回执行结果
Environment.ExitCode = result;
}
}
}
在上面的例程中,同步是等待Monitor.Pulse()来完成的。首先生产者生产了一个值,而同一时刻消费者处于等待状态,直到收到生产者的“脉冲(Pulse)”通知它生产已经完成,此后消费者进入消费状态,而生产者开始等待消费者完成操作后将调用Monitor.Pulse()发送的“脉冲”。
程序执行过程:线程通过 lock(this)
即Enter方法获得对象cell的锁(或者等待获得),首先判断数据是否存在,即readFlag的值。
如果为false,即没有数据可以读取,producer线程执行写数据操作,即 cellContents =
n;Console.WriteLine("Produce: {0}", cellContents);
同时设置readerFlag为真,并通知等待队列中的consumer线程。readerFlag =
true; Monitor.Pulse(this);
consumer线程则执行Wait(),等待producer线程执行完后,将锁再次归还consumer线程。
如果为true,即有数据可以读取,producer线程执行Wait(),等待consumer线程执行完后,将锁再次归还producer线程;consumer线程执行读数据操作,即Console.WriteLine("Consume:
{0}", cellContents);
同时设置readerFlag为假,并通知等待队列中的producer线程。readerFlag =
false; Monitor.Pulse(this);
相关文章推荐
- C# 图形图像
- C# 绘制曲线(一)
- 深入C#内存管理来分析值类型&…
- C#异常处理
- C#定义类
- 谈C#中的Delegate
- Head First C# 实验室 赛狗日
- C#属性升级版--自动属性-chapter 3 P34-36
- C#的async和await
- 【整理】c# 调用windows API(user32.dll)
- C# 窗口优化扩展
- C# 获取系统时间及时间格式
- Leetcode-201- Bitwise AND of Numbers Range
- C#中双问号(??)语法
- C#配置EmguCV2.2.0
- C# 5.0 与 .Net 4.5 学习(三)Interlocked
- C#学习之 调用 AForge.NET框架启动摄像头抓拍
- C#反射机制
- C#学习之 调用 AForge.NET Framework 启动摄像头
- C#判断大小端并转换int数据