c#中多线程间的同步示例详解

目录
  • 一、引入
  • 二、Lock
  • 三、Monitor
  • 四、Interlocked
  • 五、Semaphore
  • 六、Event
  • 七、Barrier
  • 八、ReaderWriterLockSlim
  • 九、Mutex
  • 十、ThreadLocal ,AsyncLocal,Volatile
  • 十一、有意思的示例
  • 总结

一、引入

先给出一个Num类的定义

internal class Num
{
  public static int odd = 50000;
  public static int even = 10000;
}

假设现在要求输出小于 odd 的所有奇数,输出小于 even 的所有偶数,不考虑多线程时可以写出如下的代码:(为了演示多线程时线程间的争用,先把值赋给了 num,实际上这个赋值操作毫无意义 )

//同步代码段
public long Sum()
	{
  	Stopwatch sw = new Stopwatch();
    sw.Start();
    int num = 0;
    for (int i = 0; i <= Num.odd; i++)
    {
         num = i;
         if ((i & 1) == 1)
         {
            Console.WriteLine($"奇数:{num}");
         }
     }           

    for (int i = 0; i <= Num.even; i++)
    {
        num = i;
        if ((i & 1) == 0)
        {
            Thread.Sleep(10);
            Console.WriteLine($"偶数:{num}");
        }
    }

  	sw.Stop();
    return sw.ElapsedMilliseconds;
}

现在,因为耗时太长,引入多线程进行处理,修改为如下形式:

//NoLock Task
  private readonly object sync = new();
  int num = 0;
  public int Sum()
  {
    Stopwatch sw = new Stopwatch();
    sw.Start();
    var ta =Task.Run(() =>
    {
      for (int i = 0; i <= Num.odd; i++)
      {
        num = i; //判断条件之前赋值是为了提高触发几率
        if((i & 1) == 1)
        {
        	Console.WriteLine($"奇数:{num}");
        }
      }
    });

    var tb =  Task.Run(() =>
    {
    	for (int i = 0; i <= Num.even; i++)
      {
        num = i;
        if ((i & 1) == 0)
        {
        	Thread.Sleep(10);  //在此处添加延时,在 tb 线程等待时,num.sum的值可能已经被 ta 修改为了其他值
        	Console.WriteLine($"偶数:{num}");
        }
      }
    });

    Task.WaitAll(ta, tb);  //为了保证任务完成,获取执行时间
    sw.Stop();
    return sw.ElapsedMilliseconds;
  }
}

上面这段代码中,我们期望线程ta会输出odd以内的奇数值,线程tb会输出even以内的偶数。但实际运行时会出现下图所示的情况。

如上,当程序涉及到多线程的时候,在 [各线程间共享的数据] 总会因为线程间的争用导致意料之外的情况,为此,各种语言也会提供协助线程间同步的特性,这里简单记录一下我对c#中机制的理解。

二、Lock

借用Lock的方法对示例进行修改,可以有两种方式,此处展示ta部分,tb 与ta做相同修改:

//方式一:在Task内全局Lock
var ta = Task.Run(() =>
  {
    	lock (sync)
    	{
        for (int i = 0; i <= 10000; i++)
        {
          num = i; //判断条件之前赋值是为了提高触发几率
          if ((i & 1) == 1)
          {
            Console.WriteLine($"奇数:{num}");
          }
        }
    	}
   });

//方式二: 为每一次For循环Lock
var ta = Task.Run(() =>
{
  //lock (sync)
    for (int i = 0; i <= Num.odd; i++)
    {
      lock(sync)
      {
        num = i; //判断条件之前赋值是为了提高触发几率
        if ((i & 1) == 1)
        {
       	  Console.WriteLine($"奇数:{num}");
        }
      }
  }
});

上述的两种方式中,

  • 方法一相当于对Task进行了锁定,同一时刻只能运行一个被锁定的代码段(即取决于当前对象实例中ta,tb谁先取得了使用权),这样多线程其实退化为了单线程处理。
  • 方法二应该是更合理的使用方式,每一次循环时进行锁定,保证了每次赋值及使用时的独占性,也不影响另一个线程的循环操作。方式二仍然会存在一个线程等待的情况,只是会比第一种方式好一些。但是,对每一次循环进行Lock,性能是需要考虑的一个点。

说回Lock本身,网上有很多文章介绍,lock只是一个语法糖,编译器会将其转换为对 monitor 的调用。

IL代码如下图:

可以看到,编译器会帮我们构建try块,并在finally块调用Monitor.Exit方法。若要获取更精细的控制,可以自己调用Monitor进行使用。

三、Monitor

monitor与lock相比更为灵活,可以使用IsEntered(object)判断当前线程是否获取到了sync的锁定,可以使用 TryEnter()尝试获取排它锁,也可以调用重载方法指定等待时间。

需要指出的是,

1、所有等待获取锁定的线程会处于阻塞状态。

2、在等待获取锁定的线程上执行Thread.Interrupt会中断当前线程的等待并抛出ThreadInterruptedException的异常

3、monitor与lock锁定的对象sync必须为引用类型,查看反编译的代码会发现在每一次lock之前,会将sync赋值给一个object对象,如果sync为值类型,则会被装箱为一个新的对象。

4、以锁定For循环为例,在定义锁定对象时,可以定义为

   public class LockFor
   {
       private readonly object sync = new();
       ....
   }

或者定义为

 public class LockForStaticSync
 {
        private static readonly object sync = new();
   			...
 }

后者添加一个 static 修饰符将其定义静态只读对象。

我们知道,static 修饰的变量并不属于对象,而是归属于 class 本身,按照我的理解来说,如果在多个线程中都实例化了 LockForStaticSync 的对象,并同时调用一个 SUM 方法时,不论线程间是否会发生冲突,只能有一个线程取得sync的锁定继续执行,其他的线程会处于阻塞等待状态。

而如果是第一种定义方式,则多个线程的多个对象间,不会产生冲突,所有线程都可以执行。

此例的场景下,多实例调用时,方式一应该会有速度上的绝对优势。为此我删除了任务内打印及等待的部分,执行了如下内容进行验证:

private static void Invocation()
{
  Stopwatch sw = new Stopwatch();
  sw.Start();
  var t1 = new TaskFactory().StartNew(() => new LockFor().Sum());
  var t2 = new TaskFactory().StartNew(() => new LockFor().Sum());
  var t3 = new TaskFactory().StartNew(() => new LockFor().Sum());
  Task.WaitAll(t1, t2, t3);
  sw.Stop();
  Console.WriteLine(sw.ElapsedMilliseconds);

  sw.Restart();
  var t4 = new TaskFactory().StartNew(() => new LockForStaticSync().Sum());
  var t5 = new TaskFactory().StartNew(() => new LockForStaticSync().Sum());
  var t6 = new TaskFactory().StartNew(() => new LockForStaticSync().Sum());
  Task.WaitAll(t4, t5, t6);
  sw.Stop();
  Console.WriteLine(sw.ElapsedMilliseconds);
}

但实际执行结果是出乎意料的,多次运行后,类变量锁的运行速度远超对象变量的方式,这是为什么呢?

思考之后,考虑到类变量省略了每次创建锁定对象的时间,而数量较少的循环次数可能无法弥补这个时间差,于是我逐渐调大Num类中定义的变量。随着循环次数的增加,也就是方法执行时间的增加,对象变量的优势逐渐显现

既然对象变量有速度上的优势,而使用过程中又不可避免的会出现多方调用的情况,那么是不是应该一直选择定义为对象变量呢?其实不然,比如静态类中需要的锁定对象,全局缓存字典,文件操作帮助类都应该是静态锁,更多场景,欢迎补充。

四、Interlocked

Interlocked类中的方法可以实现原子操作,通过操作系统及硬件CPU级别的控制,确保CPU在执行当前操作时不会被中断,这个类里面提供了一些简单的方法,如Add,Increment等。

五、Semaphore

信号量是一种计数的互斥锁定。什么意思呢?以Monitor来讲,从monitor.enetr开始,到monitor.exit为止,被包裹着的这一段代码,同一时刻只能由一个线程访问,而 Semaphore 可以定义同时访问某些资源的线程数量,即允许多线程同时访问被保护的代码。

Semaphore有三种签名的构造函数,其中 Semaphore(int initialCount, int maximumCount) 的参数指定最初释放的信号量可用数量与最大量,两者的差值归创建信号量的线程所有。

以下内容来自 MSDN

class SemaphoreTest
    {
        // A semaphore that simulates a limited resource pool.
        private static Semaphore _pool;

        // 协助设置线程休眠时间.
        private static int _padding;

        public static void Main()
        {
            // 这里是创建了最大可以有三个访问线程的信号量,但此时可用为0,需要当前线程释放以后才可用。
            // 如果此处设置可用量非0,主线程释放时仍然传递了3,程序运行过程中会出现SemaphoreFullException的异常
            _pool = new Semaphore(0, 3);
            for (int i = 1; i <= 5; i++)
            {
                Thread t = new Thread(new ParameterizedThreadStart(Worker));
                t.Start(i);
            }

            // 主线程休眠,让其他线程运行到等待信号量的状态
            Thread.Sleep(500);

            //主线程调用Release(3),将可用量设置为最大值,正在等待的线程会获得信号
            Console.WriteLine("Main thread calls Release(3).");
            _pool.Release(3);

            Console.WriteLine("Main thread exits.");
            Console.ReadLine();
        }

        private static void Worker(object num)
        {
            // 阻塞当前线程,直到获取到信号量
            Console.WriteLine("Thread {0} begins " +"and waits for the semaphore.", num);
            _pool.WaitOne(); 

            // 每一个线程等待的时间间隔参数
            int padding = Interlocked.Add(ref _padding, 100);
            Console.WriteLine("Thread {0} enters the semaphore.", num);

            //与padding共用,让输出更有顺序
            Thread.Sleep(1000 + padding);

            Console.WriteLine("Thread {0} releases the semaphore.", num);
            Console.WriteLine("Thread {0} previous semaphore count: {1}",num, _pool.Release());                    }
    }

目前为止,提到的内容均是在同一个进程内同步的方法,信号量作为一个系统级的存在,是可以帮助我们实现进程间同步的,只需要在创建 Seamphore 对象的实例时,为信号量指定名称即可。

另外特别需要注意的是,信号量是可重入的,简单说就是可以在一个线程内执行多次 WaitOne() 方法,多次调用时,如果处理不好,就可能出现意外情况,修改 Worker 为下方内容

private static void Worker(object num)
        {
            Console.WriteLine("Thread {0} begins " +
                "and waits for the semaphore.", num);
            _pool.WaitOne();
            Console.WriteLine($"{num} getOne");
            // A padding interval to make the output more orderly.
            int padding = Interlocked.Add(ref _padding, 100);
            _pool.WaitOne();     //此处添加一处调用
            Console.WriteLine("Thread {0} enters the semaphore.", num);          

            Thread.Sleep(1000 + padding);

            Console.WriteLine("Thread {0} releases the semaphore.", num);
            Console.WriteLine("Thread {0} previous semaphore count: {1}",
                num, _pool.Release());
					//这里应该再调用一次_pool.Release(),或者在上一句传入参数,改为 _pool.Release(2)
        }

建议实际运行一下,查看运行的表现,运行过后会发现,任务线程全部阻塞在了第二处 WaitOne,导致程序无法向下执行。

因此,重入必须要谨慎,而且退出时必须要释放等量的锁定数值,如果执行了多余的Release(),最终程序在运行过程中会出现

SemaphoreFullException的异常

六、Event

与信号量一样,事件也是一个系统范围内的资源同步方法。又分为ManualResetEvent,AutoResetEvent,CountdownEvent以及ManualResetEventSlim,在构建对象实例时若传入了name参数,代表这是一个可以跨进程的系统级同步事件。

ManualResetEvent 为例,该类有 signaled 和 nonsignaled 两种状态,这两种状态通过实例化对象时的 布尔类型 参数决定,TRUE 就是signaled, False相反

文档中常见的翻译是发出信号的状态和未发出信号的状态,微软官网的机翻是终止状态和非终止状态,还有一些释放线程之类的描述,直观上难以理解。其实就是改变状态而已,还不如英文的好理解。

ManualResetEvent 的基类 EventWaitHandle 中提供了Set() 和Reset() 方法,用于改变状态,Set 将事件修改为 signaled,Reset重置为 nonsignaled

这里说一下Set和Reset,这两个方法是改变了事件的状态,并不是一个瞬时性的动作,也就意味着在调用Set后,调用Reset之前,事件都处于 signaled 状态(AutoResetEvent会自动调用Reset重置事件状态)

WaitHandle 类中提供了众多等待信号的方法,EventWaitHandle 继承自WaitHandle, ManualResetEvent中也可以调用WaitOne等方法。

回头再看一下信号量 Semaphore 的示例,也调用 WaitOne 等待信号,因为它也继承自 Waithandler。

有了上面的基础,可以看一下下面 MSDN 的示例

private static ManualResetEvent mre = new ManualResetEvent(false);
static void Main()
{
  for (int i = 0; i <= 2; i++)
  {
    Thread t = new Thread(ThreadProc);
    t.Name = "Thread_" + i;
    t.Start();
  }

  Thread.Sleep(500);
  Console.WriteLine(@"线程012都会处于等待状态,直至 mre修改状态,按 Enter继续");
  Console.ReadLine();

  mre.Set();

  Thread.Sleep(500);
  Console.WriteLine(@"调用了mre.set后,事件处于 signaled 状态,下一个线程不会被阻塞");
  Console.ReadLine();

  for (int i = 3; i <= 4; i++)
  {
    Thread t = new Thread(ThreadProc);
    t.Name = "Thread_" + i;
    t.Start();
  }

  Thread.Sleep(500);
  Console.WriteLine("调用mre.reset后,事件处于nonsignaled状态,此时会被阻塞");
  Console.ReadLine();
  mre.Reset();

  Thread t5 = new Thread(ThreadProc);
  t5.Name = "Thread_5";
  t5.Start();

  Thread.Sleep(500);
  Console.WriteLine("\nPress Enter to call Set() and conclude the demo.");
  Console.ReadLine();

  mre.Set();
  Console.ReadLine();
}

private static void ThreadProc()
{
  string name = Thread.CurrentThread.Name;
  Console.WriteLine(name + " starts and calls mre.WaitOne()");
  mre.WaitOne();
  Console.WriteLine(name + " ends.");
}
  • AutoResetEvent,名字可以看出来这个类会自动调用 Reset方法,事实也是这样,这个类会在等待线程执行结束后将事件置为non-signaled
  • ManualResetEventSlim 是 ManualResetEvent的轻量实现,他并不是继承自 EventWaitHandle基类,
  • CountdownEvent 也不是继承自 EventWaitHandle基类,它会在初始化时得到一个数值,称为InitialCount,同时赋给CurrentCount , 每次调用Signal时,CurrentCount会减少相应的值,当调用后CurrentCount为0时,会发出信号,并将其设置为 IS_SET 状态。

七、Barrier

Barrier 是一个有意思的类,可以使多个任务能够采用并行方式依据某种算法在多个阶段中协同工作。

Enables multiple tasks to cooperatively work on an algorithm in parallel through multiple phases.

大白话来讲,借助于这个对象,可以管控多个并行任务间的合作关系。

Barrier的构造函数中可以传入并行任务的数量(participantCount),也可以通过 AddParticipant,RemoveParticipant 动态地调整参与者的数量。另外可以通过 CurrentPhaseNumber 得知当前是第几个参与者,通过 ParticipantsRemaining 知道还有几个参与者未到达任务点。

还可以传入一个可空委托,这个委托会在接收到所有参与者线程发出信号后执行。

就好像几个人一起玩游戏闯关,关卡boss需要所有人一起才能打败,照顾到每个人的游戏理解不同,规定每个人可以按照自己的安排推进游戏进度。

在这个比喻中,每个人都是单独的线程,可能有人很快就抵达了关卡,但是因为关卡的性质,他必须在这里等待其他人都到达后,大家一起打boss,打败boss之后存档,之后大家再各玩各的,直到下一个关卡BOSS。

Barrier就承担了boss的任务,他负责让所有线程抵达某一个预设的点后再一起放行。放行之后,会执行初始化对象时传入的委托,比如上方说的存档。只是我们可以通过委托,更灵活地指定要进行的操作。

而所谓的预设点,其实就是调用 SignalAndWait,发出信号并等待其他线程发出信号的代码

如果你暂时没有理解我上面的比喻,那就看一下下面的代码吧,代码是在MSDN拿来的,我自己加了几个console语句,可以运行看看效果

public static void Barriers()
        {
            int count = 0;
           //初始化 3 个参与者,传入委托
            Barrier barrier = new Barrier(3, (b) =>
            {
                Console.WriteLine("Post-Phase action: count={0}, phase={1},threadid={2}", count, b.CurrentPhaseNumber,Thread.CurrentThread.ManagedThreadId);
                if (b.CurrentPhaseNumber == 2)
                    throw new Exception("D'oh!");
            });

            barrier.AddParticipants(2);

            barrier.RemoveParticipant(); //剩下4个
            Console.WriteLine($"主Thread:{Thread.CurrentThread.ManagedThreadId}");

            // This is the logic run by all participants
            Action action = () =>
            {
                Console.WriteLine($"action1 Thread:{Thread.CurrentThread.ManagedThreadId}");
                Interlocked.Increment(ref count);
                barrier.SignalAndWait(); // during the post-phase action, count should be 4 and phase should be 0

                Console.WriteLine($"action2 Thread:{Thread.CurrentThread.ManagedThreadId}");
                Interlocked.Increment(ref count);
                barrier.SignalAndWait(); // during the post-phase action, count should be 8 and phase should be 1
                Console.WriteLine($"action3 Thread:{Thread.CurrentThread.ManagedThreadId}");

                // 线程3会引发委托里抛出的异常,异常信息所有线程可见
                Interlocked.Increment(ref count);
                try
                {
                    barrier.SignalAndWait();
                }
                catch (BarrierPostPhaseException bppe)
                {
                    Console.WriteLine("Caught BarrierPostPhaseException: {0}", bppe.Message);
                }
                Console.WriteLine($"action4 Thread:{Thread.CurrentThread.ManagedThreadId}");

                // The fourth time should be hunky-dory
                Interlocked.Increment(ref count);
                barrier.SignalAndWait(); // during the post-phase action, count should be 16 and phase should be 3
                Console.WriteLine($"action5 Thread:{Thread.CurrentThread.ManagedThreadId}");

            };

            // 启动与 Barrier设置数量相同的任务,如果启动数目超过设置值,会引发如下异常
  				  //"System.InvalidOperationException: The number of threads using the barrier exceeded the total number of registered participants."
            Parallel.Invoke(action, action, action, action);

            Console.WriteLine($"主Thread2:{Thread.CurrentThread.ManagedThreadId}");

            // It's good form to Dispose() a barrier when you're done with it.
            barrier.Dispose();
        }

执行效果如下:

可以在24行,27行打断点,借助上面的比喻,理解一下Barrier的用法。

最后要声明的是Barrier的public protected 成员是线程安全的,可以跨线程使用。但是Dispose是非线程安全的,意味着一旦调用,所有线程都会受到影响,应该在任务代码之外执行,另外既然有dispose的方法,就要注意使用完毕后调用该方法释放资源。

八、ReaderWriterLockSlim

读写锁,允许多个线程处于读取模式,允许一个线程处于具有独占锁定权限的写入模式,并且允许具有读取访问权限的一个线程处于可升级读取模式,在该模式下,线程可以升级到写入模式,而无需放弃对资源的读取访问权限。

读写锁具有三种模式,读锁,写锁,可升级的读锁

读锁可以通过 EnterReadLock 进入,通过 ExitReadLock 退出,写锁类似EnterWriteLock,ExitWriteLock

可升级的读锁是指可以直接由读转换为写模式的状态,EnterUpgradeableReadLock及ExitUpgradeableReadLock

与其他同步对象相同,读写锁需要正确的进行释放,不然会引发问题

读写锁初始化时,可以传入 LockRecursionPolicy 指定递归状态,默认的构造函数为NoRecursion,微软官网并不建议新手使用递归策略,因为这具有更高的复杂性,而且容易带来死锁的问题,我自己也没有用过递归策略。

与ReaderWriterLock相比,ReaderWriterLockSlim是被推荐使用的对象

对于 ReaderWriterLockSlim 的使用,我在园子里有个提问请教ReaderWriterLockSlim的问题,建议转过去看下,我这边就不放代码了,当时找到了另一篇文章解答了我的疑惑,地址也贴在这里 C# ReaderWriterLockSlim 实现 - dz45693.

这里面就有一些需要注意的点,这几个点全部是摘抄自上面那篇文章,请知悉:

  • 对于同一把锁、多个线程可同时进入读模式。
  • 对于同一把锁、同时只允许一个线程进入写模式。
  • 对于同一把锁、同时只允许一个线程进入可升级的读模式。
  • 通过默认构造函数创建的读写锁是不支持递归的,若想支持递归 可通过构造 ReaderWriterLockSlim(LockRecursionPolicy) 创建实例。
  • 对于同一把锁、同一线程不可两次进入同一锁状态(开启递归后可以)
  • 对于同一把锁、即便开启了递归、也不可以在进入读模式后再次进入写模式或者可升级的读模式(在这之前必须退出读模式)。
  • 再次强调、不建议启用递归。
  • 读写锁具有线程关联性,即两个线程间拥有的锁的状态相互独立不受影响、并且不能相互修改其锁的状态。
  • 升级状态:在进入可升级的读模式 EnterUpgradeableReadLock后,可在恰当时间点通过EnterWriteLock进入写模式。
  • 降级状态:可升级的读模式可以降级为读模式:即在进入可升级的读模式EnterUpgradeableReadLock后, 通过首先调用读取模式EnterReadLock方法,然后再调用 ExitUpgradeableReadLock 方法。

具体的代码示例请参考我的提问及 dz45693 的文章

九、Mutex

Mutex 同Event,Semaphore类似,可以跨进程同步内容,定义跨进程的Mutex只需要在初始化时为其指定名字即可;都继承自WaitHandle,所以也有waitone的方法可以调用。

使用上与 Monitor 类似,属于互斥锁,所以在任务的最后必须要调用 ReleaseMutex()。

Mutex 实现了IDispose接口,所以需要在finally块内调用 Dispose() 方法。

Mutex可以用来限定winform程序只能有一个实例运行,实例如下:

 static void Main()
 {
   bool runone;
   //获取名为 single_test的互斥的初始所有权,runone指定是否成功
   Mutex run = new Mutex(true, "single_test", out runone);
   if (runone) //true代表当前未创建改互斥
   {
     run.ReleaseMutex();
     Application.EnableVisualStyles();
     Application.SetCompatibleTextRenderingDefault(false);
     FrmRemote frm = new FrmRemote();
     int hdc = frm.Handle.ToInt32(); // write to ...
     Application.Run(frm);
     IntPtr a = new IntPtr(hdc);
   }
   else
   {
     MessageBox.Show("已经运行了一个实例了。");
   }
 }

十、ThreadLocal ,AsyncLocal,Volatile

在定义一个类时,有时会定义全局变量,如果在编写类时未考虑在多线程中使用,那么在类中定义的全局变量很有可能会因为多线程调用引发异常,比如文章开头的引子中,将 num 定义为了类的全局变量,造成多线程调用时出现错误的情况,所以需要减少全局变量的使用。但是,有时候,我们又不得不借助全局变量帮助我们实现需求,这时候就可以考虑上面提到的这几个。

我们可能希望定义的变量对每个线程是唯一的,这时候就可以借助ThreadLocal,如果是使用了async,await的写法,因为在await之后执行线程会发生变化,这时候就可以使用AsyncLocal,只是需要注意一下变量在父子进程间的传递关系是怎么样的。

AsyncLocal变量可以在父子线程中传递,创建子线程时父线程会将自己的AsyncLocal类型的上下文变量赋值到子线程中,但是,当子线程改变线程上下文中AsnycLocal变量值后,父线程不会同步改变。也就是说AsnycLocal变量只会影响他的子线程,不会影响他的父级线程。ThreadLocal只是当前线程的上下文变量,不能在父子线程间同步。

​ 具体可看 AsnycLocal与ThreadLocal

至于Volatile,在 c # 中,对 volatile 字段使用修饰符可保证对该字段的每个访问都是易失性内存操作。我们知道vs在release模式下编译时会对代码进行优化,优化的过程中可能会修改一些内容,volatile修饰的字段则不会被编译器进行优化。除此之外,多线程协作时,希望对某一个变量的修改可以立即反馈到其他线程中,这时候也可以借助 volatile,但 volatile 修饰符不能应用于数组元素。 Volatile.ReadVolatile.Write 方法可用于数组元素。

volatile 值立马反馈到其他线程是因为处理被标记字段时,处理器不会使用缓存,而是每次都去内存里读取该字段。

至于处理器缓存之类的,如果有兴趣,可以自行了解。

AsyncLocal 和 volatile 我自己并没有实际用过,只是在网上看了一些内容,在官方文档看了一点内容,如有需要建议自行搜索。

十一、有意思的示例

记得之前看过一个例子,两个线程循环输出文字,不记得在哪看的了,试着写一下

public class Sample
    {   //先执行的线程设置为 true
        ManualResetEvent even = new ManualResetEvent(true);
        ManualResetEvent odd = new ManualResetEvent(false);        

        public void Sum()
        {
            var ta = Task.Run(() => PrintEven(even, odd));
            var tb = Task.Run(() => PrintOdd (even,odd));
        }
				//等待自己的信号,控制另一个线程的信号
        public void PrintEven(EventWaitHandle evenHandle, EventWaitHandle oddHandle)
        {
            string design =  "偶数";
            for (int i = 0; i <= 20; i++)
            {
                evenHandle.WaitOne();
                if ((i & 1) == 0)
                {
                    Console.WriteLine($"{design}:{i}");
                    evenHandle.Reset();
                    oddHandle.Set();
                }
            }
        }
        public void PrintOdd(EventWaitHandle evenHandle, EventWaitHandle oddHandle)
        {
            string design = "奇数";
            for (int i = 0; i <= 20; i++)
            {
                oddHandle.WaitOne();
                if ((i & 1) == 1)
                {
                    Console.WriteLine($"{design}:{i}");
                    oddHandle.Reset();
                    evenHandle.Set();
                }
            }
        }
    }

​流水账似的写下了任务同步的实现方法,于我自己而言,对文章中提到的内容加深了很多理解,希望对读到文章的你也有帮助。

总结

到此这篇关于c#中多线程间的同步的文章就介绍到这了,更多相关c#多线程间同步内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C#实现多线程的同步方法实例分析

    本文主要描述在C#中线程同步的方法.线程的基本概念网上资料也很多就不再赘述了.直接接入 主题,在多线程开发的应用中,线程同步是不可避免的.在.Net框架中,实现线程同步主要通过以下的几种方式来实现,在MSDN的线程指南中已经讲了几种,这里结合作者实际中用到的方式一起说明一下. 1. 维护自由锁(InterLocked)实现同步 2. 监视器(Monitor)和互斥锁(lock) 3. 读写锁(ReadWriteLock) 4. 系统内核对象 1) 互斥(Mutex), 信号量(Semaphore

  • 详解C#多线程之线程同步

    多线程内容大致分两部分,其一是异步操作,可通过专用,线程池,Task,Parallel,PLINQ等,而这里又涉及工作线程与IO线程:其二是线程同步问题,鄙人现在学习与探究的是线程同步问题. 通过学习<CLR via C#>里面的内容,对线程同步形成了脉络较清晰的体系结构,在多线程中实现线程同步的是线程同步构造,这个构造分两大类,一个是基元构造,一个是混合构造.所谓基元则是在代码中使用最简单的构造.基原构造又分成两类,一个是用户模式,另一个是内核模式.而混合构造则是在内部会使用基元构造的用户模

  • C#多线程及同步示例简析

    60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建.撤消与切换存在较大的时空开销,因此需要引入轻型进程:二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大. 因此在80年代,出现了能独立运行的基本单位--线程(Threads).        线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元.一个标准的线程由线程ID,当前指令指针(P

  • 详细解析C#多线程同步事件及等待句柄

    最近捣鼓了一下多线程的同步问题,发现其实C#关于多线程同步事件处理还是很灵活,这里主要写一下,自己测试的一些代码,涉及到了AutoResetEvent 和 ManualResetEvent,当然还有也简要提了一下System.Threading.WaitHandle.WaitOne .System.Threading.WaitHandle.WaitAny和System.Threading.WaitHandle.WaitAll ,下面我们一最初学者的角度来看,多线程之间的同步. 假设有这样的一个场

  • C#简单多线程同步和优先权用法实例

    本文实例讲述了C#简单多线程同步和优先权用法.分享给大家供大家参考.具体分析如下: 本文实例题目如下: 麦当劳有两个做汉堡的厨师(工号:11,12)和三个销售人员(工号:21,22,23). 厨师生产汉堡,并负责将做好的汉堡放入货架,货架台大小有限,最多放6个汉堡,11和12不能同时往货架台上放汉堡,11具有优先权. 销售人员负责销售食品,三个销售人员取食品时,货架不能为空,三人不能同时取,23优先权最高,21最低.21卖的最快,取得频率最高,22次之. 一天的工作量是销售70个汉堡. 这里先来

  • c#中多线程间的同步示例详解

    目录 一.引入 二.Lock 三.Monitor 四.Interlocked 五.Semaphore 六.Event 七.Barrier 八.ReaderWriterLockSlim 九.Mutex 十.ThreadLocal ,AsyncLocal,Volatile 十一.有意思的示例 总结 一.引入 先给出一个Num类的定义 internal class Num { public static int odd = 50000; public static int even = 10000;

  • Python异步爬虫多线程与线程池示例详解

    目录 背景 异步爬虫方式 多线程,多进程(不建议) 线程池,进程池(适当使用) 单线程+异步协程(推荐) 多线程 线程池 背景 当对多个url发送请求时,只有请求完第一个url才会接着请求第二个url(requests是一个阻塞的操作),存在等待的时间,这样效率是很低的.那我们能不能在发送请求等待的时候,为其单独开启进程或者线程,继续请求下一个url,执行并行请求 异步爬虫方式 多线程,多进程(不建议) 好处:可以为相关阻塞的操作单独开启线程或者进程,阻塞操作就可以异步会执行 弊端:不能无限制开

  • Go语言中循环语句使用的示例详解

    目录 一.概述 1. 循环控制语句 2. 无限循环 二.Go 语言 for 循环 1. 语法 2. for语句执行过程 3. 示例 4. For-each range 循环 三.循环嵌套 1. 语法 2. 示例 四.break 语句 1. 语法 2. 示例 五. continue 语句 1. 语法 2. 示例 六.goto 语句 1. 语法 2. 示例 一.概述 在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句. 循环程序的流程图: Go 语言提供了以下几种类型循环

  • RxJS中的Observable和Observer示例详解

    目录 引言 概念 牛刀小试 Observable Observable 剖析 Observer 结束语 引言 最近在项目当中别的小伙伴使用到了Rxjs,我一眼看上去有点懵,感觉挺复杂,挺绕的.于是抓紧补补课,然后就可以和小伙伴们一起交流怎么能优雅的使用Rxjs.由于内容比较多,会分为三篇来讲解说明 初识 RxJS中的 Observable 和 Observer 细说 RxJS中的 Operators 在谈 RxJS中的 Subject和Schedulers 概念 RxJS是一个库,可以使用可观察

  • C语言中进程间通讯的方式详解

    目录 一.无名管道 1.1无名管道的原理 1.2功能 1.3无名管道通信特点 1.4无名管道的实例 二.有名管道 2.1有名管道的原理 2.2有名管道的特点 2.3有名管道实例 三.信号 3.1信号的概念 3.2发送信号的函数 3.3常用的信号 3.4实例 四.IPC进程间通信 4.1IPC进程间通信的种类 4.2查看IPC进程间通信的命令 4.3消息队列 4.4共享内存 4.5信号灯集合 一.无名管道 1.1无名管道的原理 无名管道只能用于亲缘间进程的通信,无名管道的大小是64K.无名管道是内

  • Go语言中的字符串处理方法示例详解

    1 概述 字符串,string,一串固定长度的字符连接起来的字符集合.Go语言的字符串是使用UTF-8编码的.UTF-8是Unicode的实现方式之一. Go语言原生支持字符串.使用双引号("")或反引号(``)定义. 双引号:"", 用于单行字符串. 反引号:``,用于定义多行字符串,内部会原样解析. 示例: // 单行 "心有猛虎,细嗅蔷薇" // 多行 ` 大风歌 大风起兮云飞扬. 威加海内兮归故乡. 安得猛士兮守四方! ` 字符串支持转义

  • C++中#include头文件的示例详解

    fstream是C++ STL中对文件操作的合集,包含了常用的所有文件操作.在C++中,所有的文件操作,都是以流(stream)的方式进行的,fstream也就是文件流file stream. 最常用的两种操作为: 1.插入器(<<) 向流输出数据.比如说打开了一个文件流fout,那么调用fout<<"Write to file"<<endl;就表示把字符串"Write to file"写入文件并换行. 2.析取器(>>

  • Python中bisect的用法及示例详解

    bisect是python内置模块,用于有序序列的插入和查找. 查找: bisect(array, item) 插入: insort(array,item) 查找 import bisect a = [1,4,6,8,12,15,20] position = bisect.bisect(a,13) print(position) # 用可变序列内置的insert方法插入 a.insert(position,13) print(a) 输出: 5 [1, 4, 6, 8, 12, 13, 15, 2

  • Python中三种花式打印的示例详解

    目录 1. 引言 2. 打印圣诞树 2.1 问题描述 2.2 问题分析 3. 打印字母版圣诞树 3.1 问题描述 3.2 问题分析 4. 打印字母版菱形 4.1 问题描述 4.2 问题分析 5. 总结 1. 引言 在Python中有很多好玩的花式打印,对厉害的高手来说可能是小菜一碟,对入门的小白来说往往让人望而退步,我们今天就来挑战下面三个常见的花式打印吧... 2. 打印圣诞树 2.1 问题描述 编码实现函数christmas_tree(height),该函数输入参数为一个整数表示圣诞树的高度

  • 详解Python中生成随机数据的示例详解

    目录 随机性有多随机 加密安全性 PRNG random 模块 数组 numpy.random 相关数据的生成 random模块与NumPy对照表 CSPRNG 尽可能随机 os.urandom() secrets 最佳保存方式 UUID 工程随机性的比较 在日常工作编程中存在着各种随机事件,同样在编程中生成随机数字的时候也是一样,随机有多随机呢?在涉及信息安全的情况下,它是最重要的问题之一.每当在 Python 中生成随机数据.字符串或数字时,最好至少大致了解这些数据是如何生成的. 用于在 P

随机推荐