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、线程亲和性

  定义:由线程获取的任何锁都必须在同一个线程释放

  在特定线程内使用的任何资源通常都具有线程亲和性,这意味着只有该线程本身才能访问这些资源。

 

  下面例子可连接:

image

 

   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、死锁:

 

posted @ 2026-01-17 12:11  nonAny  阅读(2)  评论(0)    收藏  举报