18910140161

C#线程同步的几种方法

顺晟科技

2021-06-16 10:37:52

429

我们在编程的时候,有时候会用多线程来解决问题。比如你的程序需要在后台处理大量数据,但也需要使用户界面具有可操作性;或者您的程序需要访问一些外部资源,如数据库或网络文件。您可以创建一个子线程来处理这些情况。但是多线程必然会带来一个问题,就是线程同步的问题。如果这个问题处理不好,我们会得到一些意想不到的结果。

我也在网上看过一些关于线程同步的文章。其实线程同步有几种方法,下面我简单总结一下。

首先,易变关键字

Volatile是最简单的同步方法,当然简单是有代价的。只能在可变级别同步。volatile意味着告诉处理器不要把我放在工作内存中,而是直接在主内存中操作我。(【转自www.bitsCN.com】)因此,当多个线程同时访问变量时,会直接操作主存,从而在本质上共享变量。

可以确定为易变的必须是以下类型:(摘自MSDN)

任何引用类型。

任何指针类型(在不安全的上下文中)。

类型有字节、字节、短字节、短字节、int、uint、char、float、bool。

枚举类型,其枚举基本类型为字节、字节、短字节、短字节、整数或整数。

例如:

复制代码

公共甲级

{

private volatile int _ I;

公共国际

{

get { return _ I;}

set {_ I=value;}

}

}

复制代码

但是volatile无法实现真正的同步,因为它的操作级只停留在变量级,而不是原子级。如果是在单处理器系统中,没有问题。变量在主存中没有被别人修改的机会,因为只有一个处理器,这叫处理器自洽。但是在多处理器系统中,可能会有问题。每个处理器都有自己的数据缓存,更新后的数据可能不会立即写回主存。所以可能会造成异步,但是很难发生,因为cach的读写速度相当快,刷新的频率也相当高,只能发生在压力测试的时候,概率非常非常小。

二、锁定关键词

锁是一种简单易行的线程同步方法,它通过获取给定对象的互斥锁来实现同步。它可以保证当一个线程在临界代码段时,另一个线程不会进来,只能等到那个线程对象被释放,也就是说线程出了临界区。用法:

复制代码

公共无效函数()

{

对象锁定此=新对象();

锁定(锁定此)

{

//访问线程敏感资源。

}

}

复制代码

lock的参数必须是基于引用类型的对象,而不是像bool、int这样根本不能同步的基本类型。原因是锁的参数是对象。如果传入int,必然会发生打包操作,所以每次锁的时候都是一个新的不同的对象。更好避免使用不受程序控制的公共类型或对象实例,因为这可能会导致死锁。特别是不要用string作为lock的参数,因为string是由CLR“持久化”的,也就是说整个应用程序中只有一个给定字符串的实例,所以更容易造成死锁。建议使用未“驻留”的私有或受保护成员作为参数。实际上,有些类提供了专门用于被锁定的成员,例如,数组类型提供了同步根,许多其他集合类型也提供了同步根。

因此,使用锁应注意以下几点:

1.如果一个类的实例是公共的,更好不要锁定(这个)。因为使用您的类的人可能不知道您使用了锁,如果他新建了一个实例并锁定了它,很容易造成死锁。

2.如果我的类型是公共的,不要锁定(类型为(我的类型))

3.永远不要锁住一根绳子

三.系统。穿线。互锁

对于整数数据类型的简单操作,线程同步可以通过系统中存在的互锁类的成员来实现。线程命名空间。互锁类有以下方法:递增、递减、交换和比较交换。使用增量和减量可以确保整数的加减是一个原子操作。交换方法自动交换指定变量的值。CompareExchange方法结合了两种操作:比较两个值,并根据比较结果在其中一个变量中存储第三个值。比较和交换操作也作为原子操作来执行。例如:

复制代码

int I=0;

系统。螺纹。互锁。增量(参考I);

控制台。write line(I);

系统。穿线。互锁。减量(参考I);

控制台。write line(I);

系统。穿线。互锁。交换(参考文献100);

控制台。write line(I);

系统。螺纹。互锁。比较交换(参考文献I,10,100);

复制代码

输出:

四.班长

Monitor类提供了类似锁的功能,但不同于lock,它可以更好地控制同步块。当调用Monitor的Enter(Object o)方法时,它将获得o的独占权,并且直到调用Exit(Object o)方法时才会释放o的独占权。可以多次调用输入(对象0)方法。只需要调用Exit(Object o)方法相同的次数,Monitor类还提供了一个重载的TryEnter(Object o,[int])方法,试图获取o对象的独占权限,独占权限获取失败返回false。

但是,使用锁通常比直接使用监视器更好,一方面是因为锁更简单,另一方面,锁确保即使受保护的代码引发异常,底层监视器也可以被释放。这是通过最后调用Exit来实现的。事实上,锁是用监视器类实现的。以下两段代码是等效的:

复制代码

锁(x)

{

dosomesing();

}

同族的

object obj=(object)x;

系统。线程化. monitor . enter(obj);

尝试

{

dosomesing();

}

最后

{

系统。线程化. monitor . exit(obj);

}

复制代码

有关用法,请参考以下代码:

查看代码

当线程1获得m _ monitorcoid对象的独占权限时,线程2尝试调用try enter(m _ monitorcoid),此时会因为无法获得独占权限而返回false,输出信息如下:

此外,Monitor还提供了三种静态方法:Monitor。脉搏(对象0),监视器。对象和监视器。等待(对象0),用于实现唤醒机制的同步。至于这三种方法的用法,可以参考MSDN,这里就不详细描述了。

动词(verb的缩写)互斥(体)…

在使用上,Mutex类似于上面提到的Monitor,但是Mutex不具备Wait、Pulse、PulseAll的功能,所以我们不能用Mutex来实现类似的唤醒功能。但是mutex有一个很大的特点,因为Mutex是跨进程的,所以我们可以在同一台机器甚至远程机器的多个进程上使用同一个Mutex。虽然Mutex也可以实现进程内线程同步,而且功能更强大,但是在这种情况下,建议使用Monitor,因为Mutex类是win32封装的,所以需要的互操作性转换比较耗费资源。

不及物动词读写锁

在考虑资源访问时,我们会在惯性中实现对资源的锁定机制,但在某些情况下,我们只需要读取资源的数据,而不需要修改资源的数据。在这种情况下,获得资源的独占权无疑会影响运营效率。因此,Net提供了一种机制。使用ReaderWriterLock访问资源时,如果资源在某个时刻没有获得写的独占权限,那么就可以获得多次读的访问权限和单次写的独占权限。如果在某个时间获得了写入的独占权限,则其他读取的访问权限必须等待。请参考以下代码:

复制代码

私有静态reader writerlock m _ reader writerlock=new reader writerlock();

私有静态int m _ int=0;

[STAThread]

静态void Main(字符串[]参数)

{

线程读取线程=新线程(新线程启动(读取));

readThread。名称=' ReadThread1

线程读取线程2=新线程(新线程启动(读取));

readThread2。名称=' ReadThread2

Thread writeThread=new Thread(new Thread start(Writer));

writeThread。Name=' WriterThread

readThread。start();

readThread2。start();

writeThread。start();

readThread。join();

readThread2。join();

writeThread。join();

控制台。ReadLine();

}

私有静态无效读取()

{

while (true)

{

控制台。写线('线程名'线程。current thread . Name ' acquidateaderlock ');

m_readerWriterLock。收购方锁定(10000);

控制台。写线(字符串。格式(' ThreadName : { 0 } m _ int : { 1 } ',线程。CurrentThread.Name,m _ int));

m_readerWriterLock。ReleaseReaderLock();

}

}

私有静态void Writer()

{

while (true)

{

控制台。写线('线程名'线程。current thread . Name ' acquire writerlock ');

m_readerWriterLock。acquire writerlock(1000);

互锁。增量(ref m _ int);

线程。睡眠(5000);

m_readerWriterLock。release writerlock();

控制台。写线('线程名'线程。current thread . Name ' release writerlock ');

}

}

复制代码

在程序中,我们启动两个线程获取m_int的读访问权限,用一个线程获取m_int的写独占权限。执行代码后,输出如下:

可以看出,WriterThread获得写独占权限后,任何其他读线程都必须等到WriterThread释放写独占权限后才能获得数据访问权限。需要注意的是,上面的打印信息清楚的显示了多个线程可以同时获得数据读取权限,从ReadThread1和ReadThread2的信息交互输出可以看出。

七、同步属性

当我们确定一个类的实例在同一时间只能被一个线程访问时,我们可以直接将该类标识为同步的,这样CLR就会自动为该类实现同步机制。其实这涉及到同步域的概念。当类按如下方式设计时,我们可以确保该类的实例不能被多个线程同时访问。

1).添加系统。类声明中的runtime . remoting . contexts . synchronizationattribute属性。

2).继承到系统。ContextBoundObject

需要注意的是,要实现上述机制,类必须从System继承。换句话说,类必须是上下文绑定的。

下面是一个示例类代码:

【系统。运行时。远程处理。上下文。同步]

公共类同步类:系统。ContextBoundObject

{

}

八、MethodImplAttribute

如果关键部分跨越整个方法,即需要锁定整个方法内部的代码,那么使用MethodImplAttribute属性更容易。这样就不需要在方法内部加锁,只需要添加[methodimpl (MethodImplOptions。同步)]。mehtodimpl和methodimploptions都在名称空间系统中。runtime.compilerservices.但是,请注意,该属性将锁定整个方法,直到该方法返回。所以使用起来不灵活。如果您想提前释放锁,您应该使用监视器或锁。让我们看一个例子:

查看代码

这里我们有两个方法,我们可以比较一下,一个是DoSomeWorkSync()带属性MethodImpl,一个是DoSomeWorkNoSync()不带。方法中的Sleep(1000)是为了让第二个线程有足够的时间进入,而个线程仍然在方法中。每个方法有两个线程。我们先来看看结果:

可以看到,对于线程1和2,也就是调用方法的线程没有属性,线程2进入方法的时候还没有离开,但是线程1进来了,也就是说方法没有同步。我们再来看看线程3和4。当线程3进入时,方法被锁定,直到线程3释放锁定,线程4才进入。

九、同步事件和等待句柄

Lock和Monitor在线程同步中可以起到很好的作用,但是不能在线程之间传递事件。如果您想要同步线程并相互交互,您需要使用同步事件。同步事件是具有两种状态(已终止和未终止)的对象,可用于激活和挂起线程。

有两种同步事件:自动设置事件和手动重置事件。它们之间的区别是线程被激活后状态是否自动从终止变为不终止。自动设置事件自动变为不终止,这意味着自动设置事件只能激活一个线程。手动重置事件在调用其重置方法之前不会变为非终止状态。在此之前,手动重置事件可以激活任意数量的线程。

您可以调用WaitOne、WaitAny或WaitAll来使线程等待事件。他们之间的差异可以在MSDN找到。当事件的Set方法被调用时,事件将被终止,等待的线程将被唤醒。

我们来看一个例子。这个例子发生在MSDN。因为事件仅用于激活一个线程,所以您可以使用自动设置事件或手动设置事件类。

查看代码

我们先来看看输出:

在主函数中,首先创建一个自动设置事件的实例。参数false表示初始状态未终止,如果为true,则初始状态终止。然后创建并启动一个子线程,在子线程中,通过调用AutoResetEvent的WaitOne方法,使子线程等待指定的事件。然后等待一秒钟后,主线程调用AutoResetEvent的set方法,将状态从未终止改为终止,重新激活子线程。

我们已经准备好了,你呢?
2024我们与您携手共赢,为您的企业形象保驾护航