线程
创建线程
//无参数的委托ThreadStart
static void Main(string[] args)
{
Thread t = new Thread(PrintNumer);
t.Start();
PrintNumer();
Console.ReadKey();
}
static void PrintNumer()
{
Console.WriteLine();
Console.WriteLine("printing...");
for (int i = 0; i < 50; i++)
{
Console.Write(i);
Console.Write(" ");
}
}
//有参数的委托ParameterizedThreadStart,参数必须是object类型
static void Main(string[] args)
{
Thread t = new Thread(PrintNumer);
t.Start(50);
PrintNumer(50);
Console.ReadKey();
}
static void PrintNumer(object max)
{
Console.WriteLine();
Console.WriteLine("printing...");
for (int i = 0; i < (int)max; i++)
{
Console.Write(i);
Console.Write(" ");
}
}
暂停线程
线程休眠时会尽可能少的占用CPU时间
static void Main(string[] args)
{
Thread t = new Thread(PrintNumer);
t.Start();
PrintNumer();
Console.ReadKey();
}
static void PrintNumer()
{
Console.WriteLine();
Console.WriteLine("printing...");
for (int i = 0; i < 50; i++)
{
Console.Write(i);
Console.Write(" ");
//每打印一次,休眠一秒
Thread.Sleep(TimeSpan.FromSeconds(1.0));
}
}
线程等待
运用Join方法,使得当前线程阻塞,等待Join的线程执行完毕,该方法具有两个重载,可以指定阻塞等待的时间。
下面的程序运行时,当掉用完Join时,主线程会阻塞,直到t线程执行完毕。
static void Main(string[] args)
{
Thread t = new Thread(PrintNumer);
t.Start();
t.Join();
PrintNumer();
Console.ReadKey();
}
static void PrintNumer()
{
Console.WriteLine();
Console.WriteLine("printing...");
for (int i = 0; i < 50; i++)
{
Console.Write(i);
Console.Write(" ");
}
}
线程终止
可以调用线程的Abort()方法终结线程,在被终结的线程中抛出ThreadAbortException,异常可能在任何时间被抛出,导致应用程序无法及时处理尚未处理完的事务。
另外被终结的线程可以捕获这个异常并调用Thread.ResetAbort()方法,这样要被终结的线程就会拒绝被终止。
不推荐使用Abort终结线程,可以提供一个CancellationToken方法取消线程
线程状态
线程有创建、运行、阻塞、终止、等待等状态,这些状态在一定条件下可以转化。
例如一般是 创建->运行->终止。
public enum ThreadState
{
Running = 0x0,
StopRequested = 0x1,
SuspendRequested = 0x2,
Background = 0x4,
Unstarted = 0x8,
Stopped = 0x10,
WaitSleepJoin = 0x20,
Suspended = 0x40,
AbortRequested = 0x80,
Aborted = 0x100
}
线程优先级
线程优先级影响线程占用的CPU时间,在多任务环境下,优先级越高的线程占用时间片越多。
Thread t = new Thread(Nothing);
t.Priority = ThreadPriority.Lowest;
static void Nothing(){ }
public enum ThreadPriority
{
Lowest,
BelowNormal,
Normal,
AboveNormal,
Highest
}
后台线程
新创建的线程默认是前台线程。
进程会等待所有前台线程完成后再结束,忽略后台线程的状态。
如果只剩后台线程,则进程会直接结束。
Thread t = new Thread(Nothing);
t.IsBackground = true;
static void Nothing(){ }
线程同步
名称解释
线程安全:一个类被多个线程同时访问时,该类表现的跟单个线程访问时相同,这时称该类线程安全。
当多个线程访问共享资源时,可能会出现访问冲突的情况,也叫作竞态条件、临界区。
线程同步是让多个线程按照正确的顺序访问共享资源。
死锁:当存在多个锁对象时,假设存在o1,o2 ;若A线程锁住o1,等待o2,B线程锁住o2,等待o1,两个线程互不相让,程序无法继续,那么这就构成了一个死锁。
原子操作:操作在一段量子时间(极短)里只能由一个线程完成,中间不能切换。
线程阻塞:
上下文切换:操作系统的线程调度器保存当前一组寄存器状态(当前线程),然后切换到另一组状态上去(另一个线程)
内核模式:操作系统运行模式,进入内核模式通过上下文切换将当前线程挂起,被挂起后不占用CPU时间,适合线程长时间等待的场景。
用户模式:操作系统运行模式,不需要将线程切换为阻塞状态,先简单等待,会占用CPU时间,该方式适用于线程等待时间短的场景。
混合模式:先采用用户模式等待一段时间,若线程未能获取资源则进入内核模式将线程挂起。
lock关键字
lock关键字锁定一个对象,只有持有改对象锁的线程才能进入lock块内部执行
//不加锁
class Program
{
public static int Count { get; private set; }
private static object sync_obj = new object();
static void Main(string[] args)
{
Thread t1 = new Thread(Add);
Thread t2 = new Thread(Add);
Thread t3 = new Thread(Add);
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
Console.WriteLine(Count);
Console.ReadKey();
}
public static void Add()
{
for(int i=0;i<1000;i++)
Count++;
}
public static void Sub()
{
for (int i = 0; i < 1000; i++)
Count--;
}
}
//加锁
class Program
{
public static int Count { get; private set; }
private static object sync_obj = new object();
static void Main(string[] args)
{
Thread t1 = new Thread(AddSync);
Thread t2 = new Thread(AddSync);
Thread t3 = new Thread(AddSync);
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
Console.WriteLine(Count);
Console.ReadKey();
}
public static void AddSync()
{
for (int i = 0; i < 1000; i++)
lock (sync_obj)
{
Count++;
}
}
public static void SubSync()
{
for (int i = 0; i < 1000; i++)
lock (sync_obj)
{
Count--;
}
}
}
Monitor类
lock关键字有可能造成死锁问题,使用Monitor.TryEnter()方法可以指定等待锁的时间,如果超时则不获取锁,从而避免死锁。
lock关键字其实是Monitor的语法糖。
//使用Monitor的标准写法,如果加上锁一定要在finally块里释放
bool hasLock = false;
try
{
Monitor.TryEnter(sync_obj, TimeSpan.FromSeconds(3.0),ref hasLock);
//do something
}
finally
{
if (hasLock)
Monitor.Exit(sync_obj);
}
SpinLock
Monitor的轻量级版本,适用于大量锁定且锁定时间非常短的场景,只需要使用用户模式等待,其方法使用和Monitor类似。
Interlocked
原子操作类,支持int和long类型的自增,自减,对象的比较并交换(CAS)操作,可以保持在多线程环境的安全性。
class Program
{
private static int _count = 0;
static void Main(string[] args)
{
Thread t1 = new Thread(AddSync);
Thread t2 = new Thread(AddSync);
Thread t3 = new Thread(AddSync);
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
Console.WriteLine(_count);
Console.ReadKey();
}
public static void AddSync()
{
for (int i = 0; i < 1000; i++)
Interlocked.Increment(ref _count);
}
}
Mutex
互斥量是全局的操作系统对象,其名字唯一;不使用时需要正确关闭互斥量,可使用using语句。
下面的示例如果启动2个实例,则第二个实例拿不到互斥量。
互斥量经常用于一个应用程序只允许启动一个实例。
const string Mutex_Name = "MutexTest";
static void Main(string[] args)
{
//initiallyOwned:false表示如果互斥量已被创建,允许其他程序获取
using(var m=new Mutex(initiallyOwned: false,name: Mutex_Name))
{
//timeout代表在指定的时间内能否获取互斥量
if(!m.WaitOne(timeout: TimeSpan.FromSeconds(3.0),exitContext: false))
{
Console.WriteLine("第二个实例启动");
}
else
{
Console.WriteLine("运行中...");
Thread.Sleep(TimeSpan.FromSeconds(10.0));
m.ReleaseMutex();
}
}
Console.ReadKey();
}
SemaphoreSlim
信号量的轻量级版本,使用混合模式等待,构造函数可以指定并发的线程个数。
Semaphore是较老的版本,它使用内核时间等待,切换上下文消耗较多资源,可以跨进程使用。
static SemaphoreSlim slim = new SemaphoreSlim(5);
static void Main(string[] args)
{
for(int i=1;i<=10;i++)
{
string t_name = "t" + i;
var t = new Thread(() => Access(2+2*i,t_name));
t.Start();
}
Console.ReadKey();
}
static void Access(double seconds, string t_name)
{
slim.Wait();
Console.WriteLine($"{t_name}开始访问");
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine($"{t_name}访问完成");
slim.Release();
}
AutoResetEvent
使用此类可以从一个线程向另一个线程发送通知。
AutoResetEvent构造函数参数为false代表初始状态为unsignaled,调用WaitOne方法会被主色,直到调用了Set方法。如果为true,初始状态为signaled,第一次调用WaitOne则会立即处理,然后状态变成unsignaled。
static AutoResetEvent mainEvent = new AutoResetEvent(false);
static AutoResetEvent subEvent = new AutoResetEvent(false);
static void Main(string[] args)
{
//主、子任务交替执行,子->主->子->主
var t = new Thread(()=>SubTask(10));
t.Start();
subEvent.WaitOne();
Console.WriteLine("主任务开始执行");
Thread.Sleep(TimeSpan.FromSeconds(5.0));
Console.WriteLine("主任务执行完成");
mainEvent.Set();
subEvent.WaitOne();
Console.WriteLine("主任务第二次执行");
Thread.Sleep(TimeSpan.FromSeconds(5.0));
Console.WriteLine("主任务执行完成");
Console.ReadKey();
}
static void SubTask(int seconds)
{
Console.WriteLine("子任务第一次执行");
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("子任务第一次执行完成");
subEvent.Set();
Console.WriteLine("等待主任务执行完成");
mainEvent.WaitOne();
Console.WriteLine("子任务第二次执行");
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("子任务第二次执行完成");
subEvent.Set();
}
ManualResetEventSlim
ManualResetEventSlim和AutoResetEvent类似,区别是调用了一次Set方法后,其他线程调用Wait方法可以不用等待,直到调用Reset方法重置状态。
下面示例中由于t3线程睡眠时间较长,没有在大门第一次打开时进入大门,只能等待第二次大门打开,t1、t2由于睡眠时间短,则当大门打开后(调用Set)一起进入大门。
static ManualResetEventSlim slimEvent = new ManualResetEventSlim(false);
static void Main(string[] args)
{
var t1 = new Thread(() => In("t1", 5));
var t2 = new Thread(() => In("t2", 6));
var t3 = new Thread(() => In("t3", 12));
t1.Start();
t2.Start();
t3.Start();
Thread.Sleep(TimeSpan.FromSeconds(6));
Console.WriteLine("大门第一次打开");
slimEvent.Set();
Thread.Sleep(TimeSpan.FromSeconds(2));
slimEvent.Reset();
Console.WriteLine("大门关闭");
Thread.Sleep(TimeSpan.FromSeconds(10));
Console.WriteLine("大门第二次打开");
slimEvent.Set();
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("大门关闭");
slimEvent.Reset();
Console.ReadKey();
}
static void In(string t_name,int seconds)
{
Console.WriteLine("{0}睡眠", t_name);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("{0}等待进入大门",t_name);
slimEvent.Wait();
Console.WriteLine("{0}进入大门",t_name);
}
CountDownEvent
这个类可以等待一批线程完成都某些操作再继续往下执行。
类似java的CountDownLatch。
static CountdownEvent countdown = new CountdownEvent(3);
static void Main(string[] args)
{
Console.WriteLine("多线程保存数据");
var t1 = new Thread(()=>SaveData("t1",4));
var t2 = new Thread(()=>SaveData("t2",6));
var t3 = new Thread(()=>SaveData("t3",8));
t1.Start();
t2.Start();
t3.Start();
countdown.Wait();
Console.WriteLine("全部保存完成");
countdown.Dispose();
Console.ReadKey();
}
static void SaveData(string t_name,int seconds)
{
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("{0}保存数据完成..", t_name);
countdown.Signal();
}
Barrier
这个类可以使一批线程在某个时刻碰面(都到同一个时间点再往下执行)。
构造函数可以传递参与线程的数量和 每个阶段完成后回调的方法。
阶段:当一个线程调用SignalAndWait方法时触发一个阶段,当所有线程都到达这个阶段时阶段完成。
static Barrier barrier = new Barrier(2,
(b) => Console.WriteLine("end of phase {0}", b.CurrentPhaseNumber + 1));
static void Main(string[] args)
{
var t1 = new Thread(() => PlayMusic("the guitarist", "play an amazing solo", 5));
var t2 = new Thread(() => PlayMusic("the singer", "sing his song", 2));
t1.Start();
t2.Start();
Console.ReadKey();
}
static void PlayMusic(string name,string msg,int seconds)
{
for(int i=1;i<3;i++)
{
Console.WriteLine("--------------------------------------");
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("{0} starts to {1}", name, msg);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("{0} finishes to {1}", name, msg);
barrier.SignalAndWait();
}
}
ReaderWriterLockSlim
读写锁,此类代表一个管理资源访问的锁。
两种锁:读锁允许多线程读取数据,写锁在被释放前会阻塞了其他线程的所有操作。获取读锁时还有一个有意思的场景,即从集合中读取数据时,根据当前数据而决定是否获取一个写锁并修改该集合。一旦得到写锁,会阻止阅读者读取数据,从而浪费大量的时间,因此获取写锁后集合会处于阻塞状态。为了最小化阻塞浪费的时间,可以使用EnterUpgradeableReadLock和 ExitUpgradeableReadLock方法。先获取读锁后读取数据。如果发现必须修改底层集合,只需使用EnterWriteLock方法升级锁,然后快速执行一次写操作,最后使用ExitWriteLock释放写锁。
static ReaderWriterLockSlim rwls = new ReaderWriterLockSlim();
static Dictionary<int, int> item = new Dictionary<int, int>();
static void Main(string[] args)
{
new Thread(Read) { IsBackground = true }.Start();
new Thread(Read) { IsBackground = true }.Start();
new Thread(Read) { IsBackground = true }.Start();
new Thread(()=>Write("t1")) { IsBackground = true }.Start();
new Thread(()=>Write("t2")) { IsBackground = true }.Start();
Thread.Sleep(TimeSpan.FromSeconds(30));
Console.ReadKey();
}
static void Read()
{
Console.WriteLine("读取字典");
while(true)
{
try
{
rwls.EnterReadLock();
foreach(var key in item.Keys)
{
Thread.Sleep(TimeSpan.FromSeconds(0.1));
Console.Write(item[key]);
Console.Write(" ");
}
Console.WriteLine();
}
finally
{
rwls.ExitReadLock();
}
}
}
static void Write(string t_name)
{
while(true)
{
try
{
int newKey = new Random().Next(250);
rwls.EnterUpgradeableReadLock();
if(!item.ContainsKey(newKey))
{
try
{
rwls.EnterWriteLock();
item[newKey] = newKey;
Console.WriteLine("新的键值 {0} 被 {1} 添加到字典中",newKey,t_name);
}
finally
{
rwls.ExitWriteLock();
}
}
Thread.Sleep(TimeSpan.FromSeconds(0.1));
}
finally
{
rwls.ExitUpgradeableReadLock();
}
}
}
SpinWait
采用混合模式等待,先使用用户模式等待一段时间,在使用内核模式将线程挂起以节省CPU时间。
使用volatile关键字修饰变量表示 变量修改后对每个线程可见,确保每个线程获取的总是该变量最新值。
static volatile bool isComplete = false;
static void Main(string[] args)
{
var t1 = new Thread(UserWait);
var t2 = new Thread(SpinWait);
Console.WriteLine("用户模式等待");
t1.Start();
Thread.Sleep(20);
isComplete = true;
Thread.Sleep(1000);
isComplete = false;
Console.WriteLine("自旋等待");
t2.Start();
Thread.Sleep(5);
isComplete = true;
Console.ReadKey();
}
static void UserWait()
{
while(!isComplete)
{
Console.Write(".");
}
Console.WriteLine();
Console.WriteLine("等待完成");
}
static void SpinWait()
{
var sw = new SpinWait();
while(!isComplete)
{
sw.SpinOnce();
Console.WriteLine(sw.NextSpinWillYield);
}
Console.WriteLine("等待完成");
}
10万+

被折叠的 条评论
为什么被折叠?



