以下是學習筆記:
回顧:
Thread線程和ThreadPool線程池
Thread:我們可以開啟一個線程。但是請大家記住:線程開啟會在空間和時間上有不小的開銷。所以,不能隨便開。
ThreadPool:會根據你的CPU的核心數開啟一個最合適的線程數量。如果你操作中,非常耗時,就不要用線程池,如果耗時十幾分鍾,那就不合適線程池了。
Task=>Thread + ThreadPool結合 ,使用多線程,盡量使用Task
1,Task和各種任務阻塞、延續及其線程鎖Lock
#region Task使用【1】多線程任務的開啟3種方式
//【1】通過new的方式創建一個Task對象,並啟動
static void Method1_1()
{
Task task1 = new Task(() =>
{
//在這個地方編寫我們需要的邏輯...
Console.WriteLine($"new一個新的Task啟動的子線程Id={Thread.CurrentThread.ManagedThreadId}");
});
task1.Start();
}
//【2】使用Task的Run()方法
static void Method1_2()
{
Task task2 = Task.Run(() =>
{
//在這個地方編寫我們需要的邏輯...
Console.WriteLine($"使用Task的Run()方法開啟的子線程Id={Thread.CurrentThread.ManagedThreadId}");
});
}
//1和2對比
//1,靈活開啟線程,想什么時候開啟就什么時候開啟
//2, 馬上開啟線程
//【3】使用TaskFactory啟動(類似於ThreadPool)
static void Method1_3()
{
Task task3 = Task.Factory.StartNew(() =>
{
//在這個地方編寫我們需要的邏輯...
Console.WriteLine($"使用TaskFactory開啟的子線程Id={Thread.CurrentThread.ManagedThreadId}");
});
}
#endregion
#region Task使用【2】Task的阻塞方式和任務延續
//【1】回顧之前使用Thread多個子線程執行時阻塞的方法
static void Method2()
{
Thread thread1 = new Thread(() =>
{
Thread.Sleep(2000);
Console.WriteLine("Child Thread (1)......");
});
Thread thread2 = new Thread(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Child Thread (2)......");
});
thread1.Start();
thread2.Start();
//...
thread1.Join();//讓調用線程阻塞
thread2.Join();
//如果有很多的thread,是不是也得有很多的Join?還有,我們只希望其中一個執行完以后,后面的其他線程就能執行,這個也做不了!
Console.WriteLine("This is Main Thread!");
}
//【2】Task各種【阻塞】方式(3個)
static void Method3()
{
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"Task1子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task1.Start();
Task task2 = new Task(() =>
{
Thread.Sleep(2000);
Console.WriteLine($"Task2子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task2.Start();
////第1種方式:挨個等待和前面一樣
//task1.Wait();
//task2.Wait();
////第2種方式:等待所有的任務完成 【推薦】
Task.WaitAll(task1, task2);
//第3種方式:等待任何一個完成即可 【推薦】
//Task.WaitAny(task1, task2);
Console.WriteLine("主線程開始運行!Time=" + DateTime.Now.ToLongTimeString());
/*
第2中方式結果:
Task1子線程Id=4 21:46:58
Task2子線程Id=3 21:46:59
主線程開始運行!Time=21:46:59
第3種方式結果
Task1子線程Id = 3 21:41:34
主線程開始運行!Time = 21:41:34
Task2子線程Id = 4 21:41:35
*/
}
//Task任務的延續:WhenAll 希望前面所有任務執行完畢后,再繼續執行后面的線程,和前面相比,既有阻塞,又有延續。
static void Method4()
{
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"Task1子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task1.Start();
Task task2 = new Task(() =>
{
Thread.Sleep(2000);
Console.WriteLine($"Task2子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task2.Start();
//線程的延續(主線程不等待,子線程依次執行,如果你需要主線程也按照子線程的順序來,請你自己把主線程的任務放到延續任務中就可以)
//線運行主線程,然后task1和task2都執行完,再執行task3
Task.WhenAll(task1, task2).ContinueWith(task3 =>
{
//在這里可以編寫你需要的業務...
Console.WriteLine($"Task3子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
Console.WriteLine("主線程開始運行!Time=" + DateTime.Now.ToLongTimeString());
/*
主線程開始運行!Time = 21:44:46
Task1子線程Id = 3 21:44:47
Task2子線程Id = 4 21:44:48
Task3子線程Id = 3 21:44:48
*/
}
//Task的延續:WhenAny
static void Method5()
{
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"Task1子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task1.Start();
Task task2 = new Task(() =>
{
Thread.Sleep(2000);
Console.WriteLine($"Task2子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
task2.Start();
//線程的延續(主線程不等待,子線程任何一個執行完畢,就會執行后面的線程)
Task.WhenAny(task1, task2).ContinueWith(task3 =>
{
//在這里可以編寫你需要的業務...
Console.WriteLine($"Task3子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
});
Console.WriteLine("主線程開始運行!Time=" + DateTime.Now.ToLongTimeString());
/*
主線程開始運行!Time=21:48:51
Task1子線程Id=3 21:48:52
Task3子線程Id=6 21:48:52
Task2子線程Id=4 21:48:53
*/
}
#endregion
#region Task使用【3】Task常見枚舉 TaskCreationOptions(父子任務運行、長時間運行的任務處理)
//請大家通過Task的構造方法,觀察TaskCreationOptions這個枚舉的類型,自己通過F12查看
static void Method6()
{
Task parentTask = new Task(() =>
{
Task task1 = new Task(() =>
{
Thread.Sleep(1000);
Console.WriteLine($"Task1子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}, TaskCreationOptions.AttachedToParent);
Task task2 = new Task(() =>
{
Thread.Sleep(3000);
Console.WriteLine($"Task2子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}, TaskCreationOptions.AttachedToParent);
task1.Start();
task2.Start();
});
parentTask.Start();
parentTask.Wait();//等待附加的子任務全部完成。相當於Task.WaitAll(taks1,task2);
//TaskCreationOptions.AttachedToParent如果這個枚舉參數不添加,主線程會直接運行,不等待
Console.WriteLine("主線程開始執行!Time= " + DateTime.Now.ToLongTimeString());
/*
Task1子線程Id=4 21:52:17
Task2子線程Id=5 21:52:19
主線程開始執行!Time= 21:52:19
*/
}
//長時間的任務運行,需要采取的方法
static void Method7()
{
Task task1 = new Task(() =>
{
Thread.Sleep(2000);
Console.WriteLine($"Task1子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}, TaskCreationOptions.LongRunning);
//LongRunning:如果你明確知道這個任務是長時間運行的,建議你加上。
//當然你使用Thread也是可以的。但是不要使用ThreadPool,因為長時間占用不歸還線程,系統會強制開啟新的線程,會一定程度影響性能
task1.Start();
task1.Wait();
Console.WriteLine("主線程開始執行!Time= " + DateTime.Now.ToLongTimeString());
/*
Task1子線程Id=3 21:57:42
主線程開始執行!Time= 21:57:42
*/
}
#endregion
#region Task使用【4】Task中的取消功能:使用的是CacellationTokenSoure解決多任務中協作取消和超時取消方法
//【1】Task任務的取消和判斷
static void Method8()
{
//創建取消信號源對象
CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Factory.StartNew(() =>
{
int i = 0;
while (!cts.IsCancellationRequested) //判斷任務是否被取消
{
Thread.Sleep(200);
i++;
Console.WriteLine(
$"執行次數:{i},子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}
}, cts.Token);
//我們在這個地方模擬一個事件產生,如果發生某個錯誤,就取消線程
Thread.Sleep(2000);
cts.Cancel(); //取消任務,只要傳遞這樣一個信號就可以
/*
執行次數:1,子線程Id=3 22:06:18
執行次數:2,子線程Id=3 22:06:18
執行次數:3,子線程Id=3 22:06:18
執行次數:4,子線程Id=3 22:06:18
執行次數:5,子線程Id=3 22:06:19
執行次數:6,子線程Id=3 22:06:19
執行次數:7,子線程Id=3 22:06:19
執行次數:8,子線程Id=3 22:06:19
執行次數:9,子線程Id=3 22:06:19
執行次數:10,子線程Id=3 22:06:20
*/
}
//【2】Task任務取消:同時我們也希望做一些清理的工作,也就是取消這個動作會觸發一個任務。
static void Method9()
{
CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Factory.StartNew(() =>
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(500);
Console.WriteLine($"子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}
}, cts.Token);
//注冊一個委托:這個委托將在任務取消的時候調用
cts.Token.Register(() =>
{
//在這個地方可以編寫自己要處理的邏輯...
Console.WriteLine($"任務取消,開始清理工作......{DateTime.Now.ToLongTimeString()}");
Thread.Sleep(2000);
Console.WriteLine($"任務取消,清理工作結束......{DateTime.Now.ToLongTimeString()}");
});
//這個地方肯定是有其他的邏輯來控制取消
Thread.Sleep(3000);//模擬其他的耗時工作
cts.Cancel();//取消任務
/*
子線程Id=3 22:12:52
子線程Id=3 22:12:53
子線程Id=3 22:12:53
子線程Id=3 22:12:54
子線程Id=3 22:12:54
任務取消,開始清理工作......22:12:55
子線程Id=3 22:12:55
任務取消,清理工作結束......22:12:57
*/
}
//【3】Task任務延時自動取消:比如我們請求一個遠程接口,如果長時間沒有返回數據,我們可以做一個時間限制,超時可以取消任務(比如微信紅包退回)
static void Method10()
{
CancellationTokenSource cts = new CancellationTokenSource();
// CancellationTokenSource cts = new CancellationTokenSource(3000);
Task task = Task.Factory.StartNew(() =>
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(300);
Console.WriteLine($"子線程Id={Thread.CurrentThread.ManagedThreadId} {DateTime.Now.ToLongTimeString()}");
}
}, cts.Token);
//注冊一個委托:這個委托將在任務取消的時候調用
cts.Token.Register(() =>
{
//在這個地方可以編寫自己要處理的邏輯...
Console.WriteLine($"任務取消,開始清理工作......{DateTime.Now.ToLongTimeString()}");
Thread.Sleep(2000);
Console.WriteLine($"任務取消,清理工作結束......{DateTime.Now.ToLongTimeString()}");
});
cts.CancelAfter(3000); //3秒后自動取消
/*
子線程Id=3 22:16:49
子線程Id=3 22:16:50
子線程Id=3 22:16:50
子線程Id=3 22:16:50
子線程Id=3 22:16:50
子線程Id=3 22:16:51
子線程Id=3 22:16:51
子線程Id=3 22:16:51
子線程Id=3 22:16:52
任務取消,開始清理工作......22:16:52
子線程Id=3 22:16:52
任務取消,清理工作結束......22:16:54
*/
}
#endregion
#region Task使用【5】Task中專門的異常處理:AggregateException
//AggregateException:是一個異常集合,因為Task中可能拋出異常,所以我們需要新的類型來收集異常對象
static void Method11()
{
var task = Task.Factory.StartNew(() =>
{
var childTask1 = Task.Factory.StartNew(() =>
{
//實際開發中這個地方寫你處理的業務,可能會發生異常....
//自己模擬一個異常
throw new Exception("my god!Exception from childTask1 happend!");
}, TaskCreationOptions.AttachedToParent);
var childTask2 = Task.Factory.StartNew(() =>
{
throw new Exception("my god!Exception from childTask2 happend!");
}, TaskCreationOptions.AttachedToParent);
});
try
{
try
{
task.Wait(); //1.異常拋出的時機(等待task執行完畢,這里是等到異常拋出)
}
catch (AggregateException ex) //2.異常所在位置
{
foreach (var item in ex.InnerExceptions)
{
Console.WriteLine(item.InnerException.Message + " " + item.GetType().Name);
}
//3.異常集合,如果你想往上拋,需要使用Handle方法處理一下
ex.Handle(p =>
{
if (p.InnerException.Message == "my god!Exception from childTask1 happend!")
return true;//就結束了,不往上拋了
else
return false; //返回false表示往上繼續拋出異常
});
}
}
catch (Exception ex)
{
Console.WriteLine("-----------------------------------------------------");
Console.WriteLine(ex.InnerException.InnerException.Message);
}
/*
my god!Exception from childTask2 happend! AggregateException
my god!Exception from childTask1 happend! AggregateException
-----------------------------------------------------
my god!Exception from childTask2 happend!
*/
}
#endregion
#region 監視鎖:Lock 限制線程個數的一把鎖
//為什么要用鎖?在多線程中,尤其是靜態資源的訪問,必然會有競爭
private static int nums = 0;
private static object myLock = new object();
static void Method12()
{
for (int i = 0; i < 5; i++)
{
//開啟5線程調用一個nums
Task.Factory.StartNew(() =>
{
//TestMethod1();//不加鎖的結果順序是亂的,1,3,2,4,6,9,,,,500
TestMethod2();//加鎖的結果順序是對的,因為把資源給鎖住了,1,2,3,4,5,6,,,,500
});
}
}
static void TestMethod1()
{
for (int i = 0; i < 100; i++)
{
nums++;
Console.WriteLine(nums);
}
}
static void TestMethod2()
{
for (int i = 0; i < 100; i++)
{
lock (myLock)
{
nums++;
Console.WriteLine(nums);
}
}
}
//Lock是Monitor語法糖,本質是解決資源的鎖定問題
//我們鎖住的資源一定是讓線程可訪問到的,所以不能是局部變量。
//鎖住的資源千萬不要是值類型。
//lock也不能鎖住string類型。
}
#endregion
2,Task中的跨線程訪問控件和UI耗時任務卡頓的解決方法

//普通方法
private void btnUpdate_Click(object sender, EventArgs e)
{
Task task = new Task(() =>
{
this.lblInfo.Text = "來自Task的數據更新:我們正在學習多線程!";
});
//task.Start(); //這樣使用會報錯
//使用下面的方式解決報錯的問題
task.Start(TaskScheduler.FromCurrentSynchronizationContext());//使用任務調度器
}
//針對UI耗時的情況,單獨重載其實並不是很好
private void btnUpdate_Click1(object sender, EventArgs e)
{
Task task = new Task(() =>
{
//模擬耗時(這個地方會卡主)
Thread.Sleep(5000);//界面會卡5秒鍾,多線程不是萬能,多線程並不是解決卡界面的。
this.lblInfo.Text = "來自Task的數據更新:我們正在學習多線程!";
});
//task.Start(); //這樣使用會報錯
//使用下面的方式解決報錯的問題
task.Start(TaskScheduler.FromCurrentSynchronizationContext());
}
//以后耗時任務都可以用這個方法
//針對耗時任務,我們可以使用新的方法
private void btnUpdate_Click2(object sender, EventArgs e)
{
this.btnUpdate.Enabled = false;
this.lblInfo.Text = "數據更新中,請等待......";
Task task =Task.Factory.StartNew(() =>
{
Thread.Sleep(5000); //有耗時的任務,我們可以放到ThreadPool中
});
//在ContinueWith中更新我們的數據
task.ContinueWith(t =>
{
this.lblInfo.Text = "來自Task的數據更新:我們正在學習多線程!";
this.btnUpdate.Enabled = true;
},TaskScheduler.FromCurrentSynchronizationContext()); //更新操作到同步的上下文中
}
