线程同步

上一篇介绍了如何开启线程,线程间相互传递参数,及线程中本地变量和全局共享变量区别。

本篇主要说明线程同步

如果有多个线程同时访问共享数据的时候,就必须要用线程同步,防止共享数据被破坏。如果多个线程不会同时访问共享数据,可以不用线程同步。

线程同步也会有一些问题存在:

  1. 性能损耗。获取,释放锁,线程上下文建切换都是耗性能的。
  2. 同步会使线程排队等待执行。

线程同步的几种方法:

阻塞

当线程调用Sleep,Join,EndInvoke,线程就处于阻塞状态(Sleep使调用线程阻塞,Join、EndInvoke使另外一个线程阻塞),会立即从cpu退出。(阻塞状态的线程不消耗cpu)

当线程在阻塞和非阻塞状态间切换时会消耗几毫秒时间。

//Join
static void Main()
{
Thread t = new Thread (Go);
Console.WriteLine ("Main方法已经运行....");
t.Start();
t.Join();//阻塞Main方法
Console.WriteLine ("Main方法解除阻塞,继续运行...");
} static void Go()
{
Console.WriteLine ("在t线程上运行Go方法...");
} //Sleep
static void Main()
{
Console.WriteLine ("Main方法已经运行....");
Thread.CurrentThread.Sleep(3000);//阻塞当前线程
Console.WriteLine ("Main方法解除阻塞,继续运行...");
} //Task
static void Main()
{
Task Task1=Task.Run(() => {
Console.WriteLine("task方法执行...");
Thread.Sleep(1000);
});
Console.WriteLine(Task1.IsCompleted);
Task1.Wait();//阻塞主线程 ,等该Task1完成
Console.WriteLine(Task1.IsCompleted);
}

加锁(lock)

加锁使多个线程同一时间只有一个线程可以调用该方法,其他线程被阻塞。

同步对象的选择:

  • 使用引用类型,值类型加锁时会装箱,产生一个新的对象。
  • 使用private修饰,使用public时易产生死锁。(使用lock(this),lock(typeof(实例))时,该类也应该是private)
  • string不能作为锁对象。
  • 不能在lock中使用await关键字

锁是否必须是静态类型?

如果被锁定的方法是静态的,那么这个锁必须是静态类型。这样就是在全局锁定了该方法,不管该类有多少个实例,都要排队执行。

如果被锁定的方法不是静态的,那么不能使用静态类型的锁,因为被锁定的方法是属于实例的,只要该实例调用锁定方法不产生损坏就可以,不同实例间是不需要锁的。这个锁只锁该实例的方法,而不是锁所有实例的方法.*

class ThreadSafe
{
 private static object _locker = new object();   void Go()
  {
lock (_locker)
{
      ......//共享数据的操作 (Static Method),使用静态锁确保所有实例排队执行
    }
  } private object _locker2=new object();
void GoTo()
{
lock(_locker2)
//共享数据的操作,非静态方法,是用非静态锁,确保同一个实例的方法调用者排队执行
}
}

同步对象可以兼作它lock的对象

如:

class ThreadSafe
{
 private List <string> _list = new List <string>();
  void Test()
  {
    lock (_list)
    {
      _list.Add ("Item 1");
}
}
}

Monitors

lock其实是Monitors的简洁写法。

lock (x)
{
DoSomething();
}

两者其实是一样的。

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}

互斥锁(Mutex)

互斥锁是一个互斥的同步对象,同一时间有且仅有一个线程可以获取它。可以实现进程级别上线程的同步。

class Program
{
//实例化一个互斥锁
public static Mutex mutex = new Mutex(); static void Main(string[] args)
{
for (int i = 0; i < 3; i++)
{
//在不同的线程中调用受互斥锁保护的方法
Thread test = new Thread(MutexMethod);
test.Start();
}
Console.Read();
} public static void MutexMethod()
{
Console.WriteLine("{0} 请求获取互斥锁", Thread.CurrentThread.Name);
mut.WaitOne();
Console.WriteLine("{0} 已获取到互斥锁", Thread.CurrentThread.Name);
Thread.Sleep(1000);
Console.WriteLine("{0} 准备释放互斥锁", Thread.CurrentThread.Name);
// 释放互斥锁
mut.ReleaseMutex();
Console.WriteLine("{0} 已经释放互斥锁", Thread.CurrentThread.Name);
}
}

互斥锁可以在不同的进程间实现线程同步

使用互斥锁实现一个一次只能启动一个应用程序的功能。

    public static class SingleInstance
{
private static Mutex m; public static bool IsSingleInstance()
{
//是否需要创建一个应用
Boolean isCreateNew = false;
try
{
m = new Mutex(initiallyOwned: true, name: "SingleInstanceMutex", createdNew: out isCreateNew);
}
catch (Exception ex)
{ }
return isCreateNew;
}
}

互斥锁的带有三个参数的构造函数

  1. initiallyOwned: 如果initiallyOwned为true,互斥锁的初始状态就是被所实例化的线程所获取,否则实例化的线程处于未获取状态。

  2. name:该互斥锁的名字,在操作系统中只有一个命名为name的互斥锁mutex,如果一个线程得到这个name的互斥锁,其他线程就无法得到这个互斥锁了,必须等待那个线程对这个线程释放。

  3. createNew:如果指定名称的互斥体已经存在就返回false,否则返回true。


信号和句柄

lockmutex可以实现线程同步,确保一次只有一个线程执行。但是线程间的通信就不能实现。如果线程需要相互通信的话就要使用AutoResetEvent,ManualResetEvent,通过信号来相互通信。它们都有两个状态,终止状态和非终止状态。只有处于非终止状态时,线程才可以阻塞。

AutoResetEvent

AutoResetEvent 构造函数可以传入一个bool类型的参数,false表示将AutoResetEvent对象的初始状态设置为非终止。如果为true标识终止状态,那么WaitOne方法就不会再阻塞线程了。但是因为该类会自动的将终止状态修改为非终止,所以,之后再调用WaitOne方法就会被阻塞。

WaitOne 方法如果AutoResetEvent对象状态非终止,则阻塞调用该方法的线程。可以指定时间,若没有获取到信号,返回false

set 方法释放被阻塞的线程。但是一次只可以释放一个被阻塞的线程。

class ThreadSafe
{
static AutoResetEvent autoEvent; static void Main()
{
//使AutoResetEvent处于非终止状态
autoEvent = new AutoResetEvent(false); Console.WriteLine("主线程运行...");
Thread t = new Thread(DoWork);
t.Start(); Console.WriteLine("主线程sleep 1秒...");
Thread.Sleep(1000); Console.WriteLine("主线程释放信号...");
autoEvent.Set();
} static void DoWork()
{
Console.WriteLine(" t线程运行DoWork方法,阻塞自己等待main线程信号...");
autoEvent.WaitOne();
Console.WriteLine(" t线程DoWork方法获取到main线程信号,继续执行...");
} } //输出
//主线程运行...
//主线程sleep 1秒...
// t线程运行DoWork方法,阻塞自己等待main线程信号...
//主线程释放信号...
// t线程DoWork方法获取到main线程信号,继续执行...

ManualResetEvent

ManualResetEventAutoResetEvent用法类似。

AutoResetEvent在调用了Set方法后,会自动的将信号由释放(终止)改为阻塞(非终止),一次只有一个线程会得到释放信号。而ManualResetEvent在调用Set方法后不会自动的将信号由释放(终止)改为阻塞(非终止),而是一直保持释放信号,使得一次有多个被阻塞线程运行,只能手动的调用Reset方法,将信号由释放(终止)改为阻塞(非终止),之后的再调用Wait.One方法的线程才会被再次阻塞。

public class ThreadSafe
{
//创建一个处于非终止状态的ManualResetEvent
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("\n新线程的方法已经启动,且被阻塞,调用Set释放阻塞线程"); mre.Set(); Thread.Sleep(500);
Console.WriteLine("\n当ManualResetEvent处于终止状态时,调用由Wait.One方法的多线程,不会被阻塞。"); for(int i = 3; i <= 4; i++)
{
Thread t = new Thread(ThreadProc);
t.Name = "Thread_" + i;
t.Start();
} Thread.Sleep(500);
Console.WriteLine("\n调用Reset方法,ManualResetEvent处于非阻塞状态,此时调用Wait.One方法的线程再次被阻塞"); mre.Reset(); Thread t5 = new Thread(ThreadProc);
t5.Name = "Thread_5";
t5.Start(); Thread.Sleep(500);
Console.WriteLine("\n调用Set方法,释放阻塞线程"); mre.Set();
} private static void ThreadProc()
{
string name = Thread.CurrentThread.Name; Console.WriteLine(name + " 运行并调用WaitOne()"); mre.WaitOne(); Console.WriteLine(name + " 结束");
}
} //Thread_2 运行并调用WaitOne()
//Thread_1 运行并调用WaitOne()
//Thread_0 运行并调用WaitOne() //新线程的方法已经启动,且被阻塞,调用Set释放阻塞线程 //Thread_2 结束
//Thread_1 结束
//Thread_0 结束 //当ManualResetEvent处于终止状态时,调用由Wait.One方法的多线程,不会被阻塞。 //Thread_3 运行并调用WaitOne()
//Thread_4 运行并调用WaitOne() //Thread_4 结束
//Thread_3 结束 ///调用Reset方法,ManualResetEvent处于非阻塞状态,此时调用Wait.One方法的线程再次被阻塞 //Thread_5 运行并调用WaitOne()
//调用Set方法,释放阻塞线程
//Thread_5 结束

Interlocked

如果一个变量被多个线程修改,读取。可以用Interlocked

计算机上不能保证对一个数据的增删是原子性的,因为对数据的操作也是分步骤的:

  1. 将实例变量中的值加载到寄存器中。
  2. 增加或减少该值。
  3. 在实例变量中存储该值。

Interlocked为多线程共享的变量提供原子操作。

Interlocked提供了需要原子操作的方法:

  • public static int Add (ref int location1, int value); 两个参数相加,且把结果和赋值该第一个参数。

  • public static int Increment (ref int location); 自增。

  • public static int CompareExchange (ref int location1, int value, int comparand);

    location1 和comparand比较,被value替换.
    
    value 如果第一个参数和第三个参数相等,那么就把value赋值给第一个参数。
    
    comparand 和第一个参数对比。

ReaderWriterLock

如果要确保一个资源或数据在被访问之前是最新的。那么就可以使用ReaderWriterLock.该锁确保在对资源获取赋值或更新时,只有它自己可以访问这些资源,其他线程都不可以访问。即排它锁。但用改锁读取这些数据时,不能实现排它锁。

lock允许同一时间只有一个线程执行。而ReaderWriterLock允许同一时间有多个线程可以执行读操作,或者只有一个有排它锁的线程执行写操作

    class Program
{
// 创建一个对象
public static ReaderWriterLock readerwritelock = new ReaderWriterLock();
static void Main(string[] args)
{
//创建一个线程读取数据
Thread t1 = new Thread(Write);
// t1.Start(1);
Thread t2 = new Thread(Write);
//t2.Start(2);
// 创建10个线程读取数据
for (int i = 3; i < 6; i++)
{
Thread t = new Thread(Read);
// t.Start(i);
} Console.Read(); } // 写入方法
public static void Write(object i)
{
// 获取写入锁,20毫秒超时。
Console.WriteLine("线程:" + i + "准备写...");
readerwritelock.AcquireWriterLock(Timeout.Infinite);
Console.WriteLine("线程:" + i + " 写操作" + DateTime.Now);
// 释放写入锁
Console.WriteLine("线程:" + i + "写结束...");
Thread.Sleep(1000);
readerwritelock.ReleaseWriterLock(); } // 读取方法
public static void Read(object i)
{
Console.WriteLine("线程:" + i + "准备读..."); // 获取读取锁,20毫秒超时
readerwritelock.AcquireReaderLock(Timeout.Infinite);
Console.WriteLine("线程:" + i + " 读操作" + DateTime.Now);
// 释放读取锁
Console.WriteLine("线程:" + i + "读结束...");
Thread.Sleep(1000); readerwritelock.ReleaseReaderLock(); }
}
//分别屏蔽writer和reader方法。可以更清晰的看到 writer被阻塞了。而reader没有被阻塞。 //屏蔽reader方法
//线程:1准备写...
//线程:1 写操作2017/7/5 17:50:01
//线程:1写结束...
//线程:2准备写...
//线程:2 写操作2017/7/5 17:50:02
//线程:2写结束... //屏蔽writer方法
//线程:3准备读...
//线程:5准备读...
//线程:4准备读...
//线程:5 读操作2017/7/5 17:50:54
//线程:5读结束...
//线程:3 读操作2017/7/5 17:50:54
//线程:3读结束...
//线程:4 读操作2017/7/5 17:50:54
//线程:4读结束...

参考:

  • MSDN
  • 《CLR via C#》

c#线程-线程同步的更多相关文章

  1. Java线程:线程的同步-同步方法

    Java线程:线程的同步-同步方法   线程的同步是保证多线程安全访问竞争资源的一种手段. 线程的同步是Java多线程编程的难点,往往开发者搞不清楚什么是竞争资源.什么时候需要考虑同步,怎么同步等等问 ...

  2. Java线程:线程的同步与锁

    一.同步问题提出 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏. 例如:两个线程ThreadA.ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据. public ...

  3. java 线程数据同步

    java 线程数据同步 由买票实例 //java线程实例 //线程数据同步 //卖票问题 //避免重复卖票 //线程 class xc1 implements Runnable{ //定义为静态,可以 ...

  4. Java多线程-线程的同步与锁

    一.同步问题提出 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏.例如:两个线程ThreadA.ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据. package ...

  5. C++ 11 线程的同步与互斥

    这次写的线程的同步与互斥,不依赖于任何系统,完全使用了C++11标准的新特性来写的,就连线程函数都用了C++11标准的lambda表达式. /* * thread_test.cpp * * Copyr ...

  6. C#线程间同步无法关闭

    用C#做了个线程间同步的小程序,但每次关闭窗口后进程仍然在,是什么原因? 解决方法: 要加一句 线程.IsBackground = true; 否则退出的只是窗体 上面的方法没看懂... MSDN上说 ...

  7. 线程间同步之 semaphore(信号量)

    原文地址:http://www.cnblogs.com/yuqilin/archive/2011/10/16/2214429.html semaphore 可用于进程间同步也可用于同一个进程间的线程同 ...

  8. Linux系统编程(29)——线程间同步(续篇)

    线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行.在pthread库中通过条件变 ...

  9. linux线程间同步方式汇总

    抽空做了下linux所有线程间同步方式的汇总(原生的),包含以下几个: 1, mutex 2, condition variable 3, reader-writer lock 4, spin loc ...

  10. C#之任务,线程和同步

    1 概述 对于所有需要等待 的操作,例 如 ,因 为文件 . 数据库或网络访 问都需要一定 的时间,此 时就可以启 动一个新线程,同时完成其他任务,即使是处理密集型的任务,线程也是有帮助的. 2 Pa ...

随机推荐

  1. 5、Selenium+Python自动登录163邮箱发送邮件

    1.Selenium实现自动化,需要定位元素,以下查看163邮箱的登录元素 (1)登录(定位到登录框,登录框是一个iframe,如果没有定位到iframe,是无法定位到账号框与密码框) 定位到邮箱框( ...

  2. 创建Task的多种方法

    Gradle的Project从本质上说只是含有多个Task的容器,一个Task与Ant的Target相似,表示一个逻辑上的执行单元. 我们可以通过多种方式定义Task,所有的Task都存放在Proje ...

  3. liferay-ui:search-container reset cur page 当点列排序时,把当前页号重置为1.

    问题描述: liferay里面要用liferay-ui:search-container 来展示结果集.并要求点列时,可以排序.然后,如果当前页数不为1时,点列排序,自动设置为1. 解决: // 列排 ...

  4. unix下网络编程之I/O复用(三)

    poll函数 在上文unix下网络编程之I/O复用(二)中已经介绍了select函数的相关使用,本文将介绍另一个常用的I/O复用函数poll.poll提供的功能与select类似,不过在处理流设备时, ...

  5. Azure VM开启资源监控

    目前China的Azure VM资源监控默认是不打开的.本文将介绍如何开启VM的监控功能. 一 Azure VM 打开Azure的Portal页面https://portal.azure.cn,登录后 ...

  6. laravel的批量插入

    在日常开发中,用到批量插入的操作还是挺多的.记得很早很早以前,我还是在循环中写sql插入,结果被项目经理按在地上摩擦.好吧,性能这东西,用不到的时候还好,万一性能成为瓶颈,那代码优化,数据库优化就首当 ...

  7. 对oracle中date/timestamp的操作

    设置oracle中date的会话格式为 'yyyy-mm-dd hh24:mi:ss' alter session set nls_date_format='yyyy-mm-dd hh24:mi:ss ...

  8. mybatis association表关联与rowbounds共同使用时的异常及其解决方案

    按照mybatis手册中所说的,association有两种实现方式,嵌套查询和嵌套结果映射.如手册中所述,select方式会带来N+1次查询的问题,考虑到效率问题的话建议使用嵌套结果映射.但是在结合 ...

  9. 四 Mixer

    Mixer在应用程序和基础架构后端之间提供通过中介层.它的设计将策略决策移出应用层,用运维人员能够控制的配置取而代之. Mixer的设计目的是改变层次之间的边界,以此降低总体复杂性.从服务代码中剔除策 ...

  10. linux 学习2 常用命令

    1.显示日期的指令: date 2.   [Tab]按键---具有『命令补全』不『档案补齐』的功能 3:  su和 sudo  su用于用户之间的切换.  su在不加任何参数,默认为切换到root用户 ...