欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

.NET进阶篇06-async异步、thread多线程4

程序员文章站 2023-11-10 22:16:16
知识需要不断积累、总结和沉淀,思考和写作是成长的催化剂 * 一、锁1、lock2、Interlocked3、Monitor4、SpinLock5、Mutex6、Semaphore7、Events1、AutoResetEvent2、ManualResetEvent3、ManualResetEvent ......

知识需要不断积累、总结和沉淀,思考和写作是成长的催化剂

*

2、interlocked3、monitor4、spinlock5、mutex6、semaphore7、events1、autoresetevent2、manualresetevent3、manualreseteventslim8、readerwriterlock1、同步编程模型spm2、异步编程模型apm3、基于事件编程模型eap4、基于任务编程模型tap四、end

一、锁

数据库中也有锁概念,行锁,表锁,事物锁等,锁的作用就是控制并发情况下数据的安全一致,使一个数据被操作时,其他并发线程等待。开发方面多线程并行编程访问共享数据时,为保证数据的一致安全,有时需要使用锁来锁定对象来达到同步

.net中提供很多线程同步技术。有lock,interlocked,monitor等用于进程内同步锁,mutex互斥锁,semaphore信号量,events,readerwriterlockslim读写锁等用于多个进程间的线程同步

1、lock

lock语句是设置对锁定和解除锁定的一种简单方式,也是最常用的一种同步方式。lock用于锁定一个引用类型字段,当线程执行到lock处,会锁定该字段,使之只有一个线程进入lock语句块内,才lock语句结束位置再释放锁定,另一个线程才可以进入。原理运用同步块索引,感兴趣可以研究下

lock (obj)
{
    //synchronized region
}

因为只有一个线程可以进去,没有并发,所以牺牲了性能,所以要尽量缩小lock的范围,另一个建议是首选锁一个私有变量,也就是syncroot模式,声明一个syncroot的私有object变量来进行锁定,而不是使用lock(this),因为外面调用者也可能锁定你这个对象的实例,但他并不知道你内部也使用了锁,所以容易造成死锁

private object syscroot = new object();
public void dothis()
{
    lock (syscroot)
    {
        //同一个时间只有一个线程能到达这里
    }
}

2、interlocked

interloacked用于将变量的一些简单操作原子化,也就是线程安全同步。我们常写的i++就不是线程安全的,从内存中取值然后+1然后放回内存中,过程中很可能被其他线程打断,比如在你+1后放回内存时,另一个线程已经先放回去了,也就不同步了。inerlocked类提供了以线程安全的方式递增、递减、交换、读取值的方法
比如以下代替lock的递增方式

int num = 0;
//lock (syscroot)
//{
//    num++;
//}
num = interlocked.increment(ref num);

3、monitor

上面lock就是monitor的语法糖,通过编译器编译会生成monitor的代码,像下面这样

lock (syscroot)
{
    //synchronized region
}
//上面的lock锁等同于下面monitor
monitor.enter(syscroot);
try
{
    //synchronized region
}
finally
{
    monitor.exit(syscroot);
}

monitor不同于lock就是它还可以设置超时时间,不会无限制的等待下去。

bool locktaken = false;
monitor.tryenter(syscroot,500,ref locktaken);
if (locktaken)
{
    try
    {
        //synchronized region
    }
    finally
    {
        monitor.exit(syscroot);
    }
}
else
{
}

4、spinlock

spinlock自旋锁是一种用户模式锁。对了,插一嘴锁分为内核模式锁和用户模式锁,内核模式就是在系统级别让线程中断,收到信号时再切回来继续干活,用户模式就是通过一些cpu指定或则死循环让线程一直运行着直到可用。各有优缺点吧,内核cpu资源利用率高,但切换损耗,用户模式就相反,如果锁定时间较长,就会白白循环等待,后面就有混合模式锁的出现了

如果有大量的锁定,且锁定时间非常短,spinlock就很有用,用法和monitor类似,enter或tryenter获取锁,exit释放锁。isheld和isheldbycurrentthread指定它当前是否锁定

另外spinlock是个结构类型,所以注意拷贝赋值时会创建全新副本问题。必要时可按引用来传递

5、mutex

mutex互斥锁提供跨多个进程同步一个类,定义互斥锁的时候可以指定互斥锁的名称,这样系统能够识别,所以在另一个进程中定义的互斥,其他进程也是可以访问到的,mutex.openexisting()便可以得到。

bool creatednew = false;
mutex mutex = new mutex(false, "procharpmutex", out creatednew);
if (mutex.waitone())
{
    try
    {
        //synchronized region
    }
    finally
    {
        mutex.releasemutex();
    }
}

介于此我们可以用来禁止一个应用程序启动两次,一般我们通过进程的名称来判断,这里我们使用mutex实现

bool creatednew = false;
mutex mutex = new mutex(false, "singletonwinappmutex", out creatednew);
if (!creatednew)
{
    messagebox.show("应用程序已经启动过了");
    application.exit();
    return;
}

6、semaphore

semaphore信号量和互斥类似,区别是,信号量可以同时让多个线程使用,是一种计数的互斥锁定。通过计数允许同时有几个线程访问受保护的资源。也可以指定信号量名称以使在多个进程间共享

semaphore和上面mutex都是继承自waithandle基类,waithandle用于等待一个信号的设置,嗲用wait,线程会等待接收一个与等待句柄相关的信号

semaphoreslim是对semaphore的轻量替代版本(它不继承waithandle),semaphoreslim(int initialcount, int maxcount)构造函数可指定最大并发个数,然后在线程内通过semaphoreslim的wait等到直到来接收信号是否可以进去受保护代码块了,最后记得要release,不然下一个线程获取不到准许进入的信号

7、events

events事件锁不同于委托中的事件,在system.threading命名空间下,用于系统范围内的事件资源的同步,有autoresetevent自动事件锁、manualresetevent手动事件锁以及轻量版本manualreseteventslim

1、autoresetevent

autoresetevent也是继承自waithandle类的,也是通过waitone来等待直到有信号,它有两种状态:终止和非终止,可以调用set和reset方法使对象进入终止和非终止状态。通俗点就是set有信号,另一个线程可以进入了,reset非终止无信息,其他线程就阻塞了。自动的意思就是一个线程进入了,自动reset设置无信号了其他线程就进不去了。类似现实中的汽车收费口,一杆一车模式

private autoresetevent autoevent = new autoresetevent(false);
public void dothis()
{
    autoevent.waitone();
    //执行同步代码块
    autoevent.set();
}
.NET进阶篇06-async异步、thread多线程4
2、manualresetevent

手动事件锁和自动的区别在于,手动事件锁没有信号时会阻塞一批线程的,有信号时,所有线程都运行,同时唤醒多个线程,除非手动reset再阻塞,类似现实场景中火车道路口的栅栏,落杆拦截一批人,起杆则一批人蜂拥通过,用法和上面一样,waitone等待信号,结束时通过set来通知有信号了,可以通过了

.NET进阶篇06-async异步、thread多线程4

3、manualreseteventslim

manualreseteventslim通过封装 manualresetevent提供了自旋等待和内核等待的混合锁模式。如果需要跨进程或者跨appdomain的同步,那么就必须使用manualresetevent。manualreseteventslim使用wait来阻塞线程,支持任务的取消。和semaphoreslim的wait一样,内部先通过用户模式自旋然后再通过内核模式效率更高

8、readerwriterlock

readerwriterlock读写锁不是从限定线程个数的角度来保护资源,而是按读写角度来区分,就是你可以锁定当某一类线程(写线程)中一个进入受保护资源时,另一类线程(读线程)全部阻塞。如果没有写入线程锁定资源,就允许多个读取线程方法资源,但只能有一个写入线程锁定该资源

具体用法参考示例

// 创建读写锁
readerwriterlock rwlock = new readerwriterlock();
// 当前线程获取读锁,参数为:超时值(毫秒)
rwlock.acquirereaderlock(250);
// 判断当前线程是否持有读锁
if (!rwlock.isreaderlockheld)
{
    return;
}
console.writeline("拿到了读锁......");
// 将读锁升级为写锁,锁参数为:超时值(毫秒)
lockcookie cookie = rwlock.upgradetowriterlock(250);
// 判断当前线程是否持有写锁
if (rwlock.iswriterlockheld)
{
    console.writeline("升级到了写锁......");
    // 将锁还原到之前所的级别,也就是读锁
    rwlock.downgradefromwriterlock(ref cookie);
}
// 释放读锁(减少锁计数,直到计数达到零时,锁被释放)
rwlock.releasereaderlock();
console.writeline("顺利执行完毕......");

// 当前线程获取写锁,参数为:超时值(毫秒)
rwlock.acquirewriterlock(250);
// 判断当前线程是否持有写锁
if (rwlock.iswriterlockheld)
{
    console.writeline("拿到了写锁......");
    // 释放写锁(将减少写锁计数,直到计数变为零,释放锁)
    rwlock.releasewriterlock();
}
// 释放写锁(将减少写锁计数,直到计数变为零,释放锁)
// 当前线程不持有锁,会抛出异常
rwlock.releasewriterlock();
console.writeline("顺利执行完毕......");
console.readline();

readerwriterlockslim同样是readerwriterlock的轻量优化版本,简化了递归、升级和降级锁定状态的规则。
1. enterwritelock 进入写模式锁定状态
2. enterreadlock 进入读模式锁定状态
3. enterupgradeablereadlock 进入可升级的读模式锁定状态
并且三种锁定模式都有超时机制、对应 try… 方法,退出相应的模式则使用 exit… 方法,而且所有的方法都必须是成对出现的

二、线程安全集合

并行环境下修改共享变量为了保证资源安全,通常使用上面介绍的锁或信号量来解决此问题。其实.net也内置了一些线程安全的集合,使用他们就像使用单线程集合一样。

类型 描述
blockingcollection 提供针对实现 iproducerconsumercollection 的任何类型的限制和阻塞功能。 有关详细信息,请参阅blockingcollection 概述。
concurrentdictionary<tkey,tvalue style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;"> 键/值对字典的线程安全实现。
concurrentqueue fifo(先进先出)队列的线程安全实现。
concurrentstack lifo(后进先出)堆栈的线程安全实现。
concurrentbag 无序的元素集合的线程安全实现。
iproducerconsumercollection 类型必须实现以在 blockingcollection 中使用的接口。

三、多线程模型

1、同步编程模型spm

2、异步编程模型apm

我们常见的xxbegin, xxend这两个经典的配对方法就是异步的,begin后会委托给线程池调用一个线程去执行。还有委托的begininvoke调用

filestream fs = new filestream("d:\\test.txt", filemode.open);
var bytes = new byte[fs.length];
fs.beginread(bytes, 0, bytes.length, (aysc) =>
{
    var num = fs.endread(aysc);
}, string.empty);

3、基于事件编程模型eap

winfrom/wpf开发中的backgroundworker类就是异步事件模式的一种实现方案,runworkerasync方法启动与dowork事件异步关联的方法,工作完成后,就触发runworkercompleted事件,也支持cancelaysnc方法取消以及reportprogress通知进度等。还又一个典型的就是webclient

webclient client = new webclient();
client.downloaddatacompleted += (sender,e)=> 
{
};
client.downloaddataasync(new uri("https://www.baidu.com/"));

4、基于任务编程模型tap

task出来后,微软就大力推广基于task的异步编程模型,apm和eap都被包装成task使用。下面示例简单用task封装上面的编程模型。webclient的downloaddatataskasync实现和示例中的类似,利用一个taskcompletionsource包装器包装成task

filestream fs = new filestream("d:\\test.txt", filemode.open);
var bytes = new byte[fs.length];
var task = task.factory.fromasync(fs.beginread, fs.endread, bytes, 0, bytes.length, string.empty);
var nums = task.result;

action action = () =>{ };
var task = task.factory.fromasync(action.begininvoke, action.endinvoke, string.empty);

public static task<int> gettaskasuc(string url)
{
    taskcompletionsource<int> source = new taskcompletionsource<int>();//包装器
    webclient client = new webclient();
    client.downloaddatacompleted += (sender, e) =>
    {
        try
        {
            source.trysetresult(e.result.length);
        }
        catch (exception ex)
        {
            source.trysetexception(ex);
        }
    };
    client.downloaddataasync(new uri(url));
    return source.task;
}

四、end

最近几篇介绍了如何编写多线程和多任务应用程序。在应用程序开发过程中要仔细规划,太多的线程导致资源问题,太少则起不到大效果。多线程编程中一个中肯的建议就是
尽量避免修改共享变量,使同步的要求变低。通过合理规划可以减少大部分的同步复杂度。

search the fucking web
read the fucking maunal

——goodgoodstudy