C# 中使用线程、Task和 ThreadPool 的并发性
C# 中的并发性涉及使用线程和任务等功能在程序中同时执行任务。这就像让多个工人同时完成不同的工作。这在现代应用程序中至关重要,因为它使它们更快、响应更迅速。并发性可确保我们的应用程序平稳运行,快速响应用户操作,并明智地使用主机的功能,使它们可靠并准备好执行任何任务。
了解线程
线程的基础知识
线程在软件中扮演独立工作角色的角色,每个线程同时处理特定操作。它们的功能类似于并行的执行流,允许任务同时进行。在 C# 中,线程的创建和管理涉及定义这些并行流、指定它们应何时启动以及监督其生命周期,包括创建、执行和完成等关键状态。
线程同步
线程同步是顺利管理这些并行操作的关键要素。它确保线程有效地协调和通信,防止在多个线程访问共享资源时可能出现的冲突。
线程同步中可能会出现死锁等挑战,其中线程卡住等待彼此继续。在这些情况下,需要实施策略来防止死锁,确保线程之间的高效协作和应用程序的最佳性能。
探索任务
任务是可以独立于其他任务执行的工作单元。任务是轻量级的,可以由操作系统安排在不同的线程上并发运行。这使它们成为异步编程的理想选择,在异步编程中,可以同时执行多个任务而不会阻塞主线程。
与传统线程相比,任务具有以下几个优点:
轻量级:与线程相比,任务的资源密集程度更低,从而减少了开销并提高了性能。
托管执行:任务由运行时环境管理,运行时环境处理资源分配、调度和同步。
异常处理:任务提供了一种在异步代码中进行异常处理的结构化方法。
任务与线程:主要区别
虽然任务和线程都代表工作单元,但它们在几个关键方面有所不同:
线程模型:线程由操作系统管理,而任务由运行时环境管理。
资源管理:线程需要显式的资源管理,而任务则由运行时管理。
异常处理:基于线程的异常可能难以处理,而任务则提供了一种结构化的异常处理方法。
using System;
using System.Threading.Tasks; class Program
{
static async Task Main()
{
// Creating a task
Task task1= Task.Run(() => Console.WriteLine("Doing some work in a task.")); // Waiting for the task to complete
await task1; Console.WriteLine("Task completed!");
}
}
使用任务进行异步编程
和关键字是使用任务进行异步编程的基础。关键字将方法标记为异步,指示它可能包含异步操作。关键字用于暂停异步方法的执行,直到异步操作完成。
异步方法支持非阻塞 I/O 操作,允许主线程在异步操作进行时保持响应。这提高了应用程序的整体响应能力。
异步代码中的异常处理是使用 / 块处理的。在异步方法中引发的异常将传播到调用方,在那里可以捕获并适当地处理它们。
using System;
using System.Threading.Tasks; class Program
{
static async Task Main()
{
Task myTask1 = Task.Run(async () =>
{
Console.WriteLine("Chef 1 is preparing Dish 1");
await Task.Delay(10000); // Chef 1 takes 10 seconds to prepare Dish 1
Console.WriteLine("Dish 1 is ready!");
}); Task myTask2 = Task.Run(async () =>
{
Console.WriteLine("Chef 2 is preparing Dish 2");
await Task.Delay(1000); // Chef 2 takes 1 second to prepare Dish 2
Console.WriteLine("Dish 2 is ready!");
}); Task myTask3 = Task.Run(async () =>
{
Console.WriteLine("Chef 3 is preparing Dish 3");
await Task.Delay(1000); // Chef 3 takes 1 second to prepare Dish 3
Console.WriteLine("Dish 3 is ready!");
}); await Task.WhenAll(myTask1, myTask2, myTask3); Console.WriteLine("Manager: All dishes are ready! Task completed!");
}
}
任务并行库 (TPL)
任务并行库 (TPL) 是一组类,用于简化 .NET 中的并行编程。它提供了几个用于管理任务和协调其执行的功能。
Parallel.ForEach 和 Parallel.For
Parallel.ForEach和 是用于为集合中的每个元素并行执行委托的方法。 保留迭代顺序,但不保证顺序。
using System;
using System.Threading.Tasks; class Program
{
static void Main()
{
Chef[] chefs = { new Chef("Alice"), new Chef("Bob"), new Chef("Charlie"), new Chef("David") }; // Parallel.ForEach example
Parallel.ForEach(chefs, chef =>
{
chef.Cook();
}); // Parallel.For example
Parallel.For(0, chefs.Length, i =>
{
chefs[i].Cook();
});
}
} class Chef
{
public string Name { get; } public Chef(string name)
{
Name = name;
} public void Cook()
{
Console.WriteLine($"{Name} is cooking...");
Task.Delay(1000).Wait(); // Simulating cooking time
Console.WriteLine($"{Name} finished cooking.");
}
}
在提供的 chef 示例中,使用 和 并行处理厨师数组。对于每个厨师,都会调用该方法,模拟烹饪活动。TPL 负责管理并行执行,允许厨师同时工作。需要注意的是,不能保证执行顺序与数组中元素的顺序相同。
并行 LINQ (PLINQ)
并行 LINQ (PLINQ) 是 LINQ 的扩展,支持并行执行 LINQ 查询。PLINQ 利用 TPL 在多个线程之间分配查询工作负载。
using System;
using System.Linq; class Program
{
static void Main()
{
Chef[] chefs = { new Chef("Alice"), new Chef("Bob"), new Chef("Charlie"), new Chef("David") }; // PLINQ example
var results = chefs.AsParallel().Select(chef =>
{
chef.Cook();
return chef.Name;
}).ToList(); Console.WriteLine("Cooking completed for chefs: " + string.Join(", ", results));
}
} class Chef
{
public string Name { get; } public Chef(string name)
{
Name = name;
} public void Cook()
{
Console.WriteLine($"{Name} is cooking...");
Task.Delay(1000).Wait(); // Simulating cooking time
Console.WriteLine($"{Name} finished cooking.");
}
}
在 chef 示例中,PLINQ 应用于 chef 数组。该操作是并行执行的,其中调用每个厨师的方法。然后将结果收集到一个列表中。PLINQ 提供了一种并行化 LINQ 查询的便捷方法,而无需显式线程管理。
数据并行性
数据并行性是一种在多个线程之间分配数据密集型计算以提高性能的技术。TPL 提供了用于对数据进行分区和在每个分区上执行任务的机制。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // Data parallelism example
Parallel.ForEach(data, item =>
{
Console.WriteLine($"Processing item: {item}, Thread: {Task.CurrentId}");
// Simulating some data-intensive computation
Task.Delay(500).Wait();
});
}
}
在此示例中,整数数组表示数据,并用于并发处理每个项目。该语句指示项的并行处理,并显示关联的线程 ID。 数据并行性对于可以在数据集的不同部分上独立执行任务的方案特别有用,从而可以有效利用可用资源。
ThreadPool 管理
ThreadPool 基础知识
ThreadPool 与单个线程:
使用 ThreadPool 对于具有许多短期任务或异步操作的方案非常有用,因为它避免了不断创建和销毁线程的开销。
单个线程由开发人员显式创建和管理。虽然它们提供了更多的控制,但管理大量线程可能会导致资源耗尽。
默认设置和配置:
ThreadPool 具有由运行时自动管理的默认设置。
您可以使用 and 等方法配置 ThreadPool,以设置最大和最小线程数,从而影响 ThreadPool 管理其资源的方式。
// Example: Configuring the ThreadPool
ThreadPool.SetMinThreads(5, 5);
ThreadPool.SetMaxThreads(20, 20);
ThreadPool 匮乏
当 ThreadPool 中的所有线程都被占用,并且新任务无法获取线程进行执行时,就会发生 ThreadPool 饥饿。ThreadPool 匮乏的原因可能包括长时间运行的任务、同步阻塞操作或任务数量与可用线程之间的不平衡。
检测和诊断 ThreadPool 饥饿:
监视 ThreadPool 的可用线程、待处理任务和其他指标。
如果报告零个可用线程,则表示可能存在 ThreadPool 匮乏。
分析工具和性能监控可以帮助诊断和识别导致饥饿的模式。
您可以使用以下代码创建线程池匮乏场景:
using System;
using System.Threading; class Program
{
private static int totalOrders = 100;
private static int completedOrders = 0;
private static object lockObject = new object(); static void Main()
{
// Set the maximum number of chefs (threads) in the kitchen (thread pool) to 10
ThreadPool.SetMaxThreads(10, 10); Console.WriteLine("Press Enter to start the simulation...");
Console.ReadLine(); // Simulate a restaurant scenario with chefs (thread pool) handling orders (tasks)
for (int i = 0; i < totalOrders; i++)
{
ThreadPool.QueueUserWorkItem(ProcessOrder, i);
} // Monitor the completion of orders (tasks)
while (true)
{
lock (lockObject)
{
if (completedOrders == totalOrders)
{
Console.WriteLine("All orders completed. Press Enter to exit.");
break;
} // Check for kitchen (thread pool) starvation
int pendingOrders = totalOrders - completedOrders; // Get kitchen (thread pool) information
int maxChefs, minChefs, availableChefs;
ThreadPool.GetMaxThreads(out maxChefs, out _);
ThreadPool.GetMinThreads(out minChefs, out _);
ThreadPool.GetAvailableThreads(out availableChefs, out _); Console.WriteLine($"Kitchen (Thread pool) information - MaxChefs: {maxChefs}, MinChefs: {minChefs}, AvailableChefs: {availableChefs}");
Console.WriteLine($"Pending orders: {pendingOrders}"); // Check for kitchen (thread pool) starvation
if (availableChefs == 0)
{
Console.WriteLine("Kitchen (Thread pool) starvation detected!");
}
} Thread.Sleep(100); // Wait for a short duration before checking again
} Console.ReadLine();
} static void ProcessOrder(object orderContext)
{
Console.WriteLine($"Chef {Thread.CurrentThread.ManagedThreadId} is cooking order {orderContext}"); // Simulate cooking time
Thread.Sleep(1000); Console.WriteLine($"Chef {Thread.CurrentThread.ManagedThreadId} completed cooking order {orderContext}"); lock (lockObject)
{
completedOrders++;
}
}
}
使用 async/await 解决 ThreadPool 饥饿问题:
利用异步编程可以帮助缓解 ThreadPool 。
通过使用 ,线程在异步操作期间不会被阻塞,从而允许它们被释放回 ThreadPool。async/await
这对于 I/O 绑定操作特别有效,在这些操作中,线程不会主动计算,而是等待外部资源。
您可以通过将 COTO 转换为以下格式来解决上述线程池饥饿问题:
using System;
using System.Threading.Tasks; class Program
{
private static int totalOrders = 100;
private static int completedOrders = 0;
private static object lockObject = new object(); static async Task Main()
{
// Set the maximum number of chefs (threads) in the kitchen (thread pool) to 10
ThreadPool.SetMaxThreads(10, 10); Console.WriteLine("Press Enter to start the simulation...");
Console.ReadLine(); // Simulate a restaurant scenario with chefs (thread pool) handling orders (tasks) asynchronously
var orderTasks = new Task[totalOrders];
for (int i = 0; i < totalOrders; i++)
{
orderTasks[i] = ProcessOrderAsync(i);
} // Wait for all orders to be completed
await Task.WhenAll(orderTasks); Console.WriteLine("All orders completed. Press Enter to exit.");
Console.ReadLine();
} static async Task ProcessOrderAsync(int orderNumber)
{
Console.WriteLine($"Chef {Thread.CurrentThread.ManagedThreadId} is cooking order {orderNumber}"); // Simulate asynchronous cooking time
await Task.Delay(1000); Console.WriteLine($"Chef {Thread.CurrentThread.ManagedThreadId} completed cooking order {orderNumber}"); lock (lockObject)
{
completedOrders++;
} MonitorThreadPool();
} static void MonitorThreadPool()
{
lock (lockObject)
{
// Check for kitchen (thread pool) starvation
int pendingOrders = totalOrders - completedOrders; // Get kitchen (thread pool) information
int maxChefs, minChefs, availableChefs;
ThreadPool.GetMaxThreads(out maxChefs, out _);
ThreadPool.GetMinThreads(out minChefs, out _);
ThreadPool.GetAvailableThreads(out availableChefs, out _); Console.WriteLine($"Kitchen (Thread pool) information - MaxChefs: {maxChefs}, MinChefs: {minChefs}, AvailableChefs: {availableChefs}");
Console.WriteLine($"Pending orders: {pendingOrders}"); // Check for kitchen (thread pool) starvation
if (availableChefs == 0)
{
Console.WriteLine("Kitchen (Thread pool) starvation detected!");
}
}
}
}
ThreadPool 匮乏,即使在使用 时,也可能由于各种原因而发生,理解和解决这些问题对于维护应用程序性能至关重要。使用 时,确保异步方法确实是非阻塞的,这一点很重要。如果方法中存在同步阻塞操作,它们可能会占用线程并导致 ThreadPool 匮乏。您应仔细检查异步方法,以消除任何可能妨碍线程有效利用的阻塞操作。
// Incorrect: Blocking operation inside async method
static async Task MyAsyncMethod()
{
// This synchronous operation can lead to ThreadPool starvation
SomeBlockingOperation();
await SomeAsyncOperation();
} // Correct: Non-blocking operations inside async method
static async Task MyAsyncMethod()
{
// Asynchronous operation
await SomeAsyncOperation();
}
此外,方法内部长时间运行的操作或受 CPU 限制的操作仍可能带来挑战。如果此类操作不是真正的异步操作,则可能导致 ThreadPool 匮乏。为了解决这个问题,您应该将 CPU 密集或长时间运行的操作移出上下文,仅在必要时使用各种机制。
// Incorrect: Long-running CPU-bound operation inside async method
static async Task MyAsyncMethod()
{
// This CPU-bound operation can lead to ThreadPool starvation
await Task.Run(() => SomeLongRunningOperation());
} // Correct: Moving long-running operation outside async method
static async Task MyAsyncMethod()
{
// Asynchronous operation without blocking threads
await SomeAsyncOperation();
} // Long-running operation moved outside async method
static void SomeLongRunningOperation()
{
// CPU-bound operation
}
ThreadPool 配置是另一个关键因素。显式设置使用 或使用 的线程不足可能会限制可用线程数,从而导致 ThreadPool 匮乏。根据应用程序的需求正确配置 ThreadPool 对于获得最佳性能至关重要。
// Incorrect: Configuring the ThreadPool with insufficient threads
ThreadPool.SetMinThreads(2, 2);
ThreadPool.SetMaxThreads(5, 5); // Correct: Adjusting ThreadPool settings
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(50, 50);
外部因素(例如外部服务或依赖项的问题)也可能导致 ThreadPool 匮乏。网络或 I/O 瓶颈可能会导致线程等待资源。识别和解决这些外部因素对于整体系统的稳定性至关重要。
高级线程和任务注意事项
线程安全
线程安全在并发编程中的重要性
在并发编程中,当多个线程可以访问和修改共享资源时,确保线程安全对于避免数据损坏至关重要。该语句通常用于同步对共享数据的访问。在以下示例中,类使用 a 安全地递增共享计数器:
using System;
using System.Threading; class SharedResource
{
private int counter = 0;
private object lockObject = new object(); public void IncrementCounter()
{
lock (lockObject)
{
counter++;
Console.WriteLine($"Counter: {counter}, Thread ID: {Thread.CurrentThread.ManagedThreadId}");
}
}
} class Program
{
static void Main()
{
SharedResource sharedResource = new SharedResource(); // Simulating multiple threads incrementing the counter
for (int i = 0; i < 5; i++)
{
new Thread(() => sharedResource.IncrementCounter()).Start();
} Console.ReadLine();
}
}
不可变类型及其作用
不可变类型,其状态在创建后无法修改,本质上提供线程安全性。在此示例中,将创建一个具有不可变属性的类:
using System; public class ImmutableData
{
public int Value { get; } public ImmutableData(int value)
{
Value = value;
}
} class Program
{
static void Main()
{
ImmutableData immutableData = new ImmutableData(42); // The state of immutableData cannot be modified after creation
Console.WriteLine($"Immutable Data Value: {immutableData.Value}");
}
}
ThreadLocal<T> 用于每个线程的数据
ThreadLocal<T>是一个有用的类,用于在不同步的情况下管理每线程数据。在以下示例中,a 用于存储每个线程的唯一值:
using System;
using System.Threading; class Program
{
static ThreadLocal<int> threadLocalValue = new ThreadLocal<int>(() => 0); static void SetThreadLocalValue(int newValue)
{
threadLocalValue.Value = newValue;
} static int GetThreadLocalValue()
{
return threadLocalValue.Value;
} static void Main()
{
// Simulating multiple threads with unique per-thread values
for (int i = 1; i <= 3; i++)
{
new Thread(() =>
{
SetThreadLocalValue(i);
Console.WriteLine($"Thread ID: {Thread.CurrentThread.ManagedThreadId}, ThreadLocal Value: {GetThreadLocalValue()}");
}).Start();
} Console.ReadLine();
}
}
背景线程与前景线程
区分背景线程和前景线程
.NET 中的线程分为后台线程和前台线程。前台线程使应用程序保持活动状态,直到它们完成,而后台线程不会阻止应用程序终止。默认情况下,ThreadPool 创建的线程是后台线程。在此示例中,我们创建前台线程和后台线程:
using System;
using System.Threading; class Program
{
static void Main()
{
Thread foregroundThread = new Thread(() =>
{
Console.WriteLine("Foreground Thread");
}); Thread backgroundThread = new Thread(() =>
{
Console.WriteLine("Background Thread");
}); foregroundThread.Start(); // Foreground thread
backgroundThread.IsBackground = true; // Set as background thread
backgroundThread.Start(); Console.WriteLine("Main Thread Exiting");
}
}
对应用程序终止的影响
前台线程使应用程序保持活动状态,直到它们完成。在此示例中,应用程序将等待前台线程完成,然后退出,但不会等待后台线程:
using System;
using System.Threading; class Program
{
static void Main()
{
Thread foregroundThread = new Thread(() =>
{
Console.WriteLine("Foreground Thread");
Thread.Sleep(3000); // Simulating work
}); Thread backgroundThread = new Thread(() =>
{
Console.WriteLine("Background Thread");
Thread.Sleep(2000); // Simulating work
}); foregroundThread.Start(); // Foreground thread
backgroundThread.IsBackground = true; // Set as background thread
backgroundThread.Start(); Console.WriteLine("Main Thread Exiting");
}
}
线程和任务优先级
调整线程和任务优先级
可以使用 调整线程和任务优先级。在此示例中,我们创建一个高优先级和低优先级线程:
using System;
using System.Threading; class Program
{
static void Main()
{
Thread highPriorityThread = new Thread(() =>
{
Thread.CurrentThread.Priority = ThreadPriority.Highest;
Console.WriteLine("High-Priority Thread");
}); Thread lowPriorityThread = new Thread(() =>
{
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
Console.WriteLine("Low-Priority Thread");
}); highPriorityThread.Start();
lowPriorityThread.Start(); Console.WriteLine("Main Thread Exiting");
}
}
对整体系统性能的影响
虽然调整线程或任务优先级会影响它们的调度顺序,但考虑对整体系统性能的影响至关重要。大量调整可能会影响调度程序的公平性,并可能导致优先级倒置。建议明智地使用优先级调整。
学习 C# 并发(包括线程、任务和 ThreadPool)是构建响应式应用程序的关键。了解并行执行、最佳实践和高级注意事项可确保开发更顺畅,并增强整体 C# 并发体验。
C# 中使用线程、Task和 ThreadPool 的并发性的更多相关文章
- 11.python3标准库--使用进程、线程和协程提供并发性
''' python提供了一些复杂的工具用于管理使用进程和线程的并发操作. 通过应用这些计数,使用这些模块并发地运行作业的各个部分,即便是一些相当简单的程序也可以更快的运行 subprocess提供了 ...
- C#中假设正确使用线程Task类和Thread类
C#中使用线程Task类和Thread类小结 刚接触C#3个月左右.原先一直使用C++开发.由于公司的须要,所地採用C#开发.主要是控制设备的实时性操作,此为背景. 对于C#中的Task和Thread ...
- 违反并发性: UpdateCommand影响了预期 1 条记录中的 0 条 解决办法
本文转载:http://www.cnblogs.com/litianfei/archive/2007/08/16/858866.html UpdateCommand和DeleteCommand出现DB ...
- 了解 .NET 的默认 TaskScheduler 和线程池(ThreadPool)设置,避免让 Task.Run 的性能急剧降低
.NET Framework 4.5 开始引入 Task.Run,它可以很方便的帮助我们使用 async / await 语法,同时还使用线程池来帮助我们管理线程.以至于我们编写异步代码可以像编写同步 ...
- Java中的线程池
package com.cn.gbx; import java.util.Date; import java.util.Random; import java.util.Timer; import j ...
- C#中的线程(三) 使用多线程
第三部分:使用多线程 1. 单元模式和Windows Forms 单元模式线程是一个自动线程安全机制, 非常贴近于COM——Microsoft的遗留下的组件对象模型.尽管.NET最大地放弃摆脱了遗留 ...
- C#中的线程(下)-多线程
1. 单元模式和Windows Forms 单元模式线程是一个自动线程安全机制, 非常贴近于COM——Microsoft的遗留下的组件对象模型.尽管.NET最大地放弃摆脱了遗留下的模型,但很多时候它 ...
- C#中 Thread,Task,Async/Await,IAsyncResult 的那些事儿!
说起异步,Thread,Task,async/await,IAsyncResult 这些东西肯定是绕不开的,今天就来依次聊聊他们 1.线程(Thread) 多线程的意义在于一个应用程序中,有多个执行部 ...
- Java线程池(ThreadPool)详解
线程五个状态(生命周期): 线程运行时间 假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间. 如果:T1 + T3 远大于 T2,则可以 ...
- 探究ElasticSearch中的线程池实现
探究ElasticSearch中的线程池实现 ElasticSearch里面各种操作都是基于线程池+回调实现的,所以这篇文章记录一下java.util.concurrent涉及线程池实现和Elasti ...
随机推荐
- 航天信息诺税通SAAS接口封装DLL
项目中需要对接航天信息的诺税通接口开具电子发票,为此将功能封装到了DLL中,其他项目也可以方便的引用. Delphi调用示例: 有需要可以和我联系:yzqnet(微信)
- 从 Excel 到你的表格应用:数据验证功能的嵌入实践指南
前言: 随着信息化的不断发展,传统表格软件已无法满足用户对便携性.数据自动化管理等日益复杂的要求,将电子表格与其他系统结合.开发自己的表格应用已成为愈发火热的趋势. 然而,当企业需要将 Excel 的 ...
- access 类对象使用
类模块代码如下: Option Explicit '定义按钮对象和onclick 触发内容 Private WithEvents m_Closebtn As Access.CommandButton ...
- .NET 原生驾驭 AI 新基建实战系列(五):Milvus ── 大规模 AI 应用的向量数据库首选
1. 引言 Milvus 是一个强大的工具,帮助开发者处理大规模向量数据,尤其是在人工智能和机器学习领域.它可以高效地存储和检索高维向量数据,适合需要快速相似性搜索的场景.在 .NET 环境中,开发者 ...
- Canvas、客户端、表单
Canvas var canvas = document.querySelector('.myCanvas'); var width = canvas.width = window.innerWidt ...
- AD 侦查-MSRPC
本文通过 Google 翻译 AD Recon – MSRPC (135/539) 这篇文章所产生,本人仅是对机器翻译中部分表达别扭的字词进行了校正及个别注释补充. 导航 0 前言 1 MSRPC(远 ...
- Vue 3中的ref和template refs详解(含Vue2迁移到Vue3方法)
Vue 3中的ref和template refs详解 在Vue 3中,ref和模板引用(template refs)是两个相关但不同的概念,它们在组合式API(Composition API)中扮演着 ...
- skip
哇酷哇酷,和你的春天一样稍纵即逝的夏天 藏什么藏呢 自卑吗 你以为是缺点的 恰恰让我喜欢 但要短确实很短 说难是很难 而且烂 恰到好处吧 好男人也没的身手! 为了足以被好男人拯救 我在练习 结果是腿废 ...
- MacOS M1 安装python3.5
因为没法通过brew直接安装python 3.5,因为brew库里已经没有这个版本的python了,因此只能曲线救国,大体流程: 安装brew 通过brew 安装 pyenv 然后通过pyenv 安装 ...
- 解决更新WIFI驱动后出现网络适配器黄色三角警告
更新WIFI驱动后出现网络适配器黄色三角警告问题的解决方案 在更新 Intel 无线网卡驱动后,遇到了网络适配器异常的问题,尤其是在曾经安装/卸载过 VMware 的电脑上.本篇文章将详细介绍这个问题 ...