(七)线程

线程

创建线程

//无参数的委托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个

红包金额最低5元

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

抵扣说明:

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

余额充值