C#-线程学习笔记
一、线程概念
(1)线程(Thread)是进程中的基本执行单元,每个进程内部都有多个线程。每个线程都有自己独立的栈,但是与进程内的其他线程共享内存。
(2)多线程多线程可以提高CPU的利用率,因为当一个线程处于等待状态的时候,CPU会去执行另外的线程。直接提高程序的整体执行速度。
(3)多线程由内部线程调度程序管理,线程调度器通常是CLR委派给操作系统的函数。线程调度程序确保所有活动线程都被分配到合适的执行时间,线程在等待或阻止时 (例如,在一个独占锁或用户输入) 不会消耗 CPU 时间
二、Thread属性与方法
属性 | 说明 | |
CurrentThread | 获取正在运行的线程 | |
CurrentContext | 获取线程正在其中执行的当前上下文。 | |
ExecutionContext | 获取ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。 | |
IsAlive | 获取指示当前线程的执行状态的值 | |
IsBackground | 获取指示某个线程是否为后台线程的值 | |
IsThreadPoolThread | 获取一个值,该值指示线程是否属于托管线程池。 | |
Name | 获取或设置线程的名字 | |
ManagedThreadId | 获取当前托管线程的唯一标识符。ManagedThreadId是确认线程的唯一标识符 | |
Priority | 获取或设置一个值,该值指示线程的调度优先级 | |
ThreadState | 获取一个值,该值包含当前线程的状态 | |
GetApartmentState | 废弃用法ApartmentState | 获取当前线程的单元状态 |
SetApartmentState | 废弃用法ApartmentState | 设置当前现成的单元状态 |
方法: | ||
创建线程 | new Thread | |
线程启动 | start() | 使用start()方法具有异步执行的效果,而使用run()方法是同步执行的效果,run()方法并没有真的启动线程。 |
线程休眠 | sleep() | sleep(1000);把正在运行的线程挂起一段时间。(sleep: 不释放锁对象, 释放CPU使用权;wait(): 释放锁对象, 释放CPU使用权,能唤醒) |
线程加入 | join() | 阻塞调用线程,直到某个线程终止时为止(理解:暂时停止另一个线程线程,直到调用join()的线程运行终止) |
线程挂起 | 为什么 Thread.stop和Thread.suspend等被废弃了? | 过期方法 Suspend(),其天生不安全,不会保证释放资源,容易产生死锁 |
已挂起线程继续 | 为什么 Thread.stop和Thread.suspend等被废弃了? | 过期方法Resume(),其天生不安全,不会保证释放资源,容易产生死锁 |
线程中断 | 为什么 Thread.stop和Thread.suspend等被废弃了? | 过期方法stop(),其天生不安全,不会保证释放资源,容易产生死锁 |
Why do you disagree with the use of Thread.stop, Thread.suspend and Thread.resume? (OSCHINA) | ||
线程终止 | Abort() | 终止本线程。(Net5中被废弃,请使用取消线程的标识CancellationTokenSource) |
返回线程当前域 | GetDomain() | 返回当前线程正在其中运行的当前域。 |
返回线程当前域Id | GetDomainId() | 返回当前线程正在其中运行的当前域Id。 |
中断某些线程 | Interrupt() | 中断处于 WaitSleepJoin 线程状态的线程。 |
三、线程简单使用示例:
1、示例-线程创建、运行、休眠:
(1)简单示例(Net framework):
using System;
using System.Threading.Tasks;
namespace ConsoleThread
{
class Program
{
static void Main(string[] args)
{
//Thread t = new Thread(thread2); // 创建
//t.Start();
new Thread(thread2).Start(); //启动
for (int thread1 = 1; thread1 > 0; thread1++)
{
Console.WriteLine("线程1已开启"); // Convert.ToString(thread1)
Thread.Sleep(1000); //休眠
}
}
private static void thread2()
{
for (int thread2 = 1; thread2 > 0; thread2++)
{
Console.WriteLine("线程2已开启"); // Convert.ToString(thread2)
Thread.Sleep(1000);
}
}
}
}
(2)启动与暂停、启动与停止成对对应的写法风格(Net framework):
// _timer是线程
// 启动
if(_timer.ThreadState == ThreadState.Suspended){
_timer.Resume();
}
if(_timer.ThreadState == ThreadState.Unstarted){
_timer.Start();
}
// 停止
_timer.Suspend();
// 终止
if(_timer.ThreadState == ThreadState.Suspended){
_timer.Resume();
}
if(_timer.ThreadState == ThreadState.Running){
_timer.Abort();
}
(3)Net5+版本中停止循环线程的写法
// C#代码使用取消标记终止线程的示例:
// 在下面的例子中,使用`CancellationTokenSource`创建取消标记,以便在线程未完成时,可以随时发出取消请求。在线程内部,使用`CancellationToken`对象检查取消请求,从而平滑结束线程。
private CancellationTokenSource cts;
void StartThread()
{
cts = new CancellationTokenSource();
Task.Run(() => ThreadCode(cts.Token));
}
void StopThread()
{
cts.Cancel();
}
void ThreadCode(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// Your code here
}
}
2、线程等待的几种方式
3、hread属性/ThreadAttribute
1 using System;
2 using System.Threading;
3
4 namespace ConsoleThreadAttribute
5 {
6 /*
7 * 死锁例子
8 * 变量共享
9 */
10 class Program
11 {
12 static void Main(string[] args)
13 {
14 //获取正在运行的线程
15 Thread thread = Thread.CurrentThread;
16 //设置线程的名字
17 thread.Name = "主线程";
18 //获取当前线程的唯一标识符
19 int id = thread.ManagedThreadId;
20 //获取当前线程的状态
21 ThreadState state = thread.ThreadState;
22 //获取当前线程的优先级
23 ThreadPriority priority = thread.Priority;
24 string strMsg = string.Format("Thread ID:{0}\n" + "Thread Name:{1}\n" +
25 "Thread State:{2}\n" + "Thread Priority:{3}\n", id, thread.Name,
26 state, priority);
27
28 Console.WriteLine(strMsg);
29
30 Console.ReadKey();
31 }
32 }
33 }
三,前台线程和后台线程
1 using System;
2 using System.Threading;
3
4 namespace ConsoleThread_Foreground_Background
5 {
6 class Program
7 {
8 static void Main(string[] args)
9 {
10 //演示前台、后台线程
11 BackGroundTest background = new BackGroundTest(10);
12 //创建前台线程
13 Thread fThread = new Thread(new ThreadStart(background.RunLoop));
14 //给线程命名
15 fThread.Name = "前台线程";
16
17
18 BackGroundTest background1 = new BackGroundTest(20);
19 //创建后台线程
20 Thread bThread = new Thread(new ThreadStart(background1.RunLoop));
21 bThread.Name = "后台线程";
22 //设置为后台线程
23 bThread.IsBackground = true;
24
25 //启动线程
26 fThread.Start();
27 bThread.Start();
28 //如果没有Console.ReadKey();,以Console.ReadKey();结尾,则 执行完bThread.Start();结束;但bThread线程是后台线程,所以前台线程fThread结束后bThread线程随之结束
29 //Console.ReadKey();
30 }
31 }
32 class BackGroundTest
33 {
34 private int Count;
35 public BackGroundTest(int count)
36 {
37 this.Count = count;
38 }
39 public void RunLoop()
40 {
41 //获取当前线程的名称
42 string threadName = Thread.CurrentThread.Name;
43 for (int i = 0; i < Count; i++)
44 {
45 Console.WriteLine("{0}计数:{1}", threadName, i.ToString());
46 //线程休眠500毫秒
47 Thread.Sleep(1000);
48 }
49 Console.WriteLine("{0}完成计数", threadName);
50 }
51 }
52 }
四、线程的优先级-Priority属性
1、为什么要使用优先级:
(1)线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多。
(2)多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程。
(3)线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。
(4)线程太多会导致控制太复杂,最终可能造成很多程序缺陷。
补充:CPU按照线程的优先级给予服务。高优先级的线程可以完全阻止低优先级的线程执行。.NET为线程设置了Priority属性来定义线程执行的优先级别,里面包含5个选项,其中Normal是默认值。除非系统有特殊要求,否则不应该随便设置线程的优先级别。
2、优先级等级
成员名称 | 说明 |
---|---|
Lowest | 可以将 Thread 安排在具有任何其他优先级的线程之后。 |
BelowNormal | 可以将 Thread 安排在具有 Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前。 |
Normal | 默认选择。可以将 Thread 安排在具有 AboveNormal 优先级的线程之后,在具有BelowNormal 优先级的线程之前。 |
AboveNormal | 可以将 Thread 安排在具有 Highest 优先级的线程之后,在具有 Normal 优先级的线程之前。 |
Highest | 可以将 Thread 安排在具有任何其他优先级的线程之前。 |
五、线程同步(线程安全)- 锁
线程安全是指在当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
1、使用Lock关键字实现线程同步 (拿一个别人的例子)
(1)ThreadUnsafe(加锁前)
1 using System;
2 using System.Threading;
3
4 namespace ConsoleThreadUnsafe
5 {
6 class Program
7 {
8 static void Main(string[] args)
9 {
10 Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法
11 threadA.Name = "1";
12 Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法
13 threadB.Name = "2";
14 threadA.Start();
15 threadB.Start();
16 Console.ReadKey();
17 }
18 public static void ThreadMethod(object parameter)
19 {
20 for (int i = 0; i < 10; i++)
21 {
22 Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
23 Thread.Sleep(300);
24 }
25 }
26 }
27 }
结果:
1 我是:1,我循环0次
2 我是:2,我循环0次
3 我是:1,我循环1次
4 我是:2,我循环1次
5 我是:1,我循环2次
6 我是:2,我循环2次
7 我是:1,我循环3次
8 我是:2,我循环3次
9 我是:1,我循环4次
10 我是:2,我循环4次
11 我是:1,我循环5次
12 我是:2,我循环5次
13 我是:1,我循环6次
14 我是:2,我循环6次
15 我是:1,我循环7次
16 我是:2,我循环7次
17 我是:1,我循环8次
18 我是:2,我循环8次
19 我是:1,我循环9次
20 我是:2,我循环9次
(2)ThreadSafe(加锁后)
using System;
using System.Threading;
namespace ConsoleThreadSafe
{
class Program
{
private static object obj = new object(); //一般情况下,loke私有的、静态的并且是只读的对象。
static void Main(string[] args)
{
Program pro1 = new Program(); // 改动
Program pro2 = new Program(); // 改动
Thread threadA = new Thread(pro1.ThreadMethod); //执行的必须是无返回值的方法 - 改动
threadA.Name = "1";
Thread threadB = new Thread(pro2.ThreadMethod); //执行的必须是无返回值的方法 - 改动
threadB.Name = "2";
threadA.Start();
threadB.Start();
Console.ReadKey();
}
public void ThreadMethod(object parameter)
{
lock (obj) // lock全局的私有化静态变量,外部无法对该变量进行访问。
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
Thread.Sleep(300);
}
}
}
}
}
结果:
我是:1,我循环0次
我是:1,我循环1次
我是:1,我循环2次
我是:1,我循环3次
我是:1,我循环4次
我是:1,我循环5次
我是:1,我循环6次
我是:1,我循环7次
我是:1,我循环8次
我是:1,我循环9次
我是:2,我循环0次
我是:2,我循环1次
我是:2,我循环2次
我是:2,我循环3次
我是:2,我循环4次
我是:2,我循环5次
我是:2,我循环6次
我是:2,我循环7次
我是:2,我循环8次
我是:2,我循环9次
2、lock要点(来自:C#多线程和线程池)
(1)lock的是必须是引用类型的对象,string类型除外。
(2)lock推荐的做法是使用静态的、只读的、私有的对象。
(3)保证lock的对象在外部无法修改才有意义,如果lock的对象在外部改变了,对其他线程就会畅通无阻,失去了lock的意义。
(4)不能锁定字符串,锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。lock(typeof(Class))与锁定字符串一样,范围太广了。
3、使用Monitor类实现线程同步 (先略了,往下学)
4、使用Mutex类实现线程同步(同上;Mutex可用在进程间)
5、读写锁:
略
六、线程池
1、线程池概念:
上面介绍了介绍了平时用到的大多数的多线程的例子,但在实际开发中使用的线程往往是大量的和更为复杂的,这时,每次都创建线程、启动线程。从性能上来讲,这样做并不理想(因为每使用一个线程就要创建一个,需要占用系统开销);从操作上来讲,每次都要启动,比较麻烦。为此引入的线程池的概念。
(1)每个CLR (公共语言运行库)拥有一个线程池,这个线程池由CLR控制的APPDomain 共享。
(2)在线程池内部,它自己维护着一个操作请求队列,应用程序需要执行某个任务时,就需要调用线程池的一个方法(通常是QueueUserWorkItem 方法)将任务添加到线程池工作项中,线程池就会将任务分派给一个线程池线程处理,如果线程池中没有线程,就会创建一个线程来处理这个任务。当任务执行完成以后,这个线程会回到线程池中处于空闲状态,等待下一个执行任务。
(3)由于线程不会销毁,所以使用线程池线程在执行任务的速度上会更快。
(4)线程池最多管理线程数量=“处理器数 * 250”;
也就是说,如果您的机器为2个2核CPU,那么CLR线程池的容量默认上限便是1000
(5)通过线程池创建的线程默认为后台线程,优先级默认为Normal。
2、线程池逻辑图:
3、对象池的清理方法:
(1)手动清理,即主动调用清理的方法,如GC。
(2)自动清理,即通过System.Threading.Timer来实现定时清理。
七、保证C#线程安全的手段-线程使用委托(函数回调)
(1)ThreadStart委托中作为参数的方法不需要参数,并且没有返回值。
(2)ParameterizedThreadStart委托一个对象作为参数,利用这个参数可以很方便地向线程传递参数
1、Thread类接收ThreadStart委托的模板
Thread thread=new Thread(new ThreadStart(method)); // 创建线程
thread.Start(); // 启动线程
2、ParameterizedThreadStart委托的构造函数的模板
Thread thread=new Thread(new ParameterizedThreadStart(method)); // 创建线程
thread.Start(3); // 启动线程
本文来自博客园,作者:꧁执笔小白꧂,转载请注明原文链接:https://www.cnblogs.com/qq2806933146xiaobai/p/12751118.html