C# 多线程小试牛刀

前言

昨天在上班时浏览博问,发现了一个问题,虽然自己在 C# 多线程上没有怎么尝试过,看了几遍 CLR 中关于 线程的概念和讲解(后面三章)。也想拿来实践实践。问题定义是这样的:

对于多线程不是很懂,面试的时候遇到一个多线程的题,不会做,分享出来,懂的大佬指点一下,谢谢

建一个winform窗体,在窗体中放上一个开始按钮,一个停止按钮,一个文本框,在窗体中声明一个List类型的属性,点击开始按钮后开启10个线程,所有线程同时不间断的给List集合中添加1-10000之间的随机数,要求添加List集合中的数字不能重复,并且实时在文本框中显示集合的长度,当集合List的长度等于1000时自动停止所有线程,如果中途点击停止按钮也停止所有线程,点击开始又继续执行。

我其实没有完全实现了这位博问中提问的同学的需求,具体问题的来源可查看该地址 问题来源

开始尝试

刚拿到这个需求的时候,映入我脑海里的是 Task, Threadpool,Concurrent,和 Lock 等概念,接下来就是组装和编码的过程了,首先理一理头绪,

  • 生成随机数
  • 插入到 List 中,且不能重复
  • 开启多个线程同时插入。

首先是生成 随机数,使用 System.Random 类来生成伪随机数(这个其实性能和效率贼低,后面再叙述)

private int GenerateInt32Num()
{
    var num = random.Next(0, TOTAL_NUM);
    return num;
}

然后是插入到 List<Int32> 中的代码,判断是否 已经达到了 我们需要的 List 长度,如果已满足,则退出程序。

private void AddToList(int num)
{
    if (numList.Count == ENDNUM)
    {
        return;
    }

    numList.Add(num);
}

如果是个 单线程的,按照上面那样 while(true) 然后一直插入即可,可这个是个 多线程,那么需要如何处理呢?

我思考了一下,想到了之前在 CLR 中学到的 可以用 CancellationTokenSource 中的 Cancel 来通知 Task 来取消操作。所以现在的逻辑是,用线程池来实现多线程。然后传入 CancellationTokenSource.Token 来取消任务。

最后用 Task.WhanAny() 来获取到第一个到达此 Task 的 ID。

首先是建立 Task[] 的数组

internal void DoTheCompeteSecond()
{
    Task[] tasks = new Task[10];

    for (int i = 0; i < 10; ++i)
    {
        int num = i;
        tasks[i] = Task.Factory.StartNew(() => AddNumToList(num, cts), cts.Token);
    }

    Task.WaitAny(tasks);
}

然后 AddNumToList 方法是这样定义的,

private void AddNumToList(object state, CancellationTokenSource cts)
{-
    Console.WriteLine("This is the {0} thread,Current ThreadId={1}",
                      state,
                      Thread.CurrentThread.ManagedThreadId);

    while (!cts.Token.IsCancellationRequested)
    {
        if (GetTheListCount() == ENDNUM)
        {
            cts.Cancel();
            Console.WriteLine("Current Thread Id={0},Current Count={1}",
                              Thread.CurrentThread.ManagedThreadId,
                              GetTheListCount());

            break;
        }
        var insertNum = GenerateInt32Num();
        if (numList.Contains(insertNum))
        {
            insertNum = GenerateInt32Num();
        }

        AddToList(insertNum);
    }
}

看起来是没有什么问题的,运行了一下。得到了如下结果,

这应该是昨晚运行时得到的数据,当时也没有多想,就贴了上去,回答了那位提问同学的问题。但是心里有一个疑惑,为什么会同时由 两个 Thread 同时达到了该目标呢?

发现问题

今天早上到公司时,我又打开了这个 代码,发现确实有点不对劲,于是就和我边上 做 Go 语言开发的同学,问了问他,哪里出现了问题,他和我说:“你加了读写锁了吗?” 你这里有数据脏读写。心里面有了点眉目。

按照他说的,修改了一下AddToList 里面的逻辑,这时候,确实解决了上面的问题,

private void AddToList(int num)
{
    rwls.EnterReadLock();
    if (numList.Count == ENDNUM)
        return;
    rwls.ExitReadLock();

    rwls.EnterWriteLock();
    numList.Add(num);
    rwls.ExitWriteLock();
}

得到的结果如下:

完整的代码如下所示:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace CSharpFundamental
{
    class MultipleThreadCompete
    {
        List<int> numList = new List<int>();
        Random random = new Random();
        CancellationTokenSource cts = new CancellationTokenSource();
        private const int ENDNUM = 1000000;

        ReaderWriterLockSlim rwls = new ReaderWriterLockSlim();

        internal void DoTheCompeteSecond()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            Task[] tasks = new Task[100];

            for (int i = 0; i < 100; ++i)
            {
                int num = i;
                tasks[i] = Task.Run(() => AddNumToList(num, cts), cts.Token);
            }

            Task.WaitAny(tasks);

            Console.WriteLine("ExecuteTime={0}", sw.ElapsedMilliseconds / 1000);
        }

        private int GetTheListCount()
        {
            return numList.Count;
        }

        private void AddToList(int num)
        {
            rwls.EnterReadLock();
            if (numList.Count == ENDNUM)
                return;
            rwls.ExitReadLock();

            rwls.EnterWriteLock();
            numList.Add(num);
            rwls.ExitWriteLock();
        }

        private void AddNumToList(object state, CancellationTokenSource cts)
        {
            Console.WriteLine("This is the {0} thread,Current ThreadId={1}",
                state,
                Thread.CurrentThread.ManagedThreadId);

            while (!cts.Token.IsCancellationRequested)
            {
                try
                {
                    rwls.EnterReadLock();
                    if (numList.Count == ENDNUM)
                    {
                        cts.Cancel();
                        Console.WriteLine("Current Thread Id={0},Current Count={1}",
                            Thread.CurrentThread.ManagedThreadId,
                            GetTheListCount());
                        break;
                    }
                }
                finally
                {
                    rwls.ExitReadLock();
                }

                var insertNum = GenerateInt32Num();
                if (numList.Contains(insertNum))
                {
                    insertNum = GenerateInt32Num();
                }

                AddToList(insertNum);
            }
        }

        private int GenerateInt32Num()
        {
            return random.Next(1, ENDNUM);
        }
    }
}

这时候,那位 Go 语言的同学和我说,我们试试 1000w 的数据插入,看看需要多少时间?于是我让他用 Go 语言实现了一下上面的逻辑,1000w数据用了 三分钟,我让他看看总共生成了多少随机数,他查看了一下生成了 1亿4千多万的数据。

最开始我用上面的代码来测,发现我插入 1000w 的数据,CPU 到100% 而且花了挺长时间,程序根本没反应,查看了一下我判断重复的语句numList.Contains()

底层实现的代码为:

[__DynamicallyInvokable]
    public bool Contains(T item)
    {
        if ((object) item == null)
        {
            for (int index = 0; index < this._size; ++index)
            {
                if ((object) this._items[index] == null)
                    return true;
            }
            return false;
        }
        EqualityComparer<T> equalityComparer = EqualityComparer<T>.Default;
        for (int index = 0; index < this._size; ++index)
        {
            if (equalityComparer.Equals(this._items[index], item))
                return true;
        }
        return false;
    }

可想而知,如果数据量很大的话,这个循环不就 及其缓慢吗?

我于是请教了那位 GO 的同学,判断重复的逻辑用什么来实现的,他和我说了一个位图 bitmap 的概念,

我用其重写了一下判断重复的逻辑,代码如下:

int[] bitmap = new int[MAX_SIZE];

var index = num % TOTAL_NUM;
bitMap[index] = 1;

return bitMap[num] == 1;

在添加到 List 的时候,顺便插入到 bitmap 中,判断重复只需要根据当前元素的位置是否 等于 1 即可,

我修改代码后,跑了一下 1000w 的数据用来 3000+ ms。

这时候,引起了他的极度怀疑,一向以高性能并发 著称的 Go 速度竟然这么慢吗?他一度怀疑我的逻辑有问题。

下午结束了一个阶段的工作后,我又拾起了我上午写的代码,果不其然,发现了逻辑错误:

如下:

var insertNum = GenerateInt32Num();
if (numList.Contains(insertNum))
{
    insertNum = GenerateInt32Num();
}

生成随机数这里,这里有个大问题,就是其实只判断了一次,导致速度那么快,正确的写法应该是

while (ContainsNum(currentNum))
{
    currentNum = GenerateInt32Num();
}

private int GenerateInt32Num()
{
    var num = random.Next(0, TOTAL_NUM);
    //Console.WriteLine(num);

    return num;
}

最后的代码如下:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace CSharpFundamental
{
    class MultipleThreadCompete
    {
        List<int> numList = new List<int>();
        Random random = new Random();
        CancellationTokenSource cts = new CancellationTokenSource();
        private const int TOTAL_NUM = 1000000;
        private const int CURRENT_THREAD_COUNT = 35;

        ReaderWriterLockSlim rwls = new ReaderWriterLockSlim();

        int[] bitMap = new int[TOTAL_NUM];

        internal void DoTheCompete()
        {
            //ThreadPool.SetMinThreads(CURRENT_THREAD_COUNT, CURRENT_THREAD_COUNT);
            Stopwatch sw = new Stopwatch();
            sw.Start();
            Task[] tasks = new Task[CURRENT_THREAD_COUNT];

            for (int i = 0; i < CURRENT_THREAD_COUNT; ++i)
            {
                int num = i;
                tasks[i] = Task.Run(() => ExecuteTheTask(num, cts), cts.Token);
            }

            Task.WaitAny(tasks);

            Console.WriteLine("ExecuteTime={0}", sw.ElapsedMilliseconds);
        }

        private int GetTheListCount()
        {
            return numList.Count;
        }

        private void AddToList(int num)
        {
            if (numList.Count == TOTAL_NUM)
                return;
            numList.Add(num);

            var index = num % TOTAL_NUM;
            bitMap[index] = 1;
        }

        private void ExecuteTheTask(object state, CancellationTokenSource cts)
        {
            Console.WriteLine("This is the {0} thread,Current ThreadId={1}",
                state,
                Thread.CurrentThread.ManagedThreadId);

            while (!cts.Token.IsCancellationRequested)
            {
                try
                {
                    rwls.EnterReadLock();
                    if (numList.Count == TOTAL_NUM)
                    {
                        cts.Cancel();
                        Console.WriteLine("Current Thread Id={0},Current Count={1}",
                            Thread.CurrentThread.ManagedThreadId,
                            GetTheListCount());
                        break;
                    }
                }
                finally
                {
                    rwls.ExitReadLock();
                }

                var currentNum = GenerateInt32Num();

                while (ContainsNum(currentNum))
                {
                    currentNum = GenerateInt32Num();
                }

                rwls.EnterWriteLock();
                AddToList(currentNum);
                rwls.ExitWriteLock();
            }
        }

        private int GenerateInt32Num()
        {
            var num = random.Next(0, TOTAL_NUM);
            //Console.WriteLine(num);

            return num;
        }

        private bool ContainsNum(int num)
        {
            rwls.EnterReadLock();
            var contains = bitMap[num] == 1;
            rwls.ExitReadLock();

            return contains;
        }
    }
}

结果如下:

但是这个代码执行 1000w的数据需要好久。 这个问题继续研究。

源码地址:https://github.com/doublnt/dotnetcore/tree/master/CSharpFundamental

欢迎大佬指点,还望不吝赐教。

原文地址:https://www.cnblogs.com/xiyin/p/10915068.html

时间: 2024-11-15 06:26:20

C# 多线程小试牛刀的相关文章

多线程小试牛刀

# 导入多线程模块 import threading # 实例化一个RLock对象mlock   mlock = threading.RLock() # 将0赋值给num变量 num = 0 # 定义test函数 def test(): global num # 将num变量转换成全局变量  mlock.acquire()  # 加锁  '''please write your code''' # 要执行的代码块  num += 1  mlock.release()  # 释放锁  print(

Java多线程学习(吐血超详细总结)

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 目录(?)[-] 一扩展javalangThread类 二实现javalangRunnable接口 三Thread和Runnable的区别 四线程状态转换 五线程调度 六常用函数说明 使用方式 为什么要用join方法 七常见线程名词解释 八线程同步 九线程数据传递 本文主要讲了java中多线程的使用方法.线程同步.线程数据传递.线程状态及相应的一些线程函数用法.概述等. 首先讲一下进程和线程

Spring多线程

Spring是通过TaskExecutor任务执行器来实现多线程和并发编程的.使用ThreadPoolTaskExecutor可实现一个基于线程池的TaskExecutor.而实际开发中任务一般是非阻碍的,即异步的,所以我们要在配置类中通过@EnableAsync开启对异步的支持,并通过在实际执行的Bean的方法中使用@Async注解来声明其是一个异步任务. 实例代码: (1)配置类 package com.lwh.highlight_spring4.ch3.taskexecutor; /**

python进阶学习(一)--多线程编程

1. 多线程 概念:简单地说操作系统可以同时执行多个不用程序.例如:一边用浏览器上网,一边在听音乐,一边在用笔记软件记笔记. 并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务"一起"执行(实际上总有一些任务不在执行,因为切换任务的熟度相当快,看上去一起执行而已) 并行:指的是任务数小于等于CPU核数,即任务真的是一起执行的. 2. 线程 概念:线程是进程的一个实体,是CPU调度和分派的基本单位. threading--单线程执行: 1 import ti

多线程的实现及其安全问题

一.进程和线程概述 1.进程:进程是一个具有独立功能的程序关于某个数据集合的一次运行活动,简单来说开启一个程序就开启了一个进程: 如果开启多个进程,它们之间是由于CPU的时间片在相互的切换: 2.线程:开启一个进程的一个任务,对于多线程:每一个线程都在争夺CPU的执行权(CPU的执行权具有随机性): 如果一个程序的执行路径有多条,那么该线程是多线程;反之,就单线程线程:线程是依赖于进程存在的! 3.Jvm是多线程 -- 至少开启了两条线程 main方法 主线程 gc() 垃圾回收线程 二.多线程

多线程和多进程的区别与联系

1.单进程单线程:一个人在一个桌子上吃菜.2.单进程多线程:多个人在同一个桌子上一起吃菜.3.多进程单线程:多个人每个人在自己的桌子上吃菜. 多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了...此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢. 1.对于 Windows 系统来说,[开桌子]的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜.因此 Windows 多线程学习重点

Python有了asyncio和aiohttp在爬虫这类型IO任务中多线程/多进程还有存在的必要吗?

最近正在学习Python中的异步编程,看了一些博客后做了一些小测验:对比asyncio+aiohttp的爬虫和asyncio+aiohttp+concurrent.futures(线程池/进程池)在效率中的差异,注释:在爬虫中我几乎没有使用任何计算性任务,为了探测异步的性能,全部都只是做了网络IO请求,就是说aiohttp把网页get完就程序就done了. 结果发现前者的效率比后者还要高.我询问了另外一位博主,(提供代码的博主没回我信息),他说使用concurrent.futures的话因为我全

多线程(一)

这边来谈谈java中,我对对多线程的理解 在了解多线程前,先说说进程. 进程就是正在运行的应用程序.  当你打开任务管理器的时候,你就会发现很多的进程. 而我们要说的线程,就是依赖于进程而存在的,一个进程可以开启多个线程. Thread类 说到线程,就必须来说说Thread类. Thread类是说有线程的父类.具体请参见api 线程的创建以及执行(图解如下) 继承Thread类,或者实现rennable接口. 当继承了父类后,需要重写父类的run方法,这个run方法里面就写你要执行的代码,当这个

多线程下的单例-double check

话不多说直接上代码: public sealed class Singleton { private static Singleton _instance = null; // Creates an syn object. private static readonly object SynObject = new object(); Singleton() { } public static Singleton Instance { get { // Double-Checked Lockin