C#多线程编程02-线程同步
1、解决的问题:当使用多个线程执行任务时,可能存在这些线程需要共享一些公共资源可能会导致冲突
以下代码则是说明这一点:
int counter = 0; Thread thread1 = new Thread(IncrementCounter); Thread thread2 = new Thread(IncrementCounter);
thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); Console.WriteLine($"counter:{counter}"); void IncrementCounter() { for (int i = 0;i<10000;i++) { counter = counter+1; } }
2、线程访问的共享资源的程序片段,确保同一时间只有一个线程或进程可以访问该资源;我们称为临界区,
3、原子操作
临界区内的代码作为不可分割的一部分(作为原子的)运行,则不会出现如何竞争问题了
4、互斥锁
基本语法: Lock{ ...锁主体... } 锁主体通常为临界区
作用:当使用这种方式应用锁时,锁主体只能由一个线程执行
当一个线程持有锁时,其余线程无法访问锁主体内的代码
本质:将锁主体内的操作变为原子操作
互斥锁内部存在一套try...catch...finally机制,在finally内,锁总会被释放
int counter = 0; object counterLock = new object(); Thread thread1 = new Thread(IncrementCounter); Thread thread2 = new Thread(IncrementCounter); thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); Console.WriteLine($"counter:{counter}"); void IncrementCounter() { for (int i = 0;i<10000;i++) { //该代码同一时刻只能由一个线程执行 lock (counterLock) { counter = counter + 1; } } }
5、监视器
对临界区的一种监控手段,若监视器来监控临界区,当一个线程进入临界区时其它生产必须被阻塞,必须等待
监视器实际上会生成一个互斥锁(锁是基于监视器的,是监视器的一种简单版本)
监视器区别于锁:可以设置超时时间 TryEnter()方法的返回值表示当前线程是否获得锁
bool result = Monitor.TryEnter(counterLock,2000);
语法:
object lockObject = new Object(); Monitor.Enter(lockObject);
//Monitor.TryEnter(counterLock);
try { //code } finally { Monitor.Exit(lockObject); }
若Monitor成功进入临界区,它实际会生成一个互斥锁,随后没有一个线程能进入临界区。随后monitor.Exit()在临界区结束后被调用,以释放该锁。
int counter = 0; object counterLock = new object(); Thread thread1 = new Thread(IncrementCounter); Thread thread2 = new Thread(IncrementCounter); thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); Console.WriteLine($"counter:{counter}"); void IncrementCounter() { for (int i = 0; i < 10000; i++) { //该代码同一时刻只能由一个线程执行 Monitor.Enter(counterLock); try { counter = counter + 1; } finally { Monitor.Exit(counterLock); } } }
6、互斥量Mutex在进程间的作用
互斥量不仅用于一个进程当中,还可以跨进程使用
以下代码用于本地互斥量
using (var mutex = new Mutex()) { mutex.WaitOne();//获得Mutex的所有权 try { //获得所有权后,在此执行临界区代码 } finally { mutex.ReleaseMutex(); } }
当为互斥量命名后,该互斥量可用于跨进程使用
using (var mutex = new Mutex(false,"Gobal")) { mutex.WaitOne();//获得Mutex的所有权 try { //获得所有权后,在此执行临界区代码 } finally { mutex.ReleaseMutex(); } }
若仅为同步进程内的线程,考虑用锁或者监视器,同步进程间的使用Mutex
string filepath = "counter.txt"; using (var mutex = new Mutex(false, $"GlobalFileMutex:{filepath}"))//mutex一旦被命名,则为全局互斥锁 { for (int i = 0; i < 10000; i++) { mutex.WaitOne(); try { int counter = ReadCounter(filepath); counter++; WriteCounter(filepath, counter); } finally { mutex.ReleaseMutex(); } } } Console.WriteLine("Done"); Console.ReadLine(); int ReadCounter(string filepath) { using (var stream = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.Read,FileShare.ReadWrite)) using(var reader = new StreamReader(stream)) { string counter = reader.ReadToEnd(); return string.IsNullOrEmpty(counter)?0:int.Parse(counter); } } void WriteCounter(string filepath, int counter) { using (var stream = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) using (var writer = new StreamWriter(stream)) { writer.Write(counter); } }Console.ReadKey();
7、读写锁:允许读取者同时读取,允许写入者获得共享资源的独占访问
读取可允许多个线程访问读取,但是写入时读写锁表现为互斥锁。
//读写锁:允许读取者同时读取,允许写入者获得共享资源的独占访问 public class GlobalConfigurationCache { //定义读写锁 private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); private Dictionary<int,string> _cache = new Dictionary<int,string>(); public void Add(int key, string value) { bool lockAcquire = false; try { lockAcquire = true; //进入写入锁 _lock.EnterWriteLock(); _cache[key] = value; } finally { if (lockAcquire) { //退出写入锁 _lock.ExitWriteLock(); } } } public string? Get(int key) { bool lockAcquire = false; try { lockAcquire = true; //进入读取锁 _lock.EnterReadLock(); return _cache.TryGetValue(key, out var value) ? value : null; } finally { if(lockAcquire) //退出读取锁 _lock.ExitReadLock(); } } }
8、信号量Semaphore
信号量是一种信号同步机制,但是较少用于保护临界区.其多用于控制并发线程或进程的数量。
信号量等待和信号量释放不必在同一个线程当中
SemaphoreSlim比Semaphore轻量,若仅在线程当中控制线程数量使用SemaphoreSlim,在进程中使用选择后者
SemaphoreSlim语法:
using SemaphoreSlim semaphore = new SemaphoreSlim(initialCount:1,maxCount:3); semaphore.Wait(); try { } finally { semaphore.Release(); }
Queue<string> requestQueue = new Queue<string?>(); using SemaphoreSlim semaphore = new SemaphoreSlim(initialCount:3,maxCount:3); Thread thread = new Thread(() => MonitorQueue()); thread.Start(); //Main Thread:Enqueue the request Console.WriteLine("Server is runing.Type'B' Tricket ,Type 'C' Cancel, Type 'Exit' to stop."); while (true) { string? input = Console.ReadLine(); if (input == null) { break; } requestQueue.Enqueue(input); } //subThread:Monitor Thread void MonitorQueue() { while (true) { if (requestQueue.Count != 0) { string? input = requestQueue.Dequeue(); semaphore.Wait(); Thread processThread = new Thread(() => ProcessBook(input)); processThread.Start(); } Thread.Sleep(100); } } void ProcessBook(string? input) { try { Thread.Sleep(1000); Console.WriteLine($"Process input {input}"); }finally { var previous = semaphore.Release(); Console.WriteLine($"Thead ID:{Thread.CurrentThread.ManagedThreadId},Previous count is {previous}"); } }
9、自动重置事件
用于线程交互:发送信号,在不同线程之间传递信号.而不是用于保护临界区、
自动重置事件一次只能生成一个信号
using AutoResetEvent autoResetEvent = new AutoResetEvent(false); //若初始有产品供消费者消费为true,反之为false //消费者,消费后自动关闭信号 autoResetEvent.WaitOne(); //生产者,开启信号 autoResetEvent.Set();
using AutoResetEvent autoResetEvent = new AutoResetEvent(false); //若初始有产品供消费者消费为true,反之为false string? userInput = null; Console.WriteLine("Server is running.Type 'go' to process!"); for(int i = 0; i < 3; i++) { Thread thread = new Thread(Worker); thread.Name = $"Worker {i}"; thread.Start(); } //bug:并非所有信号均可以被消费,当信号被生产多个,而消费者来不及消费。 ////而一旦其中一个线程继续执行,信号就会被关闭 //主线程接受用户收入并发送信号 while(true) { userInput = Console.ReadLine()??""; //如果用户输入'go'发送信号 if(userInput.ToLower() == "go") { //生产者,开启信号 autoResetEvent.Set(); } } void Worker() { while(true) { Console.WriteLine($"{Thread.CurrentThread.Name} Worker thread is waiting for the signal"); //消费者,消费后自动关闭信号 autoResetEvent.WaitOne(); Console.WriteLine($"{Thread.CurrentThread.Name} Worker thread prcocess!"); Thread.Sleep(2000); } }
10、手动重置信号
手动重置信号一次可以消费多个信号
using ManualResetEventSlim manualResetEvent = new ManualResetEventSlim(false); manualResetEvent.Set(); //关闭手动重置事件 manualResetEvent.Reset();
using ManualResetEventSlim manualResetEvent = new ManualResetEventSlim(false); Console.WriteLine("Press enter relese all threads ..."); for (int i = 0; i <= 3; i++) { Thread thread = new Thread(Work); thread.Name = $"Thread {i}"; thread.Start(); } Console.ReadLine(); manualResetEvent.Set(); Console.ReadLine() ; void Work(object? obj) { Console.WriteLine($"{Thread.CurrentThread.Name} is wait for the signal"); manualResetEvent.Wait(); Thread.Sleep(1000); Console.WriteLine($"{Thread.CurrentThread.Name} has been relesed."); }
12、线程亲和性
定义:由线程获取的任何锁都必须在同一个线程释放
在特定线程内使用的任何资源通常都具有线程亲和性,这意味着只有该线程本身才能访问这些资源。
下面例子可连接:

public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { Thread thread = new Thread(() => { ShowMessage("first message", 3000); }); thread.Start(); } private void button2_Click(object sender, EventArgs e) { Thread thread = new Thread(() => { ShowMessage("second message", 5000); }); thread.Start(); } private void ShowMessage(string message, int delay) { Thread.Sleep(delay); //抛出异常:线程间操作无效: 从不是创建控件“lblMessage”的线程访问它。 //所在函数是一个独立的线程,而lblMessage.Text则是UI线程的资源 lblMessage.Text = message; } }
当需要从工作线程访问标签时,需要让这两个线程合二为一,即同步上下文
private void ShowMessage(string message, int delay) { Thread.Sleep(delay); if (lblMessage.InvokeRequired) { //同步上下文 lblMessage.Invoke(new Action(() => { lblMessage.Text = message; } )); } else { lblMessage.Text = message; } }
13、线程安全:在多线程计算机编程中,当一个函数、数据结构或类可以被多个线程并发使用而不会导致竞态条件、意外行为或数据损坏时,它就是线程安全的。
这意味着在该函数、数据结构或类内部,已经使用了适当的锁定机制。
14、死锁:
浙公网安备 33010602011771号