浅谈.NET下的多线程和并行计算(三)线程同步基础上

简介: 其实,如果线程都是很独立的,不涉及到任何资源访问的,那么这些毫无干扰的线程不会产生什么问题。但是在实际应用中我们的线程总是涉及到资源访问的,而且往往涉及到共享资源的访问,那么就产生了线程同步的问题。一直觉得线程同步这个名词很奇怪,字面上看同步就是使得步调一致,线程同步是不是就是让线程步调一致的访问资源呢?事实上反了,线程同步恰巧是让线程不同时去访问资源而是去按照我们期望的顺序依次访问资源(是同步资源访问的行为而不是同步同时访问资源)。

其实,如果线程都是很独立的,不涉及到任何资源访问的,那么这些毫无干扰的线程不会产生什么问题。但是在实际应用中我们的线程总是涉及到资源访问的,而且往往涉及到共享资源的访问,那么就产生了线程同步的问题。一直觉得线程同步这个名词很奇怪,字面上看同步就是使得步调一致,线程同步是不是就是让线程步调一致的访问资源呢?事实上反了,线程同步恰巧是让线程不同时去访问资源而是去按照我们期望的顺序依次访问资源(是同步资源访问的行为而不是同步同时访问资源)。一句话,多个线程(不仅仅局限于相同进程)如果需要访问相同的可变资源的话就可能需要考虑到线程同步的手段。还有两个常见的名词是线程安全和线程冲突,所谓线程冲突就是由于多线程访问共享资源带来的问题,某个操作是线程安全就是表明这个操作没有线程冲突问题,要达到线程安全就要用线程同步的手段来解决。在MSDN类库中可以看到方法都注明了是不是线程安全的,如果不是那么我们在多线程程序总使用这个方法的话就要考虑是否要线程同步了。

既然要让线程的步调一致,那么我们首先可以想到的是,如果一个线程没有完成我们就等,一直等到它完成:

Stopwatch sw = Stopwatch.StartNew();
Thread t1 = new Thread(() =>
{
    Thread.Sleep(1000);
    result = 100;
});
t1.Start();
Thread.Sleep(500);
while (t1.IsAlive) ;
Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine(result);

假设线程在完成后会把结果写入result这么一个静态的变量,主线程在启动了新线程之后只花了500毫秒就做好了自己的事情,接下去一定要等待线程计算完成之后才能进行后续的操作,这个时候我们通过不断询问线程是不是还存在来得知线程是不是完成了计算,500毫秒后返回结果:

image

由于我们主线程是采用循环来等待的,对于处理器来说可不是等待,它足足运算了500毫秒,也就是浪费了500毫秒处理器资源。其实,我们可以在循环中让线程休眠一小段时间,让出处理器时间片,然后再去轮询检查,这样虽然不能在线程一完成后就得到结果,但是却节省了处理器资源:

Stopwatch sw = Stopwatch.StartNew();
Thread t1 = new Thread(() =>
{
    Thread.Sleep(1000);
    result = 100;
});
t1.Start();
Thread.Sleep(500);
while (t1.IsAlive)
    Thread.Sleep(500);
Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine(result);

结果如下:

image

除了轮询傻等还有一种方式就是使用Join来阻塞主线程一直到次线程的计算任务完成:

Stopwatch sw = Stopwatch.StartNew();
Thread t1 = new Thread(() =>
{
    Thread.Sleep(1000);
    result = 100;
});
t1.Start();
Thread.Sleep(500);
t1.Join();
Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine(result);

结果如下:

image

通过刚才两个例子我们再次回顾了Sleep和Join的用法,现在我们来看看采用锁机制进行同步的方法:

Stopwatch sw = Stopwatch.StartNew();
Thread t1 = new Thread(() =>
{
    lock (locker)
    {
        Thread.Sleep(1000);
        result = 100;
    }
});
t1.Start();
Thread.Sleep(100);
lock (locker)
{
    Console.WriteLine(sw.ElapsedMilliseconds);
    Console.WriteLine(result);
}

我想即使是ASP.NET应用,我们都经常会需要用到lock来锁定资源。IIS本身是一个多线程环境,如果在我们的应用程序中使用了全局的缓存,对于一个网站程序来说非常有可能在同一时间会有多个请求去修改这个缓存,在这个时候我们通常就会采用lock来锁定一个引用类型的对象。这个对象代表了锁的范围,同一时间只有一个进程能获取到这个锁对象,也就是其中包含的代码只能由一个线程同时执行。上面的例子中我们在开启线程后让主线程等待了一小段时间,然后对象就被锁定了,这个过程持续一秒,然后主线程在一秒后才能再次获得这个locker并输出结果:

image

在这个例子中我们的locker定义如下:

static object locker = new object();

有关lock之后会有更详细的讨论。之前我们看了等待和锁定的同步方法,不管怎么说这是一种被动的方法,是不是能让线程在完成之后主动通知其它等待的线程呢?答案是肯定的:

Stopwatch sw = Stopwatch.StartNew();
Thread t1 = new Thread(() =>
{
    Thread.Sleep(1000);
    result = 100;
    are.Set();
});
t1.Start();
are.WaitOne();
Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine(result);

来看看这样一段代码,暂缺不管are是什么对象,但是看这个逻辑可以理解到,主线程在启动了新线程后就开始等待了,然后线程在一秒后完成了结果之后,调用了Set(),这句话的意思就是说我完成了,所有等我的人可以继续了,are对象定义如下:

static EventWaitHandle are = new AutoResetEvent(false);

AutoResetEvent从名字上可以看出它是一个自动Reset的事件通知方式。我举一个例子,您可能一下子就明白了,我们可以把它比作地铁的闸机,插入了票,闸机放行一个人。AutoResetEvent的构造方法中传入的false代表,这个闸机默认是关闭的,只有调用了一次Set之后才能通过,如果您把它改为true的话可以看到主线程根本没有等待,直接输出了结果。所谓的Reset就是闸机关闭的操作,也就是说对于AutoResetEvent,一个人用一个票过去之后闸机就关闭了,除非还有后面的人插入了票(Set)。这个程序的运行结果如下:

image

和AutoResetEvent对应的还有一个ManualResetEvent

static EventWaitHandle mre = new ManualResetEvent(false);

它们唯一的区别就是这个闸机在插入一张票之后还是可以通行,除非有人手动去设置了一下Reset来关闭闸机,我们来看一个简单的例子:

Stopwatch sw = Stopwatch.StartNew();
Thread t1 = new Thread(() =>
{
    Thread.Sleep(1000);
    result = 100;
    mre.Set();
});
Thread t2 = new Thread(() =>
{
    mre.WaitOne();
    Console.WriteLine("get result" + result + " and write to file");
});
t1.Start();
t2.Start();
mre.WaitOne();
Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine(result);

比如说我们开一个线程来计算,不但主线程等待这个结果来显示给用户,另外一个线程也等待这个结果来写入日志文件,这样我们就可以利用ManualResetEvent的特性,由于t2或主线程在通过闸机之后并不会导致它自动关闭,所以两者都可以通过,输出结果如下:

image

前面说过,AutoResetEvent像一个地铁闸机,一票一人,那么我们来看一个更形象的例子:

are.Set();
for (int i = 0; i < 10; i++)
{
    new Thread(() =>
        {
            are.WaitOne();
            Console.WriteLine(DateTime.Now.ToString("mm:ss"));
            Thread.Sleep(1000);
            are.Set();
        }).Start();
}

我们先调用了AutoResetEvent的Set方法来让闸机默认是可以通行的。然后开10个线程,排队等待进入,如果能进入了,花一秒的时间来通行然后关闭闸机,程序输出结果如下:

image 

一秒过一个人,就像我们预期的那样。ManualResetEvent在闸机过人之后不会自动关闭,这就相当于很多旅游景点的团队检票方式,导游把100张团队票交给检票人员,检票人员数了人数之后一批放行,然后关闭闸机(散客的话就用AutoResetEvent咯),想一下我们怎么写程序来模拟这种批量进入闸机的方式:

image

首先要使用一个变量来统计进入的人数,达到一定人数之后放行:

static int group = 0;

然后我们来分析一下这段程序:

for (int i = 0; i < 10; i++)
{
    new Thread(() =>
    {
        lock (locker)
        {
            Thread.Sleep(1000); // 一秒来一个人
            group++;
            if (group == 5) // 好了,旅游团5人到齐
            {
                mre.Set(); // 检票员说你们进去吧
                are.Set(); // 检票员打开闸机
                group = 0;
            }
        }
        mre.WaitOne(); // 大家等旅游团到齐(等检票员数人数)
        Console.WriteLine(DateTime.Now.ToString("mm:ss")); // 进去了,团体进入时间
        Thread.Sleep(100); // 让大家都走完
        mre.Reset(); // 进去之后检票员停止放行
        are.WaitOne(); // 第二波人等闸机再次打开
    }).Start();
}

这个例子使用了ManualResetEvent和AutoResetEvent模拟这种团体检票的情景,不过这里有一个很有趣的问题,不知您有没有想过,在mre.Reset()之前我们让线程休眠了100毫秒,目的是为了让阻塞的线程都得到恢复然后输出当前的时间。如果我们注释这句话的话就发现很有可能在5个线程没有全部恢复的情况下就有某一个线程先调用了mre.Reset()导致闸机关闭,那么这个情况就很难预测了。这相当于在同步线程时出现的线程同步问题。其实.NET内置了另外一个结构来实现这种操作:

static Semaphore s = new Semaphore(2, 2);
for (int i = 0; i < 10; i++)
{
    new Thread(() =>
    {
        s.WaitOne();
        Thread.Sleep(1000);
        Console.WriteLine(DateTime.Now.ToString("mm:ss"));
        s.Release();
    }).Start();
}

结果如下:

image 

本节我们走马观花看了很多同步结构,下节继续来讨论同步的一些基础结构。

作者: lovecindywang
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
9天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
21天前
|
存储 缓存 NoSQL
Redis单线程已经很快了6.0引入多线程
Redis单线程已经很快了6.0引入多线程
31 3
|
23天前
|
消息中间件 安全 Linux
线程同步与IPC:单进程多线程环境下的选择与权衡
线程同步与IPC:单进程多线程环境下的选择与权衡
57 0
|
1月前
|
Java 调度 C#
C#学习系列相关之多线程(一)----常用多线程方法总结
C#学习系列相关之多线程(一)----常用多线程方法总结
|
1月前
|
安全 编译器 C#
C#学习相关系列之多线程---lock线程锁的用法
C#学习相关系列之多线程---lock线程锁的用法
|
1月前
|
Java C#
C#学习系列相关之多线程(五)----线程池ThreadPool用法
C#学习系列相关之多线程(五)----线程池ThreadPool用法
|
1月前
|
存储 安全 Java
深入理解 Java 多线程、Lambda 表达式及线程安全最佳实践
线程使程序能够通过同时执行多个任务而更有效地运行。 线程可用于在不中断主程序的情况下在后台执行复杂的任务。 创建线程 有两种创建线程的方式。 扩展Thread类 可以通过扩展Thread类并覆盖其run()方法来创建线程:
103 1
深入理解 Java 多线程、Lambda 表达式及线程安全最佳实践
|
1月前
|
并行计算 安全 Java
C# .NET面试系列四:多线程
<h2>多线程 #### 1. 根据线程安全的相关知识,分析以下代码,当调用 test 方法时 i > 10 时是否会引起死锁? 并简要说明理由。 ```c# public void test(int i) { lock(this) { if (i > 10) { i--; test(i); } } } ``` 在给定的代码中,不会发生死锁。死锁通常是由于两个或多个线程互相等待对方释放锁而无法继续执行的情况。在这个代码中,只有一个线程持有锁,且没有其他线程参与,因此不
102 3
|
1月前
|
数据采集 存储 Java
「多线程大杀器」Python并发编程利器:ThreadPoolExecutor,让你一次性轻松开启多个线程,秒杀大量任务!
「多线程大杀器」Python并发编程利器:ThreadPoolExecutor,让你一次性轻松开启多个线程,秒杀大量任务!
|
16天前
|
安全 Java 容器
Java并发编程:实现高效、线程安全的多线程应用
综上所述,Java并发编程需要注意线程安全、可见性、性能等方面的问题。合理使用线程池、同步机制、并发容器等工具,可以实现高效且线程安全的多线程应用。
14 1