C#中的多線程小試牛刀
前言
昨天在上班時(shí)瀏覽博問(wèn),發(fā)現(xiàn)了一個(gè)問(wèn)題,雖然自己在 C# 多線程上沒(méi)有怎么嘗試過(guò),看了幾遍 CLR 中關(guān)于 線程的概念和講解(后面三章)。也想拿來(lái)實(shí)踐實(shí)踐。問(wèn)題定義是這樣的:
對(duì)于多線程不是很懂,面試的時(shí)候遇到一個(gè)多線程的題,不會(huì)做,分享出來(lái),懂的大佬指點(diǎn)一下,謝謝
建一個(gè)winform窗體,在窗體中放上一個(gè)開始按鈕,一個(gè)停止按鈕,一個(gè)文本框,在窗體中聲明一個(gè)List類型的屬性,點(diǎn)擊開始按鈕后開啟10個(gè)線程,所有線程同時(shí)不間斷的給List集合中添加1-10000之間的隨機(jī)數(shù),要求添加List集合中的數(shù)字不能重復(fù),并且實(shí)時(shí)在文本框中顯示集合的長(zhǎng)度,當(dāng)集合List的長(zhǎng)度等于1000時(shí)自動(dòng)停止所有線程,如果中途點(diǎn)擊停止按鈕也停止所有線程,點(diǎn)擊開始又繼續(xù)執(zhí)行。
我其實(shí)沒(méi)有完全實(shí)現(xiàn)了這位博問(wèn)中提問(wèn)的同學(xué)的需求,具體問(wèn)題的來(lái)源可查看該地址 問(wèn)題來(lái)源
開始嘗試
剛拿到這個(gè)需求的時(shí)候,映入我腦海里的是 Task, Threadpool,Concurrent,和 Lock 等概念,接下來(lái)就是組裝和編碼的過(guò)程了,首先理一理頭緒,
- 生成隨機(jī)數(shù)
- 插入到 List 中,且不能重復(fù)
- 開啟多個(gè)線程同時(shí)插入。
首先是生成 隨機(jī)數(shù),使用 System.Random 類來(lái)生成偽隨機(jī)數(shù)(這個(gè)其實(shí)性能和效率賊低,后面再敘述)
private int GenerateInt32Num() { var num = random.Next(0, TOTAL_NUM); return num; }
然后是插入到 List<Int32> 中的代碼,判斷是否 已經(jīng)達(dá)到了 我們需要的 List 長(zhǎng)度,如果已滿足,則退出程序。
private void AddToList(int num) { if (numList.Count == ENDNUM) { return; } numList.Add(num); }
如果是個(gè) 單線程的,按照上面那樣 while(true) 然后一直插入即可,可這個(gè)是個(gè) 多線程,那么需要如何處理呢?
我思考了一下,想到了之前在 CLR 中學(xué)到的 可以用 CancellationTokenSource 中的 Cancel 來(lái)通知 Task 來(lái)取消操作。所以現(xiàn)在的邏輯是,用線程池來(lái)實(shí)現(xiàn)多線程。然后傳入 CancellationTokenSource.Token 來(lái)取消任務(wù)。
最后用 Task.WhanAny() 來(lái)獲取到第一個(gè)到達(dá)此 Task 的 ID。
首先是建立 Task[] 的數(shù)組
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); } }
看起來(lái)是沒(méi)有什么問(wèn)題的,運(yùn)行了一下。得到了如下結(jié)果,
這應(yīng)該是昨晚運(yùn)行時(shí)得到的數(shù)據(jù),當(dāng)時(shí)也沒(méi)有多想,就貼了上去,回答了那位提問(wèn)同學(xué)的問(wèn)題。但是心里有一個(gè)疑惑,為什么會(huì)同時(shí)由 兩個(gè) Thread 同時(shí)達(dá)到了該目標(biāo)呢?
發(fā)現(xiàn)問(wèn)題
今天早上到公司時(shí),我又打開了這個(gè) 代碼,發(fā)現(xiàn)確實(shí)有點(diǎn)不對(duì)勁,于是就和我邊上 做 Go 語(yǔ)言開發(fā)的同學(xué),問(wèn)了問(wèn)他,哪里出現(xiàn)了問(wèn)題,他和我說(shuō):“你加了讀寫鎖了嗎?” 你這里有數(shù)據(jù)臟讀寫。心里面有了點(diǎn)眉目。
按照他說(shuō)的,修改了一下AddToList 里面的邏輯,這時(shí)候,確實(shí)解決了上面的問(wèn)題,
private void AddToList(int num) { rwls.EnterReadLock(); if (numList.Count == ENDNUM) return; rwls.ExitReadLock(); rwls.EnterWriteLock(); numList.Add(num); rwls.ExitWriteLock(); }
得到的結(jié)果如下:
完整的代碼如下所示:
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); } } }
這時(shí)候,那位 Go 語(yǔ)言的同學(xué)和我說(shuō),我們?cè)囋?1000w 的數(shù)據(jù)插入,看看需要多少時(shí)間?于是我讓他用 Go 語(yǔ)言實(shí)現(xiàn)了一下上面的邏輯,1000w數(shù)據(jù)用了 三分鐘,我讓他看看總共生成了多少隨機(jī)數(shù),他查看了一下生成了 1億4千多萬(wàn)的數(shù)據(jù)。
最開始我用上面的代碼來(lái)測(cè),發(fā)現(xiàn)我插入 1000w 的數(shù)據(jù),CPU 到100% 而且花了挺長(zhǎng)時(shí)間,程序根本沒(méi)反應(yīng),查看了一下我判斷重復(fù)的語(yǔ)句numList.Contains()
底層實(shí)現(xiàn)的代碼為:
[__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; }
可想而知,如果數(shù)據(jù)量很大的話,這個(gè)循環(huán)不就 及其緩慢嗎?
我于是請(qǐng)教了那位 GO 的同學(xué),判斷重復(fù)的邏輯用什么來(lái)實(shí)現(xiàn)的,他和我說(shuō)了一個(gè)位圖 bitmap 的概念,
我用其重寫了一下判斷重復(fù)的邏輯,代碼如下:
int[] bitmap = new int[MAX_SIZE]; var index = num % TOTAL_NUM; bitMap[index] = 1; return bitMap[num] == 1;
在添加到 List 的時(shí)候,順便插入到 bitmap 中,判斷重復(fù)只需要根據(jù)當(dāng)前元素的位置是否 等于 1 即可,
我修改代碼后,跑了一下 1000w 的數(shù)據(jù)用來(lái) 3000+ ms。
這時(shí)候,引起了他的極度懷疑,一向以高性能并發(fā) 著稱的 Go 速度竟然這么慢嗎?他一度懷疑我的邏輯有問(wèn)題。
下午結(jié)束了一個(gè)階段的工作后,我又拾起了我上午寫的代碼,果不其然,發(fā)現(xiàn)了邏輯錯(cuò)誤:
如下:
var insertNum = GenerateInt32Num(); if (numList.Contains(insertNum)) { insertNum = GenerateInt32Num(); }
生成隨機(jī)數(shù)這里,這里有個(gè)大問(wèn)題,就是其實(shí)只判斷了一次,導(dǎo)致速度那么快,正確的寫法應(yīng)該是
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; } } }
結(jié)果如下:
但是這個(gè)代碼執(zhí)行 1000w的數(shù)據(jù)需要好久。 這個(gè)問(wèn)題繼續(xù)研究。
源碼地址:https://github.com/doublnt/dotnetcore/tree/master/CSharpFundamental
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
- 深入了解c#多線程編程
- C#中多線程ManualResetEvent 與 AutoResetEvent 區(qū)別
- C#多線程基礎(chǔ)知識(shí)匯總
- 詳解C#多線程編程之進(jìn)程與線程
- C# 多線程編程技術(shù)基礎(chǔ)知識(shí)入門
- 基于C#實(shí)現(xiàn)的輕量級(jí)多線程隊(duì)列圖文詳解
- c#多線程程序設(shè)計(jì)實(shí)例方法
- C# FileStream實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳
- C#中的多線程超時(shí)處理實(shí)踐方案
- C#多線程之Semaphore用法詳解
- C#多線程爬蟲抓取免費(fèi)代理IP的示例代碼
- C#多線程等待所有子線程結(jié)束的示例
相關(guān)文章
C# mysql 插入數(shù)據(jù),中文亂碼的解決方法
用C#操作mysql時(shí), 插入數(shù)據(jù)中文都是亂碼,只顯示問(wèn)號(hào),數(shù)據(jù)庫(kù)本身使用的是utf-8字符2013-10-10C#通過(guò)標(biāo)簽軟件Bartender的ZPL命令打印條碼
這篇文章介紹了C#通過(guò)標(biāo)簽軟件Bartender的ZPL命令打印條碼,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-01-01