Smart Client学习笔记(7) 使用多线程创建高响应智能客户端应用程序

来源:互联网 发布:网络隐私权的内涵包括 编辑:程序博客网 时间:2024/06/05 17:39

这次课程讲解了如何进行多线程编程

多线程应用程序的特点:

• 具有同时有多条“执行线路”的能力
• 行为无法预知并且每次调试时行为不同
• 线程增加了程序的复杂性
• 多线程不但可以用于我们所关注的SmartClient当中,也可以用于任何应用程序当中

为什么要讨论多线程应用程序?

• 向用户提供及时响应
• 在运行时层面并行执行任务
• 获得更好的全面的应用程序性能
• 硬件资源得到全面利用

在winform开发中,如果不是用多线程进行开发,那么在算法计算时间比较长的情况下,会阻塞主线程,导致界面的停滞。

进程的概念:

• 进程是正在执行的程序的一个实例
• 进程只有在执行时才存在
• 进程拥有4GB地址空间以包含应用程序的代码和数据(windows 平台下)
• 每个进程至少有一个线程

线程的概念:

• 线程是“执行单元”,负责进程内所包含代码的执行
• 调度器识别并且只调度线程,以允许程序并行运行
• 线程使用处理器寄存器,并且当其它线程开始运行时栈和其它资源都会被保存起来
• 线程可以运行在不同的优先级上
• 一个进程能够拥有多个线程

使用多线程的注意要点:

• 只有当确实需要时才使用处理器:多线程编程会影响处理器的效率,只有当必须使用的情况下才使用。

• 不要使用轮询的方式来同步线程:轮询通过循环重复地检查异步调用的状态。轮询是效率最低的线程管理方法,因为它重复地检查各个线程属性的状态,因而浪费了大量资源。

• 使用同步对象:在多线程编程中,注意使用同步对象,来保证共享资源的原子性

• 多线程增加了应用程序的复杂性

• 托管对象构建在Windows基本线程模型之上
• System.Threading.Thread类封装了Windows线
程功能
• Thread类提供了如下功能:
–创建线程
–启动线程
–设置线程优先级
–挂起/ 恢复线程执行
–终止线程

在.net中启动线程非常简单,实例化一个Thread对象,并将一个委托作为参数传入,然后调用线程对象的Start的方法,就启动了一个线程。

如何中止一个线程:

• 当所有的线程都结束时,该应用程序才能够被终止

• 由Thread.Abort引发异常的方法,在通常环境中不是终止线程结束的最好方法:最好不要调用Abort方法来终止线程,推荐使用变量来表示线程是否终结,用Join方法来等待线程终结。Join方法是阻塞一个线程,直到这个线程终止才返回。而Abort方法则是系统悄悄的销毁了线程而且不通知用户,并且不能再次重新启动该线程,并将抛出ThreadAbortException异常。
• 在应用程序终止前,所有前台线程都必须要终止:如果我们在.net的winform中创建的线程是一个前台线程,我们关闭窗口并不表示这个应用程序被终止了,因为可能还有一个前台线程在运行中,我们可以设置IsBackground属性为true来设置线程是否为后台线程。
贴段代码示例

  1. private void button1_Click(object sender, EventArgs e)
  2.         {
  3.             button1.Enabled = false;
  4.             button2.Enabled = true;
  5.             button3.Enabled = true;
  6.             label1.Text = "Processing: Active";
  7.             label1.ForeColor = Color.Red;
  8.             label1.Update();
  9.             processingDone = false;
  10.             //启动线程
  11.             myWorkerThread = new Thread(new ThreadStart(ExecuteProcessing));
  12.             myWorkerThread.IsBackground = checkBox1.Checked;
  13.             myWorkerThread.Start();
  14.         }
  15.         private void button2_Click(object sender, EventArgs e)
  16.         {
  17.             //设置线程中循环的标识true,表示结束循环。
  18.             processingDone = true;
  19.             //等待线程的完成,因为线程不循环了但仍然要执行Thread.Sleep(5000);
  20.             //所以还要等待5000毫秒Join方法才能返回
  21.             myWorkerThread.Join();
  22.             button2.Enabled = false;  
  23.             button3.Enabled = false;
  24.             button1.Enabled = true;  
  25.             label1.Text = "Processing: Not Active";
  26.             label1.ForeColor = Color.Black;
  27.         }
  28.         private void button3_Click(object sender, EventArgs e)
  29.         {
  30.             //直接销毁线程
  31.             myWorkerThread.Abort();
  32.             //等待线程的完成,因为线程销毁了,所以很快就返回
  33.             myWorkerThread.Join();
  34.             button2.Enabled = false;
  35.             button3.Enabled = false;
  36.             button1.Enabled = true;
  37.             label1.Text = "Processing: Not Active";
  38.             label1.ForeColor = Color.Black;
  39.         }
  40.         private void ExecuteProcessing()
  41.         {
  42.             //try
  43.             //{
  44.                 while (!processingDone)
  45.                 {
  46.                     Thread.Sleep(100);
  47.                 }
  48.             //}
  49.             //finally
  50.             //{
  51.                 Thread.Sleep(5000);
  52.             //}
  53.         }

有3个Button,Button1启动线程,Button1启动线程,Button2将循环标识变量设为false来终止线程中的循环,并等待利用Join方法等待线程的返回,Button3利用Abort方法销毁线程。

线程中更新UI控件:

• 控件应该只被创建它们的线程更新:也就是说在.net的winform中,UI控件应该有主线程进行更新。

• 使用Control.Invoke来从其他线程委托更新控件
• 同步和异步Invoke方法均可用:同步是Invoke方法,异步是BeginInvoke方法。

• 支持参数传递
• 当从其它线程更新UI控件时,会抛出异常

下面贴代码

  1. public partial class Form1 : Form
  2.     {
  3.         private bool processingDone;
  4.         private Thread myWorkerThread;
  5.         public Form1()
  6.         {
  7.             InitializeComponent();
  8.         }
  9.         private void button2_Click(object sender, EventArgs e)
  10.         {
  11.             processingDone = true;
  12.             //等待线程结束
  13.             myWorkerThread.Join();
  14.             button2.Enabled = false;
  15.             button1.Enabled = true;
  16.         }
  17.         private void ExecuteProcessing()
  18.         {
  19.             //创建一个委托对象,并指定这个委托的函数地址
  20.             UpdateLabelDelegate labelUpdater = new UpdateLabelDelegate(UpdateLabel);
  21.             this.Invoke(labelUpdater, new object[]{"Processing: Active", Color.Red});
  22.             try
  23.             {
  24.                 while (!processingDone)
  25.                 {
  26.                     Thread.Sleep(1000);
  27.                     //调用同步的invoke方法
  28.                     //this.Invoke(labelUpdater, new object[] { "Processing: Still Active - " + DateTime.Now, Color.Green });
  29.                     //调用异步的Invoke方法
  30.                     this.BeginInvoke(labelUpdater, new object[] { "Processing: Still Active - " + DateTime.Now, Color.Green });
  31.                 }
  32.             }
  33.             finally
  34.             {
  35.                 Thread.Sleep(1000);
  36.               //  this.Invoke(labelUpdater, new object[] { "Processing: Done", Color.Black });
  37.                 this.BeginInvoke(labelUpdater, new object[] { "Processing: Done", Color.Black });
  38.             }
  39.         }
  40.         //定义一个委托,这个委托对应的函数是返回值为void,第一个参数为string类型,第二个参数为Color类型
  41.         delegate void UpdateLabelDelegate(string labelText, Color textColor);
  42.         private void UpdateLabel(string labelText, Color textColor)
  43.         {
  44.             //设置Label的文本和颜色
  45.             label1.Text = labelText;
  46.             label1.ForeColor = textColor;
  47.         }
  48.         private void button1_Click(object sender, EventArgs e)
  49.         {
  50.             button1.Enabled = false;
  51.             button2.Enabled = true;
  52.           
  53.             processingDone = false;
  54.             //创建线程
  55.             myWorkerThread = new Thread(new ThreadStart(ExecuteProcessing));
  56.             //启动线程
  57.             myWorkerThread.Start();
  58.         }
  59.     }

在上面的代码中我们可以看到,我们没有在myWorkerThread的线程中直接更新Label的文本和颜色,而是借用委托和Invoke方法来修改Label的颜色和文本。Invoke方法和BeginInvoke方法都可以调用,但是其中有细微的区别。Invoke方法调用之后,调用Invoke方法的线程将被阻塞,直到Invoke方法返回之后才继续执行,而BeginInvoke将不会阻塞调用的线程,线程将继续执行,如果要获得BeginInvoke的返回值可以利用EndInvoke方法来获得。BeginInvoke的返回值是一个IAsyncResult对象,这个对象包含了异步调用的状态等信息,EndInvoke的参数是IAsyncResult对象,返回值是object,EndInvoke将在IAsyncResult对象中检索返回值,并以返回值进行返回。

线程与线程池:

• 采用通常方式来创建线程代价非常昂贵。

• 对于生命周期相对短的线程使用线程池
• 线程池由公共语言运行时(CLR)应用,支持:
– Threading.Timer对象
– 异步操作
• ThreadPool性能
– 当在线程池中没有空闲线程可用时,其代价与CreateThread
相同
– 由于线程池线程可以被重用,因此当线程池中存在空闲线程
时创建线程所带来的开销非常小

线程池可以说是有平台管理的已经创建好的线程集合,可以由用户来管理线程池的大小。线程池的默认线程个数为25个。

• 线程池中存在的线程可以被重用
• 如果线程池中没有线程存在,则创建新的线程(当线程池内线城池小于25个线程时)
• 如果线程池中的线程空闲一段时间(通常是60秒),那么该线程将会从池中移除,释放其所占资源

  1.     public partial class Form1 : Form
  2.     {
  3.         //线程数量统计
  4.         private int threadCounter = 0;
  5.         //线程池中的数量统计
  6.         private int threadPoolCounter = 0;
  7.         //信号灯对象
  8.         private AutoResetEvent doneEvent;
  9.         public Form1()
  10.         {
  11.             InitializeComponent();
  12.         }
  13.         private void button1_Click(object sender, EventArgs e)
  14.         {
  15.             //设置当前鼠标形状为等待形状
  16.             Cursor.Current = Cursors.WaitCursor;
  17.             threadCounter = 0;
  18.             //初始化信号灯,并设为非终止状态
  19.             doneEvent = new AutoResetEvent(false);
  20.             label1.Text = "";
  21.             label2.Update();
  22.             //设置启动计算时间开始的毫秒数
  23.             int elapsedTime = Environment.TickCount;
  24.             //循环创建1000个线程,并启动线程
  25.             for (int i = 0; i < 1000; i++)
  26.             {
  27.                 Thread workerThread = new Thread(new ThreadStart(MyWorkerThread));
  28.                 workerThread.Start();
  29.             }
  30.             //等待信号灯的状态,主线程被阻塞
  31.             doneEvent.WaitOne();
  32.             //用当前毫秒数减去初始毫秒数得到创建1000个线程花费的时间
  33.             elapsedTime = Environment.TickCount - elapsedTime;
  34.             //将时间显示在Label上
  35.             label1.Text = "Creating threads: " + elapsedTime.ToString() + "mSec";
  36.             Cursor.Current = Cursors.Default;
  37.         }
  38.         private void MyWorkerThread()
  39.         {
  40.             //将threadCounter变量进行原子操作加1
  41.             Interlocked.Increment(ref threadCounter);
  42.             Thread.Sleep(0);
  43.             if(threadCounter == 1000)
  44.             {
  45.                 //如果线程数量等于1000,设置信号灯为终止状态
  46.                doneEvent.Set();
  47.             }
  48.         }
  49.         private void button2_Click(object sender, EventArgs e)
  50.         {
  51.             Cursor.Current = Cursors.WaitCursor;
  52.             threadPoolCounter = 0;
  53.             //设置信号灯为非终止转台
  54.             doneEvent = new AutoResetEvent(false);
  55.             label2.Text = "";
  56.             label2.Update();
  57.             //设置创建线程池线程的初始时间
  58.             int elapsedTime = Environment.TickCount;
  59.             for(int i = 0;i < 1000;i++)
  60.             {
  61.                //将方法排入线程池队列,也就是说利用线程池中的线程来执行MyWaitCallBack方法
  62.                ThreadPool.QueueUserWorkItem(new WaitCallback(MyWaitCallBack));
  63.             }
  64.             //等待信号灯的终止状态,主线程被阻塞
  65.             doneEvent.WaitOne();
  66.             //等到创建1000个线程池中线程的毫秒数
  67.             elapsedTime = Environment.TickCount - elapsedTime;
  68.             //更新Label
  69.             label2.Text = "Creating threads: " + elapsedTime.ToString() + "mSec";
  70.             Cursor.Current = Cursors.Default;
  71.         }
  72.         private void MyWaitCallBack(object stateInfo)
  73.         {
  74.             //原子量操作threadPoolCounter
  75.             Interlocked.Increment(ref threadPoolCounter);
  76.             Thread.Sleep(0);
  77.             if (threadPoolCounter == 1000)
  78.             {
  79.                 //设置型号灯为终止状态
  80.                 doneEvent.Set();
  81.             }
  82.         }
  83.     }

 

执行上面一段代码,我们可以观测到利用线程池来创建1000个线程比直接创建1000个线程时间上快的多。但是注意对于生命周期相对短的线程使用线程池,如果生命周期较长,不要使用线程池。

线程同步:

• 非同步线程的运行时间不可预知
• 保护数据,当多线程访问数据时避免出现不可知结果:确定数据访问的原子性,保证同一时刻只有一个线程在访问数据。
• 协调线程使线程在达到某个状态时另一个线程可以改变其状态

同步对象
• 线程通常来讲不能自治,需要开发者进行协调处理
• 我们可以使用下列同步对象:
– Interlocked
– AutoResetEvent
– ManualResetEvent
– Monitor
– Mutex

线程安全
• 多线程访问对象可能会引发问题
• 读取不一致数据
• 两个线程可能会同时更新数据,导致产生出错误的结果
• 使用同步对象来创建线程安全类
• 只有需要并发访问的类才需要线程安全

Interlocked的类我们在上面一段代码中已经贴出来了使用的一个示例,通过Increment来递增变量,Interlocked类有点直接类似lock,通过lock线程锁来控制数据的原子访问。

 

  1.     public partial class Form1 : Form
  2.     {
  3.         private string text;
  4.         private bool thread1Running;
  5.         private bool thread2Running;
  6.         
  7.         private AutoResetEvent m_Event = null;
  8.         
  9.         public Form1()
  10.         {
  11.             InitializeComponent();
  12.         }
  13.         private void button1_Click(object sender, EventArgs e)
  14.         {
  15.             //初始化信号灯为false,那么线程中调用WaitOne方法时,线程将会
  16.             //被阻塞
  17.             m_Event = new AutoResetEvent(false);
  18.             label1.Text = "";
  19.             text = "";
  20.             button1.Enabled = false;
  21.             
  22.             //创建两个线程,并启动他们
  23.             Thread workerThread1 = new Thread(Thread1Function);
  24.             Thread workerThread2 = new Thread(Thread2Function);
  25.             thread1Running = true;
  26.             thread2Running = true;
  27.             workerThread1.Start();
  28.             workerThread2.Start();
  29.             //设置信号灯为true,当某一个线程WaitOne收到信号时,将
  30.             //继续执行
  31.             m_Event.Set();
  32.         }
  33.         
  34.         private void Thread1Function()
  35.         {
  36.             //通过WaitOne阻塞线程,当收到信号时继续执行
  37.             if(m_Event.WaitOne())
  38.             {
  39.                 //设置信号灯为false,实际上不写这句代码其他线程的WaitOne
  40.                 //方法也不会接受到信号
  41.                 m_Event.Reset();
  42.                 for (int i = 0; i < 5; i++)
  43.                 {
  44.                     text += i.ToString();
  45.                     Thread.Sleep(0);
  46.                 }
  47.             }
  48.             
  49.             //执行完毕后调用Set方法,发出信号,通知另一个线程执行
  50.             m_Event.Set();
  51.             thread1Running = false;
  52.             this.Invoke(new EventHandler(WorkerThreadsFinished));
  53.         }
  54.         
  55.         private void Thread2Function()
  56.         {
  57.             if(m_Event.WaitOne())
  58.             {
  59.                 m_Event.Reset();
  60.                 for (int i = 0; i < 5; i++)
  61.                 {
  62.                     text += i.ToString();
  63.                     Thread.Sleep(0);
  64.                 }
  65.             }
  66.             
  67.             m_Event.Set();
  68.             thread2Running = false;
  69.             this.Invoke(new EventHandler(WorkerThreadsFinished));
  70.         }
  71.         
  72.         
  73.         public delegate void EventHandler();
  74.         private void WorkerThreadsFinished()
  75.         {
  76.             label1.Text = text;
  77.             button1.Enabled = true;
  78.         }
  79.     }

我们在上面这段代码可以看到,通过AutoResetEvent同步对象的示例方法,AutoRestEvent类似与一个信号灯,当信号灯亮的时候为true,熄灭的时候为false,WaitOne方法只有在信号灯为true的时候才不会阻塞线程,如果信号灯为false肯定会阻塞线程。但在编写上面这段代码的时候,没有调用Reset方法,其他线程的WaitOne方法也不会接受到信号,Set方法会由一个WaitOne方法捕获到,在捕获到之后马上就自动将将信号灯设置为false,但是ManualResetEvent不会自动将信号灯设置为false,那么表示ManualReset对象的Reset方法将会被多个WaitOne捕获到,除非手工调用Reset方法,在上面那段例子中,Reset将不能被省略。注意这两个对象的构造函数,如果参数是true的话,不用调用Set方法WaitOne将能捕获

  1.     public partial class Form1 : Form
  2.     {
  3.         private string text;
  4.         private bool thread1Running;
  5.         private bool thread2Running;
  6.         //锁对象
  7.         private object m_Lock = new object();
  8.         public Form1()
  9.         {
  10.             InitializeComponent();
  11.         }
  12.         private void button1_Click(object sender, EventArgs e)
  13.         {
  14.            
  15.             label1.Text = "";
  16.             text = "";
  17.             button1.Enabled = false;
  18.             
  19.             //创建两个线程,并启动他们
  20.             Thread workerThread1 = new Thread(Thread1Function);
  21.             Thread workerThread2 = new Thread(Thread2Function);
  22.             thread1Running = true;
  23.             thread2Running = true;
  24.             workerThread1.Start();
  25.             workerThread2.Start();
  26.         }
  27.         
  28.         private void Thread1Function()
  29.         {
  30.             //获取排它锁
  31.             Monitor.Enter(m_Lock);
  32.             for (int i = 0; i < 5; i++)
  33.             {
  34.                 text += i.ToString();
  35.                 Thread.Sleep(0);
  36.             }
  37.             
  38.             //释放排它锁
  39.             Monitor.Exit(m_Lock);
  40.             thread1Running = false;
  41.             this.Invoke(new EventHandler(WorkerThreadsFinished));
  42.         }
  43.         
  44.         private void Thread2Function()
  45.         {
  46.             Monitor.Enter(m_Lock);
  47.             for (int i = 0; i < 5; i++)
  48.             {
  49.                 text += i.ToString();
  50.                 Thread.Sleep(0);
  51.             }
  52.             Monitor.Exit(m_Lock);
  53.             thread2Running = false;
  54.             this.Invoke(new EventHandler(WorkerThreadsFinished));
  55.         }
  56.         
  57.         
  58.         public delegate void EventHandler();
  59.         private void WorkerThreadsFinished()
  60.         {
  61.             label1.Text = text;
  62.             button1.Enabled = true;
  63.         }
  64.     }

 

这是Monitor类锁的用法,Monitor类还可以进行线程中的通讯,主要用于生产者和消费者。下面是一段简单的生产者和消费者的代码

  1.     public partial class Form1 : Form
  2.     {
  3.         private string text;
  4.         //锁对象
  5.         private object m_Lock = new object();
  6.         //生产者消费者标识,架设true为生产,false为消费,
  7.         //初始值为生产
  8.         private bool m_Flag = true;
  9.         public Form1()
  10.         {
  11.             InitializeComponent();
  12.         }
  13.         private void button1_Click(object sender, EventArgs e)
  14.         {
  15.            
  16.             text = "";
  17.             button1.Enabled = false;
  18.             
  19.             //创建两个线程,并启动他们
  20.             Thread workerThread1 = new Thread(Thread1Function);
  21.             Thread workerThread2 = new Thread(Thread2Function);
  22.             workerThread1.Start();
  23.             workerThread2.Start();
  24.         }
  25.         
  26.         //生产线程
  27.         private void Thread1Function()
  28.         {
  29.             for(int i = 0; i < 100; i++)
  30.             {
  31.                 lock(m_Lock)
  32.                 {
  33.                     //当标识变量为消费的时候,Monitor暂时释放锁,并阻塞
  34.                     //当前线程
  35.                     if(!m_Flag)
  36.                     {
  37.                         Monitor.Wait(m_Lock);
  38.                     }
  39.                     
  40.                     //当标识变量为生产的时候,设置文本,并把标识
  41.                     //变量设置为消费
  42.                     text = "(t1) Called";
  43.                     this.Invoke(new EventHandler(WorkerThreadsFinished));
  44.                     m_Flag = false;
  45.                     //通知线程等待队列锁状态已经被改变,Wait的线程可以继续执行
  46.                     Monitor.Pulse(m_Lock);
  47.                 }
  48.             }
  49.         }
  50.         
  51.         //消费线程
  52.         private void Thread2Function()
  53.         {
  54.             for(int i = 0; i < 100; i++)
  55.             {
  56.                 lock(m_Lock)
  57.                 {
  58.                     //当标识变量为生产的时候,释放锁对象,阻塞当前线程
  59.                     if(m_Flag)
  60.                     {
  61.                         Monitor.Wait(m_Lock);
  62.                     }
  63.                     
  64.                     text = "(t2) Called";
  65.                     this.Invoke(new EventHandler(WorkerThreadsFinished));
  66.                     
  67.                     //设置标识标量为生产
  68.                     m_Flag = true;
  69.                     //通知线程等待队列锁状态已经被改变,Wait的线程可以继续执行
  70.                     Monitor.Pulse(m_Lock);
  71.                 }
  72.             }
  73.         }
  74.         
  75.         public delegate void EventHandler();
  76.         private void WorkerThreadsFinished()
  77.         {
  78.             listBox1.Items.Add(text);
  79.         }
  80.     }

 

Monitor.Wait方法将会暂时释放锁对象并阻塞线程,一旦其他线程调用Pulse方法,锁对象自动恢复,线程继续执行。

下面在看一个Mutex(互斥量)的例子

  1.     public partial class Form1 : Form
  2.     {
  3.         private string text;
  4.         private bool thread1Running;
  5.         private bool thread2Running;
  6. #if RUN_IN_SYNC
  7.         private Mutex myMutex;
  8. #endif
  9.         public Form1()
  10.         {
  11.             InitializeComponent();
  12.         }
  13.         private void button1_Click(object sender, EventArgs e)
  14.         {
  15.             label1.Text = "";
  16.             text = "";
  17.             button1.Enabled = false;
  18. #if RUN_IN_SYNC
  19.             myMutex = new Mutex();
  20. #endif
  21.             Thread workerThread1 = new Thread(Thread1Function);
  22.             Thread workerThread2 = new Thread(Thread2Function);
  23.             thread1Running = true;
  24.             thread2Running = true;
  25.             workerThread1.Start();
  26.             workerThread2.Start();
  27.         }
  28.         private void Thread1Function()
  29.         {
  30. #if RUN_IN_SYNC
  31.             myMutex.WaitOne();
  32. #endif
  33.             for (int i = 0; i < 5; i++)
  34.             {
  35.                 text += i.ToString();
  36.                 Thread.Sleep(0);
  37.             }
  38. #if RUN_IN_SYNC
  39.             myMutex.ReleaseMutex();
  40. #endif
  41.             thread1Running = false;
  42.             this.Invoke(new EventHandler(WorkerThreadsFinished));
  43.         }
  44.         private void Thread2Function()
  45.         {
  46. #if RUN_IN_SYNC
  47.             myMutex.WaitOne();
  48. #endif
  49.             for (int i = 0; i < 5; i++)
  50.             {
  51.                 text += i.ToString();
  52.                 Thread.Sleep(0);
  53.             }
  54. #if RUN_IN_SYNC
  55.             myMutex.ReleaseMutex();
  56. #endif
  57.             thread2Running = false;
  58.             this.Invoke(new EventHandler(WorkerThreadsFinished));
  59.         }
  60.         public delegate void EventHandler();
  61.         private void WorkerThreadsFinished()
  62.         {
  63.             label1.Text = text;
  64.             button1.Enabled = true;
  65.            
  66.         }
  67.     }

 

我们可以看到Mutex也有WaitOne的方法,但是与AutoResetEvent的WaitOne不同是ReleaseMutex方法来通知WaitOne继续执行线程,互斥量还可以用在进程之间的数据同步

原创粉丝点击