详解C#多线程编程之进程与线程
作者:白烟染黑墨 发布时间:2021-07-18 14:15:36
一、 进程
简单来说,进程是对资源的抽象,是资源的容器,在传统操作系统中,进程是资源分配的基本单位,而且是执行的基本单位,进程支持并发执行,因为每个进程有独立的数据,独立的堆栈空间。一个程序想要并发执行,开多个进程即可。
Q1:在单核下,进程之间如何同时执行?
首先要区分两个概念——并发和并行
并发:并发是指在一段微小的时间段中,有多个程序代码段被CPU执行,宏观上表现出来就是多个程序能”同时“执行。
并行:并行是指在一个时间点,有多个程序段代码被CPU执行,它才是真正的同时执行。
所以应该说进程之间是并发执行。对于CPU来讲,它不知道进程的存在,CPU主要与寄存器打交道。有一些常用的寄存器,如程序计数器寄存器,这个寄存器存储了将要执行的指令的地址,这个寄存器的地址指向哪,CPU就去哪。还有一些堆栈寄存器和通用寄存器等等等,总之,这些数据构成了一个程序的执行环境,这个执行环境就叫做”上下文(Context)“,进程之间切换本质就是保存这些数据到内存,术语叫做”保存现场“,然后恢复某个进程的执行环境,也即是”恢复现场“,整个过程术语叫做“上下文切换”,具体点就是进程上下文切换,这就是进程之间能并发执行的本质——频繁的切换进程上下文。这个功能是由操作系统提供的,是内核态的,对应用软件开发人员透明。
二、 线程
进程虽然支持并发,但是对并发不是很友好,不友好是指每开启一个进程,都要重新分配一部分资源,而线程相对进程来说,创建线程的代价比创建进程要小,所以引入线程能更好的提高并发性。在现代操作系统中,进程变成了资源分配的基本单位,而线程变成了执行的基本单位,每个线程都有独立的堆栈空间,同一个进程的所有线程共享代码段和地址空间等共享资源。相应的上下文切换从进程上下文切换变成了线程上下文切换。
三、 为什么要引入进程和线程#
提高CPU利用率,在早期的单道批处理系统中,如果执行中的代码需要依赖与外部条件,将会导致CPU空闲,例如文件读取,等待键盘信号输入,这将浪费大量的CPU时间。引入多进程和线程可以解决CPU利用率低这个问题。
隔离程序之间的数据(每个进程都有单独的地址空间),保证系统运行的稳定性。
提高系统的响应性和交互能力。
四、 在C#中创建托管线程
1. Thread类
在.NET中,托管线程分为:
前台线程
后台线程
一个.Net程序中,至少要有一个前台线程,所有前台线程结束了,所有的后台线程将会被公共语言运行时(CLR)强制销毁,程序执行结束。
如下将在控制台程序中创建一个后台线程
static void Main(string[] args)
{
var t = new Thread(() =>
{
Thread.Sleep(1000);
Console.WriteLine("执行完毕");
});
t.IsBackground = true;
t.Start();
}
主线程(默认是前台线程)执行完毕,程序直接退出。
2. 有什么问题
直接使用Thread类来进行多线程编程浪费资源(服务器端更加明显)且不方便,举个栗子。
假如我写一个Web服务器程序,每个请求创建一个线程,那么每一次我都要new一个Thread对象,然后传入处理HttpRequest的委托,处理完之后,线程将会被销毁,这将会导致浪费大量CPU时间和内存,在早期CPU性能不行和内存资源珍贵的情况下这个缺点会被放大,在现在这个缺点不是很明显,原因是硬件上来了。
不方便体现在哪呢?
无法直接获取另一个线程内未被捕捉的异常
无法直接获取线程函数的返回值
public static void ThrowException()
{
throw new Exception("发生异常");
}
static void Main(string[] args)
{
var t = new Thread(() =>
{
Thread.Sleep(1000);
ThrowException();
});
t.IsBackground = false;
try
{
t.Start();
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
}
上述代码将会导致程序奔溃,如下图。
要想直接获取返回值和可以直接从主线程捕捉线程函数内未捕捉的异常,我们可以这么做。
新建一个MyTask.cs文件,内容如下
using System;
using System.Threading;
namespace ConsoleApp1
{
public class MyTask
{
private Thread _thread;
private Action _action;
private Exception _innerException;
public MyTask()
{
}
public MyTask(Action action)
{
_action = action;
}
protected virtual void Excute()
{
try
{
_action();
}
catch(Exception e)
{
_innerException = e;
}
}
public void Start()
{
if (_thread != null) throw new InvalidOperationException("任务已经开始");
_thread = new Thread(() => Excute());
_thread.Start();
}
public void Start(Action action)
{
_action = action;
if (_thread != null) throw new InvalidOperationException("任务已经开始");
_thread = new Thread(() => Excute());
_thread.Start();
}
public void Wait()
{
_thread.Join();
if (_innerException != null) throw _innerException;
}
}
public class MyTask<T> : MyTask
{
private Func<T> _func { get; }
private T _result;
public T Result {
private set => _result = value;
get
{
base.Wait();
return _result;
}
}
public MyTask(Func<T> func)
{
_func = func;
}
public new void Start()
{
base.Start(() =>
{
Result = _func();
});
}
}
}
简单的包装了一下(不要在意细节),我们便可以实现我们想要的效果。
测试代码如下
public static void ThrowException()
{
throw new Exception("发生异常");
}
public static void Test3()
{
MyTask<string> myTask = new MyTask<string>(() =>
{
Thread.Sleep(1000);
return "执行完毕";
});
myTask.Start();
try
{
Console.WriteLine(myTask.Result);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
public static void Test2()
{
MyTask<string> myTask = new MyTask<string>(() =>
{
Thread.Sleep(1000);
ThrowException();
return "执行完毕";
});
myTask.Start();
try
{
Console.WriteLine(myTask.Result);
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
}
public static void Test1()
{
MyTask myTask = new MyTask(() =>
{
Thread.Sleep(1000);
ThrowException();
});
myTask.Start();
try
{
myTask.Wait();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
static void Main(string[] args)
{
Test1();
Test2();
Test3();
}
可以看到,我们可以通过简单包装Thread对象,便可实现如下效果
直接读取线程函数返回值
直接捕捉线程函数未捕捉的异常(前提是调用了Wait()函数或者Result属性)
这是理解和运用Task的基础,Task功能非常完善,但是运用好Task需要掌握许多概念,下篇文章再说。
来源:https://www.cnblogs.com/hkfyf/p/13171183.html


猜你喜欢
- AssertJ是我目前见过的最强大的断言api,没有之一。官网传送门为什么使用assertJ?1、流式断言,代码即用例,直观易懂。举个例子:
- 1、确定本地网络是通的:2、确定SpringBootq启动后是不报错的3、查看是不是自己在配置文件中加入了项目路径:如果加入了项目路径的话,
- 并发与并行并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点
- 目录前言Binder的使用模糊进程间调用Binder原理ioctlbinder初始化总结前言Binder是安卓中实现IPC(进程间通信的)常
- Java8实现菜单树形数据当我们打开京东商城时,左侧的菜单依次分为 * 展示,这是如何实现的呢?本篇暂不讲述前端,只讲述如何使用java8 的
- 本文实例为大家分享了Android仿iphone自定义滚动选择器的具体代码,供大家参考,具体内容如下一、多的不说,效果图,先走起二、实例源码
- 一、 搭建struts2环境在myeclipse下,右击项目->MyEclipse->Project Facets->in
- 用途项目中使用了 dubbo,注册中心使用的 zookeeper,使用 zookeeper 实现了一个简单的分布式锁(依赖 curator)
- IDEA全局替换通过快捷键 Ctrl+Shift+r 或这点击 Edit 》Find 》Replace In Path有些IDEA版本按了快
- 目录synchronized 实现原理适应性自旋(Adaptive Spinning)锁升级Java 对象头偏向锁(Biased Locki
- [LeetCode] 169. Majority Element 求大多数Given an array nums of
- Object是所有类的父类,任何类都默认继承Object。一、Object类中的方法1.clone方法保护方法,实现对象的浅复制,只有实现了
- 很多童鞋反应在吧项目导入到eclipse(myeclipse)时中文会有乱码,修改了编码格式后还是乱码,这里给大家介绍一下关于中文乱码时修改
- 我们先要记住三者的特征:String 字符串常量StringBuffer 字符串变量(线程安全)StringBuilder 字符串变量(非线
- 线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在。一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组
- using System.Runtime.InteropServices; using System.Text; publicclass F
- AtomicInteger 类底层存储一个int值,并提供方法对该int值进行原子操作。AtomicInteger 作为java.util.
- 1.概述项目中经常会遇到一个应用需要访问多个数据源的情况,本文介绍在SpringBoot项目中利用SpringDataJpa技术如何支持多个
- 一、问题描述今天使用SDK Manager将Android SDK的版本更新到了Android 5.1的版本,eclipse创建androi
- 前言在实际的开发过程中,我们经常修改代码之后,手动的重启项目,查看修改效果。那么有没有一种方式能够快速的、自动的帮我们将修改代码自动更新,避