参考文档
1. 基础概念
1.1 进程/线程/多线程
进程(
Process
)
计算机概念,程序在服务器运行时占据全部计算机资源综合指标,虚拟的数据;普通的解释就是,进程是程序的一次执行
线程(
Thread
)
计算机概念,进程在响应操作时最小单元,也包含CPU/内存/网络/硬盘IO,是虚拟的概念;线程可以理解为进程中的执行的一段程序片段
句柄
操作系统用来标识应用程序的某一个最小单元,比如一个窗体标识,一个按钮标识等,一个long
数字,操作系统通过句柄识别应用程序,也可以通过句柄操作控件
多线程
计算机概念,一个进程有多个线程同时运行
2. C#中的线程(Thread
)
在C#中,Thread
类是操作线程的,是C#对线程的一个封装
说明:这种方式不是特别重要,因为此操作线程方法是.NET Framework 1.0低版本的,只做学习了解即可
2.1 构造函数
线程函数通过委托传递,可以不带参数,也可以带参数(只能有一个参数),可以用一个类或结构体封装参数
方式一,实例一个无参数,无返回值的线程对象
ThreadStart threadStart = () => Console.WriteLine("无参数无返回值委托");
Thread thread = new Thread(threadStart);
thread.Start();
方式二:实例一个有参数,无返回值的线程对象
ParameterizedThreadStart threadStart = (v) => Console.WriteLine("有参数无返回值委托:" + v);
Thread thread = new Thread(threadStart);
thread.Start("libai");
2.2 属性,方法
静态属性(常用)
属性名 | 属性值类型 | 描述 |
---|---|---|
Thread.CurrentThread | Thread | 获取当前正在运行的线程 |
Thread.CurrentContext | Context | 获取当前线程的上下文 |
实例属性(常用)
属性名 | 属性值类型 | 描述 |
---|---|---|
thread.ThreadState | ThreadState | 获取当前线程状态 |
thread.Priority | ThreadPriority | 获取或设置线程优先级 |
thread.Name | string | 获取或设置线程名称 |
thread.ManagedThreadId | int | 获取线程唯一ID |
thread.IsBackground | bool | 获取或设置线程是否为后台线程 |
实例方法(常用)
方法名 | 返回值类型 | 描述 |
---|---|---|
thread.Start(...) | void | 启动线程 |
thread.Abort(...) | void | 终止线程 |
thread.Join(...) | bool | 等待线程 |
2.3 线程等待方式
方式一:持续判断线程状态,直到线程停止
using System;
using System.Threading;
namespace th3
{
class Program
{
static void Main(string[] args)
{
{
ThreadStart threadStart = () => Console.WriteLine("无参数无返回值委托");
Thread thread = new Thread(threadStart);
thread.Start();
while (thread.ThreadState != ThreadState.Stopped)
{
Thread.Sleep(200);
}
}
}
}
}
方式二:
Join
方法
using System;
using System.Threading;
namespace th3
{
class Program
{
static void Main(string[] args)
{
{
ThreadStart threadStart = () => Console.WriteLine("无参数无返回值委托");
Thread thread = new Thread(threadStart);
thread.Start();
thread.Join(); // 一直等待,直到结束
thread.Join(3000); // 最多等待 3000 毫秒
}
}
}
}
2.4 线程状态值
// 指定的执行状态
public enum ThreadState
{
// 启动线程
Running = 0,
// 正在请求线程停止,仅供内部使用
StopRequested = 1,
// 正在请求线程挂起
SuspendRequested = 2,
// 该线程将作为后台线程
Background = 4,
Unstarted = 8,
// 该线程已停止
Stopped = 16,
// 线程将受阻
WaitSleepJoin = 32,
// 该线程已挂起
Suspended = 64,
AbortRequested = 128,
Aborted = 256
}
2.5 线程的优先级别
.NET为线程设置Priority
属性来定义线程执行的优先级别,里面包含5个选项,其中Normal
是默认值,除非系统有特殊要求,否则不应该随便设置线程的优先级别;因为线程是计算机概念,执行由操作系统执行,所以这个属性只是增加优先级概率,并不一定按照设置的优先级执行
ThreadStart threadStart = () => Console.WriteLine("无参数无返回值委托");
Thread thread = new Thread(threadStart);
thread.Priority = ThreadPriority.Highest;
thread.Start();
线程优先级选项:
// 指定的调度优先级的
public enum ThreadPriority
{
// 可以安排在具有任何其他优先级的线程之
Lowest = 0,
// 可以将 Thread 安排在具有 Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前
BelowNormal = 1,
// 可以将 Thread 安排在具有 AboveNormal 优先级的线程之后,在具有 BelowNormal 优先级的线程之前
Normal = 2,
// 可以将 Thread 安排在具有 Highest 优先级的线程之后,在具有 Normal 优先级的线程之前
AboveNormal = 3,
// 可以将 Thread 安排在具有任何其他优先级的线程之前
Highest = 4
}
2.6 线程池(ThreadPool
)
概念说明
由来:在 Thread
中对线程的管理需要手动操作,在不断的开启和销毁中,存在很大的开销,为了让线程反复使用,出现了池化思想;线程池可以节省资源,控制线程总数量,防止滥用
线程池维护一个请求队列,线程池的代码从队列提取任务,然后委派给线程池的一个线程执行,线程执行完不会被立即销毁,这样既可以在后台执行任务,又可以减少线程创建和销毁所带来的开销
线程池并不是在 CLR 初始化的时候立刻创建线程的,而是在应用程序要创建线程来执行任务的时候,线程池才会初始化一个线程,初始化的线程和其他线程一样,但是在线程完成任务之后不会自行销毁,而是以挂起的状态回到线程池。当应用程序再次向现成池发出请求的时候,线程池里挂起的线程会再度激活执行任务。这样做可以减少线程创建和销毁所带来的开销
在C#中,提供了操作线程池的静态帮助类ThreadPool
,并提供一些静态方法操作线程池
静态方法(常用)
方法 | 返回值类型 | 描述 |
---|---|---|
ThreadPool.QueueUserWorkItem(...) | bool | 可以理解为开启一个线程,成功true |
ThreadPool.GetMaxThreads(...) | void | 获取本机最大辅助,异步I/O线程数 |
ThreadPool.GetMinThreads(...) | void | 获取本机最小辅助,异步I/O线程数 |
ThreadPool.SetMaxThreads(...) | bool | 设置本机最大辅助,异步I/O线程数 |
ThreadPool.SetMinThreads(...) | bool | 设置本机最小辅助,异步I/O线程数 |
代码示例
示例一:开启一个线程
using System;
using System.Threading;
namespace th3
{
class Program
{
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(o=>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00"));
Tesk();
});
Thread.Sleep(2000);
}
static void Tesk()
{
Console.WriteLine("开启了一个线程");
}
}
}
示例二:开启一个线程并传入参数
using System;
using System.Threading;
namespace th3
{
class Program
{
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(o =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00"));
Tesk(o);
}, "libai");
Thread.Sleep(2000);
}
static void Tesk(object value)
{
Console.WriteLine("开启了一个线程,参数:" + value);
}
}
}
示例三:获取线程池中辅助线程,异步 I/O 线程的最大,最小数目
using System;
using System.Threading;
namespace th3
{
class Program
{
static void Main(string[] args)
{
ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads);
Console.WriteLine($"辅助线程数,最大【{maxWorkerThreads}】,最小【{minWorkerThreads}】");
Console.WriteLine($"异步I/O线程数,最大【{maxCompletionPortThreads}】,最小【{minCompletionPortThreads}】");
}
}
示例四:设置线程池最大最小线程数(这个设置是全局的)
using System;
using System.Threading;
namespace th3
{
class Program
{
static void Main(string[] args)
{
ThreadPool.SetMaxThreads(100, 100); // 设置最大数量时不能低于本机线程数,否则设置无效
ThreadPool.SetMinThreads(1, 2);
ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads);
Console.WriteLine($"辅助线程数,最大【{maxWorkerThreads}】,最小【{minWorkerThreads}】");
Console.WriteLine($"异步I/O线程数,最大【{maxCompletionPortThreads}】,最小【{minCompletionPortThreads}】");
}
}
}
3. Task任务
3.1 简述说明
Task
对象
Task
对象是一种的中心思想基于任务的异步模式首次引入.NET Framework 3
中,表示一个任务
System.Threading.Tasks.Task
类是Task Programming Library(TPL)
中最核心的一个类
注意的是,Task
里面的线程是来自线程池的
任务与线程
任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行,但是任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制
如下图所示,任务最后是托管到线程上执行的
3.2 创建方式
创建Task
有三种方式,使用构造函数创建,使用Task.Run
创建,使用 Task.Factory.StartNew
进行创建,这三种方式都是一样的,不过Task.Factory
是对Task
进行管理,调度管理这一类的
方式一:使用构造函数创建
using System;
using System.Threading;
using System.Threading.Tasks;
namespace th3
{
class Program
{
static void Main(string[] args)
{
Task task1 = new Task(() =>
{
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
task1.Start();
Thread.Sleep(2000);
}
}
}
方式二:使用
Task.Run
创建
using System;
using System.Threading;
using System.Threading.Tasks;
namespace th3
{
class Program
{
static void Main(string[] args)
{
Task task1 = Task.Run(() =>
{
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
Thread.Sleep(2000);
}
}
}
方式三:使用
Task.Factory.StartNew
进行创建并运行
using System;
using System.Threading;
using System.Threading.Tasks;
namespace th3
{
class Program
{
static void Main(string[] args)
{
// 方式一:使用静态工厂属性
{
Task task1 = Task.Factory.StartNew(() =>
{
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
}
// 方式二:使用实例工厂
{
TaskFactory factory = new TaskFactory();
Task task1 = factory.StartNew(() =>
{
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
}
Thread.Sleep(2000);
}
}
}
重载参数补充说明
在创建Task
的时候,Task
有很多的构造函数的重载,一个主要的重载就是传入TaskCreateOptions
的枚举
-
TaskCreateOptions.None
:用默认的方式创建一个Task
-
TaskCreateOptions.PreferFairness
:请求scheduler
尽量公平的执行Task
(优先级) -
TaskCreateOptions.LongRunning
:声明Task
将会长时间的运行 -
TaskCreateOptions.AttachToParent
:因为Task
是可以嵌套的,所以这个枚举就是把一个子task
附加到一个父task
中
3.3 属性,方法
实例属性(常用)
属性名 | 属性值类型 | 描述 |
---|---|---|
task1.Id | int | 获取此Task 实例的 ID |
task1.IsCompleted | bool | 获取此Task 是否已完成 |
task1.Status | TaskStatus | 获取此任务的任务状态 |
runTask.Result | T | 获取任务返回值 |
实例方法(常用)
方法名 | 返回值类型 | 描述 |
---|---|---|
task1.Start(...) | void | 启动,开启一个线程 |
task1.ContinueWith(...) | Task | 创建一个在目标Task 完成时异步执行的延续任务 |
task1.Wait(...); | bool | 等待任务完成 |
静态方法(常用)
方法名 | 返回值类型 | 描述 |
---|---|---|
Task.WaitAny(...) | int | 等待提供的任一 Task 对象完成执行过程 |
Task.WaitAll(...) | void | 等待提供的所有Task 对象完成执行过程 |
代码示例
示例一:WaitAny
,WaitAll
方法示例(结果会受计算机性能影响,建议执行多次)
using System;
using System.Threading;
using System.Threading.Tasks;
namespace th3
{
class Program
{
static void Main(string[] args)
{
{
Task task1 = new Task(() =>
{
Thread.Sleep(2000);
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
Task task2 = new Task(() =>
{
Thread.Sleep(2000);
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
task1.Start();
task2.Start();
Task.WaitAny(task1, task2);
Console.WriteLine("执行完成WaitAny");
Thread.Sleep(2000);
}
Console.WriteLine("============================================");
{
Task task1 = new Task(() =>
{
Thread.Sleep(2000);
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
Task task2 = new Task(() =>
{
Thread.Sleep(2000);
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
task1.Start();
task2.Start();
Task.WaitAll(task1, task2);
Console.WriteLine("执行完成WaitAll");
Thread.Sleep(2000);
}
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
namespace th3
{
class Program
{
static void Main(string[] args)
{
{
Task task1 = new Task(() =>
{
Thread.Sleep(3000);
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
Task task2 = new Task(() =>
{
Thread.Sleep(2000);
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
task1.Start();
task2.Start();
Task.WaitAny(task1, task2);
Console.WriteLine("有一个执行结束");
Task.WaitAll(task1, task2);
Console.WriteLine("全部执行结束");
}
}
}
}
3.4 常用示例
1. 示例:连续任务
所谓的延续的Task
就是在第一个Task
完成后自动启动下一个Task
,参数为上次Task
的引用
示例一:单个连续任务
using System;
using System.Threading;
using System.Threading.Tasks;
namespace th3
{
class Program
{
static void Main(string[] args)
{
{
Task task1 = new Task(() =>
{
Thread.Sleep(2000);
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
Task task2 = new Task(() =>
{
Thread.Sleep(1000);
Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
});
task1.ContinueWith(t=>
{
task2.Start();
});
task1.Start();
Thread.Sleep(5000);
}
}
}
}
示例二:多个连续任务
using System;
using System.Threading;
using System.Threading.Tasks;
namespace th3
{
class Program
{
static void Main(string[] args)
{
var SendTask = Task.Factory.StartNew(() => Console.WriteLine("无参数"))
.ContinueWith<bool>(s => { return true; })
.ContinueWith<string>(r =>
{
Console.WriteLine(r.Result);
return "libai";
});
Console.WriteLine(SendTask.Result);
}
}
}
2. 示例:任务结果
示例一:单个任务结果
using System;
using System.Threading.Tasks;
namespace th3
{
class Program
{
static void Main(string[] args)
{
// 方式一
{
Task<int> task = new Task<int>(()=> { return 1; });
task.Start();
Console.WriteLine(task.Result);
}
// 方式二
{
Task<int> task = Task.Run<int>(()=> { return 1; });
Console.WriteLine(task.Result);
}
}
}
}
3.5 线程同步
所谓同步:是指在某一时刻只有一个线程可以访问变量
c#为同步访问变量提供了一个非常简单的方式,即使用c#语言的关键字Lock
,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待
关键字
Lock
定义
-
expression
代表你希望跟踪的对象:- 如果你想保护一个类的实例,一般地,你可以使用
this
- 如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可
- 如果需要锁变量,推荐使用
object
类型
- 如果你想保护一个类的实例,一般地,你可以使用
-
statement_block
就算互斥段的代码,这段代码在一个时刻内只可能被一个线程执行
Lock(expression)
{
statement_block
}
示例代码
namespace T1
{
class Program
{
static void Main(string[] args)
{
BookShop book = new BookShop();
//创建两个线程同时访问Sale方法
Thread t1 = new Thread(new ThreadStart(book.Sale));
Thread t2 = new Thread(new ThreadStart(book.Sale));
//启动线程
t1.Start();
t2.Start();
Console.ReadKey();
}
}
class BookShop
{
//剩余图书数量
public int num = 1;
public void Sale()
{
//使用lock关键字解决线程同步问题
lock (this)
{
int tmp = num;
if (tmp > 0)//判断是否有书,如果有就可以卖
{
Thread.Sleep(1000);
num -= 1;
Console.WriteLine("售出一本图书,还剩余{0}本", num);
}
else
{
Console.WriteLine("没有了");
}
}
}
}
}
4. async/await
.NET 4.5版本时,C#提供Async
和Await
关键字(语法糖)来实现异步编程,两者是一组关键子,通常一起使用
Async
是用来修饰方法,Await
在方法内部,只能方法Async
修饰的方法内或者Task
前面
作用:用同步方法的思维写异步方法,提高了内存占用率
官方介绍
-
await
运算符暂停对封闭async
方法的求值,直到其操作数表示的异步操作完成 -
异步操作完成后,
await
运算符将返回操作的结果(如果有) -
当
await
运算符应用到表示已完成操作的操作数时,它将立即返回操作的结果,而不会暂停封闭的方法 -
await
运算符不会阻止计算异步方法的线程 -
若某个函数
F
的函数体中需要使用await
关键字的函数必须以async
标记
4.1 代码示例
示例一:执行顺序
using System;
using System.Threading;
using System.Threading.Tasks;
namespace th4
{
class Program
{
static void Main(string[] args)
{
Show();
Console.ReadKey();
}
static void Show()
{
Console.WriteLine("1\t" + Thread.CurrentThread.ManagedThreadId.ToString("00")); // 1.首先执行主线程
Async(); // 2.2.调用后返回主线程
Console.WriteLine("2\t" + Thread.CurrentThread.ManagedThreadId.ToString("00")); // 3.主线程执行
}
static async void Async()
{
Console.WriteLine("5\t" + Thread.CurrentThread.ManagedThreadId.ToString("00")); // 2.1.按照顺序执行,也是主线程
await Task.Run(() => // 4.开启子线程执行,使用await等待完成
{
Thread.Sleep(200);
Console.WriteLine("3\t" + Thread.CurrentThread.ManagedThreadId.ToString("00")); // 5.子线程执行
});
Console.WriteLine("4\t" + Thread.CurrentThread.ManagedThreadId.ToString("00")); // 6.等待子线程完成后执行
}
}
}
5. 扩展补充
5.1 为什么可以多线程
-
多个CPU的核可以并行工作,几核几线程,这里的线程指的是模拟核并行;多核之间叫并行
-
CPU分片,1秒的处理能力分成1000份,操作系统调度着去响应不同的任务,从宏观角度来说,感觉就是多个任务在并行执行,从微观角度来说,一个物理CPU同一时刻只能为一个任务服务,并发:CPU分片的并发
5.2 同步异步特点
可等待
同步方法:主线程(UI线程)忙于计算,无暇其它(卡界面)
异步多线程方法:主线程闲置,计算任务交给子线程完成(不卡界面)
执行速度
同步方法:慢,因为只有一个线程计算
异步多线程方法:快,因为多个线程并发计算,CPU利用率高(密集型计算),以资源换性能
执行顺序
同步方法:有序执行
异步多线程方法:启动无序,线程资源是向操作系统申请的,由操纵系统的调度策略决定,所以启动顺序随机;同一个任务同一个线程,执行时间也不确定,CPU分片结束也无序
5.3 如何实现线程有序执行?
控制线程执行顺序有四种方法可实现
方法一:使用委托回调,回调方法是在子线程中执行
using System;
namespace th2
{
class Program
{
static void Main(string[] args)
{
Action<string> action = Tesk1;
action.Invoke("libai");
Console.WriteLine("end");
AsyncCallback callback = re =>
{
Console.WriteLine("end");
};
action.BeginInvoke("zhaoyun",callback,null);
}
static void Tesk1(string key)
{
Console.WriteLine("start:" + key);
}
}
}
5.4 线程/进程的区别
-
进程间是独立的,这表现在内存空间,上下文环境;
线程运行在进程空间内,进程包含线程(包含关系)
-
进程是无法突破进程存取其他进程内的存储空间;
而线程由于处于进程空间内,所以同一进程所产生的线程共享同一内存空间。
-
同一进程中的两段代码不能够同时执行,除非引入线程
-
线程是属于进程的,当进程退出时该进程所产生的线程都会被强制退出并清除
5.5 前台线程,后台线程
通过设置线程的IsBackground
属性,设置线程为前台或后台线程,通常用在WinFrom
程序
using System;
using System.Threading;
namespace th3
{
class Program
{
static void Main(string[] args)
{
{
ThreadStart threadStart = () => Console.WriteLine("无参数无返回值委托");
Thread thread = new Thread(threadStart);
thread.IsBackground = true; // 后台线程,进程结束,线程结束
thread.IsBackground = false; // 前台线程,进程结束,等待线程执行结束
thread.Start();
}
}
}
}
6. 应用示例
6.1 示例一:封装线程回调
需求:封装一个方法,当子线程执行结束后,调用回调方法
思路:首先肯定是需要一个子线程来执行的,其次回调方法必须是线程要执行的内容结束后调用;所以可以在其外部加一层,因为子线程是不卡主线程的,可以在子线程内部依次等待调用
using System;
using System.Threading;
namespace th3
{
class Program
{
static void Main(string[] args)
{
{
ThreadStart threadStart = () =>
{
Console.WriteLine("开始执行");
Thread.Sleep(2000);
};
Action action = () =>
{
Console.WriteLine("执行结束");
};
ThreadWithCallBack(threadStart, action);
}
}
static void ThreadWithCallBack(ThreadStart threadStart, Action callback)
{
ThreadStart start = () =>
{
threadStart.Invoke();
callback.Invoke();
};
Thread thread = new Thread(start);
thread.Start();
}
}
}