多線程的使用對於程序員來說是必不可少的一項技能,多線程會用的程序員很多,大部分程序員都不敢說自己玩的賊6,
比如博主自己,多線程玩得不6就需要不斷充能。這次總結一下學習多線程的學習心得。
說單線程跟多線程之前先了解一下什么是並行,什么是並發,這兩個概念一定得搞懂。
並行:多個任務並列進行。例如:五千米長跑,信號槍響后,運動員同時並列跑。
並發:CUP切片執行。例如:電風扇的扇葉的旋轉,在其中一片葉子上做好標記,速度慢的時候可以看到哪片扇葉先到某個位置,
當旋轉速度達到一定速度后就分不清順序了,感覺多個扇葉到達某個位置的速度都是一樣的。電腦同時運行QQ,運行vs 2019,
在寫代碼時候QQ可以同時接收消息,就是因為計算機執行速度足夠快,CUP快速的調度,1秒中QQ跟vs 2019切換了上億次,
給人感覺就是同時在運行。在微觀的角度上,CPU的一個核只能執行一個任務,相當於只能運行一個程序,因為切換速度快,在宏觀上就感覺不出來。
多線程應用場景非常廣泛,那么它為什么就應用廣泛呢?相當於單線程有何有點?我在這里舉個栗子說說自己的理解。
舉例:博主比較喜歡在看電影電視劇時吃點小零食喝點飲料,比如最近的慶余年特別火爆,博主也是甚是喜愛。
可以想象一下博主一邊吃着炸雞一邊喝着啤酒看着帶有喜感的王啟年並翹着小腳丫的樣子,還在加班的你們是不是有種想打死博主的沖動。
一邊吃着炸雞、喝啤酒、翹着腳丫子、看着電視劇都是在同一時間端同時進行的,那么這就是多線程。
吃完炸雞才能喝啤酒,喝完啤酒才能翹腳丫子,翹完腳丫子才能看電視劇,這樣一件事做完才能做下一件事的例子就是單線程。
在C#中,創建線程的方式有Thread、Action(委托)、ThreadPool(線程池)、Task、Parallel等多種方式,接下來就一樣使用這幾種方式來創建線程以及它們之間的區別。
1.使用Thread的類創建線程
Thread是CLR2.0(framework 2.0)才出現的。
public void Dosomething()
{
Console.WriteLine($"此處做點啥,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
}
public void TheardEstablish()
{
{
//framework2.0創建多線程的方式
//ThreadStart 是一個無參數的委托
//創建方式1:
ThreadStart threadStart = this.Dosomething;
Thread t = new Thread(threadStart);
t.Start();//運行當前線程
//創建方式2:
Thread thread = new Thread(() =>
{
Console.WriteLine($"此處做點啥,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
});
thread.Start();//運行當前線程
thread.Join();//等待線程結束
}
}
Console.WriteLine($"當前主線程start.....,線程ID:{Thread.CurrentThread.ManagedThreadId}");
Thread thread = new Thread(() =>
{
Thread.Sleep(5000);
Console.WriteLine($"Thread start.......,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
});
thread.Start();//運行當前線程
//thread.Join();//等待線程結束,主線程處於等待狀態,界面會卡住
//Console.WriteLine($"Thread end.......,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
//不卡界面的方式,重新開啟一個線程用於等待線程
Thread thread2 = new Thread(() =>
{
thread.Join();
Console.WriteLine($"Thread end.......,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
});
thread2.Start();
Console.WriteLine($"當前主線程end.....,線程ID:{Thread.CurrentThread.ManagedThreadId}");

for (int i = 0; i < 20; i++)
{
int k = i;//范圍變量
new Thread(() =>
{
Console.WriteLine($"執行第{k}次,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
}).Start();
}

2.Action(委托)創建線程
所有的線程都是基於委托實現的
//線程執行完成后的回調
AsyncCallback callback =(o)=>
{
Console.WriteLine($"Thread end.......,參數:{o.AsyncState},當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
};
Action action = new Action(() =>
{
Thread.Sleep(5000);
Console.WriteLine($"Thread start.......,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
});
//action.Invoke();//等待,使用的是同步方式,主線程處於等待狀態
//action.BeginInvoke(null, null);//不等待,異步方式,不阻塞
action.BeginInvoke(callback, "我是參數");//第二個是傳給回調的參數,是個object類型

3.ThreadPool(線程池)創建線程
//ThreadPool.SetMaxThreads(4, 4);//設置當前應用程序支持線程並發時的最大數,最小是4
//使用線程池的方式創建線程可以更好的利於線程,創建的線程可以重復利於,性能相當於Thread創建的方式更好
//原理:一次性創建多個線程放到線程池中,使用線程池里面的線程去執行任務,任務執行完成后再把線程放回去,需要執行任務后先判斷線程池有沒有
//空閑的線程,如果沒有會等待其它線程執行完成后再從線程池取空閑的線程來執行任務
for (int i = 0; i < 20; i++)
{
int k = i;
ThreadPool.UnsafeQueueUserWorkItem((o) =>
{
Thread.Sleep(500);
Console.WriteLine($"執行操作00{k},參數:{o},當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
}, $"我是參數{k}");
}

4.Parallel創建線程
//Parallel 創建線程執行任務時,如果主線程閑置也會參與執行任務
Parallel.Invoke(() =>
{
Thread.Sleep(500);
Console.WriteLine($"執行第1次,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
}, () =>
{
Thread.Sleep(500);
Console.WriteLine($"執行第2次,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
}, () =>
{
Thread.Sleep(500);
Console.WriteLine($"執行第3次,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
}, () =>
{
Thread.Sleep(500);
Console.WriteLine($"執行第4次,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
}, () =>
{
Thread.Sleep(500);
Console.WriteLine($"執行第5次,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
});
new Thread(() =>//這里包一層,可以不讓主線程參與,可以解決占用主線程的問題。(沒有什么問題是包一層解決不了的,解決不了再包一層)
{
int[] taskNums = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };//設置任務數
var data = Parallel.For(1, taskNums.Length, new ParallelOptions()
{
MaxDegreeOfParallelism = 3//一次性執行任務最大數,可以控制並發的線程數,需要控制並發數的時候非常有用
}, (i) =>
{
Thread.Sleep(1000);
Console.WriteLine($"執行第{i}次,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
});
}).Start();

5.Task 創建線程
//Task task=Task.Run(() =>
//{
// Thread.Sleep(500);
// Console.WriteLine($"開啟一個線程任務,start.....,線程ID:{Thread.CurrentThread.ManagedThreadId}");
//});
//task.Wait();//主線程等待任務完成
//Task task = new Task(()=>
// {
// Thread.Sleep(500);
// Console.WriteLine($"開啟一個線程任務,start.....,線程ID:{Thread.CurrentThread.ManagedThreadId}");
// });
//task.Start();//啟動線程
//task.Wait();//主線程等待任務完成
List<Task> tasks=new List<Task>();
TaskFactory taskFactory=new TaskFactory();
for (int i = 0; i < 20; i++)
{
int k = i;
tasks.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(500);
Console.WriteLine($"執行操作{k},當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
}));
}
//Task.WaitAll(tasks.ToArray());//等待所有任務執行完成
//Console.WriteLine("所有任務都執行完成了......");
int result =Task.WaitAny(tasks.ToArray());//等待其一任意一個任務完成成功后執行
Console.WriteLine($"任務{result}優先完成......");

以上就是多種創建線程的方式,接下來來了解線程內部出現異常如何處理。
為了了解線程內部異常如何處理先看如下代碼:
List<Task> tasks = new List<Task>();
TaskFactory taskFactory = new TaskFactory();
try
{
for (int i = 0; i < 20; i++)
{
int k = i;
tasks.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(500);
if (k == 10 || k == 15)
{
throw new Exception($"線程{k}拋出了異常");
}
Console.WriteLine($"執行操作{k},當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
}));
}
Task.WaitAll(tasks.ToArray());//等待任務全部執行完成
}
catch (AggregateException e)
{
foreach (var ex in e.InnerExceptions)
{
Console.WriteLine($"異常信息:{ex.Message}");
}
}
catch (Exception e)
{
Console.WriteLine(e);
}

線程內部異常可以通過,try catch 的方式捕獲,但是當異常發生時后續任務都被執行了。接下來看優化后的代碼:
List<Task> tasks = new List<Task>();
TaskFactory taskFactory = new TaskFactory();
CancellationTokenSource cts=new CancellationTokenSource();
try
{
for (int i = 0; i < 20; i++)
{
int k = i;
tasks.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(500);
if (k == 10 || k == 15)
{
cts.Cancel();
throw new Exception($"線程{k}拋出了異常");
}
Console.WriteLine($"執行操作{k},當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
}, cts.Token));
}
Task.WaitAll(tasks.ToArray());//等待任務全部執行完成
}
catch (AggregateException e)
{
foreach (var ex in e.InnerExceptions)
{
Console.WriteLine($"異常信息:{ex.Message}");
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
finally
{
cts.Dispose();
}

以上結果就是我們需要的。
我們做異常處理最終的目的就是為了記錄異常信息,日后好排查,其實最好處理就是讓線程不拋異常,在線程內部做try catch 處理,如下圖所示:

隨着多線程技術使用得越來越廣泛,當多個線程訪問共享變量時就會存在線程安全問題,接下來就來了解如何處理線程安全問題。
為了更好的觀察多線程安全問題,請看下面兩個例子:
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
Task.Run(() =>
{
Console.WriteLine($"正在執行第{i}次操作,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
});
}
}
{
int num = 0;
int num_1 = 0;
for (int i = 0; i < 10000; i++)
{
num+=1;
}
List<Task> tasks = new List<Task>();
for (int i = 0; i < 10000; i++)
{
tasks.Add(Task.Run(() =>
{
num_1+=1;
}));
}
Task.WaitAll(tasks.ToArray());//等待所有線程執行完成
Console.WriteLine($"num的值為:{num},num_1的值為:{num_1}");
}


第一個案例出現多線程問題是因為for循環執行太快了,第一個Task.Run開始運行時候for循環已經走完了。
第二個案例出現的問題是因為多個線程操作同一變量,導致了內容被覆蓋問題。
如何解決上面的問題,眾所周知都認為可以是lock鎖,被鎖的內容只能由一個線程接入,操作完成后下一個線程才能進入。修改后的代碼如下:
{
for (int i = 0; i < 10; i++)
{
int k = i;
Task.Run(() =>
{
Console.WriteLine($"正在執行第{k}次操作,當前線程ID:{Thread.CurrentThread.ManagedThreadId}");
});
}
}
{
int num = 0;
int num_1 = 0;
object lockObj=new object();
for (int i = 0; i < 10000; i++)
{
num+=1;
}
List<Task> tasks = new List<Task>();
for (int i = 0; i < 10000; i++)
{
tasks.Add(Task.Run(() =>
{
lock (lockObj)
{
num_1 += 1;
}
}));
}
Task.WaitAll(tasks.ToArray());//等待所有線程執行完成
Console.WriteLine($"num的值為:{num},num_1的值為:{num_1}");
}

加lock鎖就可以解決線程安全問題。
當然還有另外一種方式,就是使用C#內部的一些類型安全變量,這些類型安全變量在【System.Collections.Concurrent】下

使用lock鎖有一些需要特別注意,lock括號內的值不能是null,string類型的值,(string 是一個特別的引用類型),不建議使用lock(this)這種寫法(這是一個坑)
關於lock(this)寫法之前遇到一個經典的面試題,代碼如下:
public class LockDemo
{
private int num = 0;
public void Test()
{
lock (this)
{
this.num++;
if (this.num < 10)
{
Console.WriteLine($"當前次數為{this.num}次");
this.Test();
}
else
{
Console.WriteLine("結束......");
}
}
}
}
問執行Test方法是會不會出現死鎖問題,當時不太清楚說會,正確的答案是不會,看着答案給我第一感覺是不是題目有問題,自己敲了一邊代碼,確實可以運行。最后的結論是線程自己不能鎖住自己。

async/await 的使用
async/await是farmework 4.5出現的語法糖。如何使用如代碼下:
public void TestDemoAsync()
{
Console.WriteLine($"當前主線程start.....,線程ID:{Thread.CurrentThread.ManagedThreadId}");
//AwaitDemo();
ThreadDemo();
Console.WriteLine($"當前主線程end.....,線程ID:{Thread.CurrentThread.ManagedThreadId}");
}
/// <summary>
///
/// </summary>
public void ThreadDemo()
{
Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"dosomeThing .......,線程ID:{Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"在dosomeThing之后執行.....,線程ID:{Thread.CurrentThread.ManagedThreadId}");
}
/// <summary>
/// async 跟await 一般都是配套使用
/// </summary>
/// <returns></returns>
public async Task AwaitDemo()
{
await Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"dosomeThing .......,線程ID:{Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"在dosomeThing之后執行.....,線程ID:{Thread.CurrentThread.ManagedThreadId}");
}
分別執行AwaitDemo(),ThreadDemo()兩個方法,執行結果如下


從執行結果可以看出,沒有添加await關鍵字的Task.Run 后面一句代碼是沒有阻塞的,直接被主線程執行了,添加了await關鍵字的Task.Run后面一句代碼被阻塞了,子線程執行完成后才由主線程執行。
從中可以總結出被await關鍵字修飾的代碼,當主線程遇到await時直接跳過Task.Run后面的代碼,當子線程執行完成后,主線程再執行Task.Run后面的代碼,可以達到這種效果是因為底層使用了狀態機這種機制。
以上就是學習多線程的一些總結,為以后復習提供參考價值。
