当前位置: 首页 > news >正文

C#多线程基本使用和探讨

线程是并发编程的基础概念之一。在现代应用程序中,我们通常需要执行多个任务并行处理,以提高性能。C# 提供了多种并发编程工具,如ThreadTask、异步编程和Parallel等。

Thread 类

Thread 类是最基本的线程实现方法。使用Thread类,我们可以创建并管理独立的线程来执行任务。

基本使用Thread 

创建一个新的实例对象,将一个方法直接给Thread类,并使用实例对象启动线程运行。

   static void Test() {Console.WriteLine("Test事件开始执行");Thread.Sleep(1000);Console.WriteLine("Test事件睡眠之后打印数据");Console.WriteLine("Test事件id: " + Thread.CurrentThread.ManagedThreadId);}static void Main(string[] args){#region //主线程idConsole.WriteLine("主线程id: " + Thread.CurrentThread.ManagedThreadId);#endregion#region //传递一个方法给线程,执行方法Thread t = new Thread(Test);t.Start();#endregion
}

线程数据传输

传递单个参数

Thread 提供了一个 Start(object parameter) 方法,可以在启动线程时传递一个参数。

static int Downlod(object obj) {string str = obj as string;Console.WriteLine(str);}static void Main(string[] args){Thread t1 = new Thread(Downlod);//这里传递的参数是字符串t1.Start("数据传递方法执行 ,数据传递方法id: "+Thread.CurrentThread.ManagedThreadId);}

这种方式适用于需要传递单个参数的情况。

使用类的实例方法传递多个参数

先new一个类的实例,给实例字段赋值,调用类的实例,通过实例调用方法,构造函数接收参数,然后在线程中调用该实例的方法。

     static void Main(string[] args){
// 使用 DownloadTool 实例化并传递数据
DownloadTool downloadTool = new DownloadTool("www.baidu.com", "这是下载链接哦");
Thread thread = new Thread(downloadTool.Download);
thread.Start();
}public class DownloadTool
{private string url;private string message;public DownloadTool(string url, string message){this.url = url;this.message = message;}public void Download(){Console.WriteLine("下载链接: " + url);Console.WriteLine("提示信息: " + message);Console.WriteLine("Download 线程 ID: " + Thread.CurrentThread.ManagedThreadId);}
}

thread 线程启动时,它会执行 downloadTool.Download() 方法,输出传递的数据。

线程优先级 

在 C# 中,可以使用 Thread.Priority 属性来设置线程的优先级。线程优先级决定了操作系统在多线程环境中调度线程的顺序,但并不保证高优先级的线程总是比低优先级的线程更早或更频繁地执行。

线程优先级级别

C# 提供了五个线程优先级级别,定义在 ThreadPriority 枚举中:

  1. Lowest:最低优先级。操作系统尽可能少地调度这个优先级的线程。
  2. BelowNormal:低于正常的优先级。优先级比 Normal 低,但高于 Lowest。
  3. Normal:默认优先级,大多数线程默认的优先级。适用于一般用途。
  4. AboveNormal:高于正常的优先级。操作系统更倾向于调度这个优先级的线程。
  5. Highest:最高优先级。操作系统尽可能多地调度这个优先级的线程。
  internal class Program{static void A(){int i = 0;while (true){i++;Console.WriteLine($"A 输出第{i}");Thread.Sleep(1000);}}static void B(){int i = 0;while (true){i++;Console.WriteLine($"B 输出第{i}");Thread.Sleep(1000);}}static void Main(string[] args){//在C#中,线程的优先级可以通过Thread.Priority属性来设置和获取。
//        Lowest: 线程的优先级是最低的。在系统中存在其他活动线程时,此优先级的线程很少得到执行。
//BelowNormal: 线程的优先级低于正常线程。
//Normal: 线程的优先级是普通的,这是线程的默认优先级。
//AboveNormal: 线程的优先级高于正常线程。
//Highest: 线程的优先级是最高的。此优先级的线程会尽量优先于其他所有优先级的线程执行。Thread a = new Thread(A);Thread b = new Thread(B);a.Priority = ThreadPriority.Highest;a.Start();b.Priority = ThreadPriority.Lowest;b.Start();Console.WriteLine("按任意键停止线程...");Console.ReadKey();a.Join();b.Join();Console.WriteLine("线程已停止");}}
  • A 被设置为最高优先级 ThreadPriority.Highest
  • B 被设置为最低优先级 ThreadPriority.Lowest

注意事项

  1. 优先级不是绝对控制:操作系统可能会忽略优先级设置,特别是在资源有限的系统中。高优先级线程不一定会一直执行,也不能阻止低优先级线程的执行。

  2. 使用优先级的适用场景:设置线程优先级可能适用于实时系统(例如,某些任务需要优先处理)。但是,大多数应用程序通常可以使用默认的 Normal 优先级。

  3. 避免使用过多的高优先级线程:如果所有线程都被设置为 Highest,系统的整体性能可能会下降,甚至导致线程争用 CPU 资源的情况。

  4. CPU 密集型任务:在 CPU 密集型任务中,优先级可能会对性能产生较大影响,因为优先级高的线程可能会占用更多的 CPU 时间。

线程优先级的最佳实践

  • 默认使用 Normal 优先级,除非有特殊原因。
  • 避免滥用 Highest 优先级,因为它会对系统资源产生影响。
  • 在 I/O 密集型的线程中,优先级通常不会有显著差异,因为这些线程在等待 I/O 操作完成时,CPU 会调度其他线程。

通过合理设置线程优先级,可以帮助操作系统更好地调度线程,以满足应用程序的需求。 但通常在 .NET 应用程序中,多数情况下使用默认的 Normal 优先级就足够了。

线程池

线程池 (ThreadPool) 是一种高效的管理和调度线程的方式。线程池自动管理线程的创建、重用和销毁,从而减少了手动创建和管理线程的开销。

为什么使用线程池

  1. 性能更高:线程池会重用现有的线程,减少了创建和销毁线程的开销。
  2. 自动管理:线程池会根据系统负载动态调整线程数量。
  3. 避免线程资源不足:线程池限制了同时运行的线程数,避免了线程过多导致的资源耗尽问题。

基本使用 ThreadPool.QueueUserWorkItem 方法将任务排入线程池队列。

using System;
using System.Threading;class Program
{static void Main(string[] args){// 将任务排入线程池ThreadPool.QueueUserWorkItem(DoWork, "任务 1");ThreadPool.QueueUserWorkItem(DoWork, "任务 2");Console.WriteLine("主线程完成");Thread.Sleep(3000); // 等待线程池中的任务完成}static void DoWork(object state){string taskName = (string)state;Console.WriteLine($"{taskName} 开始执行 - 线程ID: {Thread.CurrentThread.ManagedThreadId}");Thread.Sleep(1000); // 模拟耗时操作Console.WriteLine($"{taskName} 执行完成 - 线程ID: {Thread.CurrentThread.ManagedThreadId}");}
}

运行结果QueueUserWorkItem自动分配线程来执行任务。

QueueUserWorkItem 方法将任务排入线程池。它接收一个委托(即方法)和一个可选的状态对象(传递给方法的数据)。

DoWork 方法接受一个参数 state,这是从 QueueUserWorkItem 传递的。

Thread.Sleep(3000) 确保主线程不会立即退出,使得线程池中的任务有机会完成。

使用带返回值的线程池任务

C# 中的 ThreadPool 通常不直接支持返回值。如果需要获得任务结果,可以使用 Task,因为 Task 本质上也是线程池的一部分。Task 更适合于带返回值的异步操作。这里使用 Task.Run 来代替 ThreadPool

     static void Main(string[] args){//使用tesk多线程int a= Task.Run(() =>{int a =  Dowload();return a;}).Result;Task<int> task = Task<int>.Run(()=>{int a =   Dowload();return a;});//初始化一个CancellationTokenSource实例CancellationTokenSource source = new CancellationTokenSource();//task.Start();task.Wait(1000);source.Cancel();int result  =    task.Result;Console.WriteLine(result);Console.WriteLine($"tesk返回值{a}");
}static int  Dowload() {int a = 0;for (int i = 0; i < 10; i++){a=  a + i + 1;}int?  id= Task.CurrentId;Console.WriteLine("Current thread ID: " + id);return a;}

线程池的限制

  • 任务运行时间过长:线程池中的线程本质上是共享资源,如果某个任务运行时间太长,将会占用线程池中的线程,导致其他任务无法及时执行。
  • 不适合实时系统:线程池中的任务调度是由系统管理的,无法保证精确的实时性。
  • 有限的线程数量:在高并发场景中,如果线程池中的线程全部被占用,新的任务将会等待,直到有线程可用。
线程池总结

线程池是一种高效的并发处理方式,适合于大多数轻量级的后台任务。在现代 C# 编程中,建议使用 Taskasync/await 进行异步操作,因为它们能简化代码,并且使用底层的线程池来管理线程。如果需要精确控制线程的执行,通常建议使用手动管理的 Thread 等。 

线程锁

使用多线程,在多线程编程中,如果多个线程同时访问和修改共享资源(如全局变量、文件、数据库等),可能会导致数据不一致或竞争条件。为了避免这种情况,多线程锁通过控制对共享资源的访问来保证线程安全性。

资源冲突示例:
  static void Main(string[] args){//调用方法循环创建新的线程来执行方法StateObject state = new StateObject();for (int i = 0; i < 30; i++){Thread thread = new Thread(state.ChangState);thread.Start();}Console.ReadKey();}//一个StateObject类public class StateObject{private int state = 5;
//里面有个状态改变方法,当状态等于5的时候进入到方法中,然后state+1 打印的应该是6 和线程idpublic void ChangState(){if (state == 5){state++;Console.WriteLine($"state:{state}  线程id:" + Thread.CurrentThread.ManagedThreadId);}state = 5;}}

 运行结果:

因为资源冲突的原因,有一些线程执行的时候,可能另外一个线程没有执行完,另外一个线程就已经进入到方法里面了。因为有修改的操作导致State状态混乱,资源冲突。这时候我们就需要一个东西来维护,让线程执行指定方法时一个一个的执行,不会冲突。

简单使用lock锁

1.创建一个private readonly object lockObject = new object(); // 锁对象

2.使用lock (lockObject){

//业务代码,执行到有修改的操作的代码

} // 使用锁对象来确保线程安全

        static void Main(string[] args){StateObject state = new StateObject();for (int i = 0; i < 100; i++){Thread thread = new Thread(state.ChangState);thread.Start();}Console.ReadKey();}public class StateObject{private object _lock = new object();private int state = 5;public void ChangState(){//使用锁保证每次执行方法的都是一个线程,防止资源冲突lock (_lock){if (state == 5){state++;Console.WriteLine($"state:{state}  线程id:" + Thread.CurrentThread.ManagedThreadId);}state = 5;}}}

 这样运行起来就能把每个线程操作隔离开,保证state的状态不会冲突。

为什么要创建一个 object 对象作为锁?

  1. 专用性:创建一个专用的锁对象(如 private readonly object lockObject = new object();),可以确保锁定的是特定的同步逻辑,而不是其他对象。这有助于避免意外的锁冲突或死锁。
  2. 避免使用其他共享对象:虽然可以使用任意的引用类型对象作为锁对象(包括 thisType 对象),但这可能会带来不必要的风险,尤其是在 public 方法或对象中,这样可能会导致意外的锁定冲突。

常见问题死锁:

锁并不是万能,也不是有锁就是最好,要看情况使用,锁也会产生问题,常见的问题就是死锁等问题。

演示死锁:

thread1方法1 的锁里面嵌套锁住了thread2的锁,thread2方法2的锁里面嵌套锁住了thread1的锁,这种锁与锁嵌套使用,就是容易出问题。导致线程锁死程序无法动弹。

   static void Main(string[] args){DeadlockExample deadlockExample = new DeadlockExample();Thread t1 = new Thread(deadlockExample.Thread1);Thread t2 = new Thread(deadlockExample.Thread2);t1.Start();t2.Start();t1.Join();t2.Join();Console.ReadKey();}}public  class DeadlockExample{private static object lock1 = new object();private static object lock2 = new object();public  void Thread1(){lock (lock1){Console.WriteLine("线程1:已获取锁1,正在等待锁2。。。");Thread.Sleep(100);  // 模拟某些工作lock (lock2){Console.WriteLine("线程1:获得锁2");}}}public  void Thread2(){lock (lock2){Console.WriteLine("线程2:已获取锁2,正在等待锁1。。。");Thread.Sleep(100);  // 模拟某些工作lock (lock1){Console.WriteLine("线程2:获得锁1");}}}}

运行结果:

死锁发生在两个或多个线程相互等待对方持有的资源,导致所有线程都无法继续执行。

 

总结:

多线程锁在 C# 中主要用于解决以下问题:

  1. 竞态条件:通过锁机制防止多个线程同时访问和修改共享资源,确保数据一致性。
  2. 死锁:防止多个线程相互等待资源,通过锁的顺序或者避免嵌套锁来解决。
  3. 资源饥饿:确保每个线程都能获取资源,使用 Monitor.TryEnter 等机制防止无限等待。
  4. 读写锁:允许多个线程并发读取资源,但写入时互斥,适合读多写少的场景。

C# 提供了多种锁机制,开发者可以根据应用场景选择合适的锁类型。

如果不想使用 lock 关键字,C# 还提供了其他锁机制,比如 MutexSemaphoreMonitor

相关文章:

C#多线程基本使用和探讨

线程是并发编程的基础概念之一。在现代应用程序中&#xff0c;我们通常需要执行多个任务并行处理&#xff0c;以提高性能。C# 提供了多种并发编程工具&#xff0c;如Thread、Task、异步编程和Parallel等。 Thread 类 Thread 类是最基本的线程实现方法。使用Thread类&#xff0…...

PHP DateTime基础用法

PHP DateTime 的用法详解 一、引言 在开发 PHP 应用程序时&#xff0c;处理日期和时间是一个至关重要的任务。PHP 提供了强大的日期和时间处理功能&#xff0c;其中 DateTime 类是最常用的工具之一。DateTime 类提供了丰富的方法来创建、格式化、计算和比较日期时间&#xff…...

一次Fegin CPU占用过高导致的事故

记录一下 一次应用事故分析、排查、处理 背景介绍 9号上午收到CPU告警&#xff0c;同时业务反馈依赖该服务的上游服务接口响应耗时太长 应用告警-CPU使用率 告警变更 【WARNING】项目XXX,集群qd-aliyun,分区bbbb-prod,应用customer,实例customer-6fb6448688-m47jz, POD实例CP…...

【Go初阶】两万字快速入门Go语言

初见golang语法 package mainimport "fmt"func main() {/* 简单的程序 万能的hello world */fmt.Println("Hello Go")} 第一行代码package main定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包&#xff0c;如&#xff1a;package main…...

【React】使用 react hooks 需要遵守的原则

1&#xff09;只能在顶层调用Hooks 这是指你不能在循环、条件语句或嵌套函数中调用Hooks。确保每次组件渲染时&#xff0c;Hooks的调用顺序保持一致。因此&#xff0c;你应该始终在React函数组件的最顶层调用Hooks。 React依赖于Hooks的调用顺序。如果这些调用在不同的渲染中顺…...

Python编程:创意爱心表白代码集

在寻找一种特别的方式来表达你的爱意吗&#xff1f;使用Python编程&#xff0c;你可以创造出独一无二的爱心图案&#xff0c;为你的表白增添一份特别的浪漫。这里为你精选了六种不同风格的爱心表白代码&#xff0c;让你的创意和情感通过代码展现出来。 话不多说&#xff0c;咱…...

腾讯IM SDK:TUIKit发送多张图片

一、问题描述 在使用腾讯IM DEMO&#xff08;https://github.com/TencentCloud/chat-uikit-vue.git&#xff09;时发现其只支持发送一张图片&#xff1a; 二、解决方案 // src\TUIKit\components\TUIChat\message-input-toolbar\image-upload\index.vue<inputref"inp…...

《本地部署开源大模型》在Ubuntu 22.04系统下ChatGLM3-6B高效微调实战

在Ubuntu 22.04系统下ChatGLM3-6B高效微调实战 无论是在单机单卡&#xff08;一台机器上只有一块GPU&#xff09;还是单机多卡&#xff08;一台机器上有多块GPU&#xff09;的硬件配置上启动ChatGLM3-6B模型&#xff0c;其前置环境配置和项目文件是相同的。如果大家对配置过程还…...

Python 脚本来自动发送每日电子邮件报告

安装必要的库 我们将使用 smtplib 发送邮件&#xff0c;以及 email.mime 来创建电子邮件内容。另外&#xff0c;为了让脚本自动定时运行&#xff0c;可以使用操作系统的计划任务工具&#xff08;如 Linux 的 cron 或 Windows 的 Task Scheduler&#xff09;。 创建邮件内容 使…...

大语言模型与ChatGPT:深入探索与应用

文章目录 1. 前言2. 大语言模型的概述2.1 什么是大语言模型&#xff1f;2.2 Transformer架构的核心2.3 预训练与微调 3. ChatGPT的架构与技术背景3.1 GPT模型的演进3.2 ChatGPT的工作原理 4. ChatGPT的实际应用4.1 日常对话助手4.2 内容生成与写作4.3 编程辅助4.4 教育与学习辅…...

【从零开始的LeetCode-算法】3164.优质数对的总数 II

给你两个整数数组 nums1 和 nums2&#xff0c;长度分别为 n 和 m。同时给你一个正整数 k。 如果 nums1[i] 可以被 nums2[j] * k 整除&#xff0c;则称数对 (i, j) 为 优质数对&#xff08;0 < i < n - 1, 0 < j < m - 1&#xff09;。 返回 优质数对 的总数。 示…...

FastDFS VS MinIO:文件存储与对象存储的抉择(包含SpringBoot集成FastDFS范例)

FastDFS vs MinIO&#xff1a;文件存储与对象存储的抉择&#xff08;包含SpringBoot集成FastDFS范例&#xff09; 我坐在窗边&#xff0c;随着飞机穿过云层&#xff0c;在云层之上滑翔。可以清晰的看到飞机在天空留下的痕迹&#xff0c;不知道那是蔚蓝中的纯白&#xff0c;还是…...

【Redis】缓存预热、雪崩、击穿、穿透、过期删除策略、内存淘汰策略

Redis常见问题总结&#xff1a; Redis常见问题总结Redis缓存预热Redis缓存雪崩Redis缓存击穿Redis缓存穿透 Redis 中 key 的过期删除策略数据删除策略 Redis内存淘汰策略一、Redis对过期数据的处理&#xff08;一&#xff09;相关配置&#xff08;二&#xff09;内存淘汰流程&a…...

【LeetCode】每日一题 2024_10_15 三角形的最大高度(枚举、模拟)

前言 每天和你一起刷 LeetCode 每日一题~ LeetCode 启动&#xff01; 题目&#xff1a;三角形的最大高度 代码与解题思路 久违的简单题 这道题读完题目其实不难想到有两条路可以走&#xff1a; 1、题目很明显只有两种情况&#xff0c;枚举是第一个球是红球还是蓝球这两种情…...

2024版最新网络安全工程师入门教程(非常详细)从零基础入门到精通,看完这一篇就够了

前言 想要成为网络安全工程师&#xff0c;却苦于没有方向&#xff0c;不知道从何学起的话&#xff0c;下面这篇 网络安全入门 教程可以帮你实现自己的网络安全工程师梦想&#xff0c;如果想学&#xff0c;可以继续看下去&#xff0c;文章有点长&#xff0c;希望你可以耐心看到…...

vue中关于router.beforeEach()的用法

router.beforeEach()是Vue.js中的路由守卫&#xff0c;用于在路由跳转前进行校验、取消、重定向等操作。 基本使用&#xff1a; const router new VueRouter({ ... })router.beforeEach((to, from, next) > {// ... }) to: 即将要进入的目标路由对象 from: 当前导航正要…...

C++模板初阶,只需稍微学习;直接起飞;泛型编程

&#x1f913;泛型编程 假设像以前交换两个函数需要&#xff0c;函数写很多个或者要重载很多个&#xff1b;那么有什么办法实现一个通用的函数呢&#xff1f; void Swap(int& x, int& y) {int tmp x;x y;y tmp; } void Swap(double& x, double& y) {doubl…...

【数据结构 | 红黑树】红黑树的性质和插入结点时的调整

文章目录 红黑树红黑树插入时的调整&#xff1f;1. 插入结点是根结点2. 插入结点的叔叔是红色3. 插入结点的叔叔是黑色LL 型RR型LR型RL型 红黑树 前提&#xff1a;二叉搜索树&#xff08;左 < 根 < 右&#xff09;—— 左根右根和**叶子&#xff08;NULL&#xff09;**都…...

mysql学习教程,从入门到精通,SQL导入数据(44)

1.SQL 导出数据 以下是一个关于如何使用 SQL 导出数据的示例。这个示例将涵盖从一个关系数据库管理系统&#xff08;如 MySQL&#xff09;中导出数据到 CSV 文件的基本步骤。 1.1、前提条件 你已经安装并配置好了 MySQL 数据库。你有访问数据库的权限。你知道要导出的表名。…...

【SpringAI】(二)让你的Java程序接入大模型——适合Java宝宝的大模型应用开发

开始之前&#xff0c;如果你对大模型完全没了解过&#xff0c;建议阅读之前的大模型入门文章&#xff1a; 【SpringAI】&#xff08;一&#xff09;从实际场景入门大模型——适合Java宝宝的大模型应用开发 那么今天就开始写一个基于Spring AI程序的HelloWord!将大模型接入到咱…...

eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)

说明&#xff1a; 想象一下&#xff0c;你正在用eNSP搭建一个虚拟的网络世界&#xff0c;里面有虚拟的路由器、交换机、电脑&#xff08;PC&#xff09;等等。这些设备都在你的电脑里面“运行”&#xff0c;它们之间可以互相通信&#xff0c;就像一个封闭的小王国。 但是&#…...

7.4.分块查找

一.分块查找的算法思想&#xff1a; 1.实例&#xff1a; 以上述图片的顺序表为例&#xff0c; 该顺序表的数据元素从整体来看是乱序的&#xff0c;但如果把这些数据元素分成一块一块的小区间&#xff0c; 第一个区间[0,1]索引上的数据元素都是小于等于10的&#xff0c; 第二…...

【WiFi帧结构】

文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成&#xff1a;MAC头部frame bodyFCS&#xff0c;其中MAC是固定格式的&#xff0c;frame body是可变长度。 MAC头部有frame control&#xff0c;duration&#xff0c;address1&#xff0c;address2&#xff0c;addre…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例

使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件&#xff0c;常用于在两个集合之间进行数据转移&#xff0c;如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model&#xff1a;绑定右侧列表的值&…...

FastAPI 教程:从入门到实践

FastAPI 是一个现代、快速&#xff08;高性能&#xff09;的 Web 框架&#xff0c;用于构建 API&#xff0c;支持 Python 3.6。它基于标准 Python 类型提示&#xff0c;易于学习且功能强大。以下是一个完整的 FastAPI 入门教程&#xff0c;涵盖从环境搭建到创建并运行一个简单的…...

ESP32读取DHT11温湿度数据

芯片&#xff1a;ESP32 环境&#xff1a;Arduino 一、安装DHT11传感器库 红框的库&#xff0c;别安装错了 二、代码 注意&#xff0c;DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)

可以使用Sqliteviz这个网站免费编写sql语句&#xff0c;它能够让用户直接在浏览器内练习SQL的语法&#xff0c;不需要安装任何软件。 链接如下&#xff1a; sqliteviz 注意&#xff1a; 在转写SQL语法时&#xff0c;关键字之间有一个特定的顺序&#xff0c;这个顺序会影响到…...

如何将联系人从 iPhone 转移到 Android

从 iPhone 换到 Android 手机时&#xff0c;你可能需要保留重要的数据&#xff0c;例如通讯录。好在&#xff0c;将通讯录从 iPhone 转移到 Android 手机非常简单&#xff0c;你可以从本文中学习 6 种可靠的方法&#xff0c;确保随时保持连接&#xff0c;不错过任何信息。 第 1…...

【JavaWeb】Docker项目部署

引言 之前学习了Linux操作系统的常见命令&#xff0c;在Linux上安装软件&#xff0c;以及如何在Linux上部署一个单体项目&#xff0c;大多数同学都会有相同的感受&#xff0c;那就是麻烦。 核心体现在三点&#xff1a; 命令太多了&#xff0c;记不住 软件安装包名字复杂&…...

大学生职业发展与就业创业指导教学评价

这里是引用 作为软工2203/2204班的学生&#xff0c;我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要&#xff0c;而您认真负责的教学态度&#xff0c;让课程的每一部分都充满了实用价值。 尤其让我…...