C#软件开发工程师高频面试题,持续更新中。。。

1.抽象类和接口有什么区别?

**抽象类是特殊的类,不能被直接实例化。它们可以包含抽象方法和普通方法,以及属性和事件等成员。抽象类的主要目的是作为其他类的基类只能被继承提供部分实现的功能和一些共享的属性或方法

**接口是一种引用类型,也不能被实例化。它只包含方法的声明,不包含任何实现接口的成员(包括方法、属性、事件等)都是公开的。接口的主要目的是定义一组行为,这些行为可以被任何实现该接口的类所共享。

2.说一下构造函数?

**父类构造函数先执行:当创建一个子类的对象时,首先会调用父类的构造函数。如果父类没有定义任何构造函数(即没有显式声明任何构造函数),C#编译器会自动提供一个默认

的无参构造函数。如果父类有一个或多个带参数的构造函数,并且没有默认构造函数,那么在创建子类对象时,必须在子类的构造函数中通过: base(...)语法显式地调用父类的构造函数。

**子类构造函数后执行:在父类构造函数执行完毕后,接着会执行子类的构造函数。子类构造函数中可以使用base关键字来访问父类的成员,包括父类的构造函数

**父类构造函数不会被继承,父类的静态属性和私有属性也不会被继承

3.讲一下面向对象?三大特性?

**面向对象是一种编码的思想,就OOP思想,编写代码的过程中,对于类的使用可以是编写代码更加边界,让代码结构更清晰。

**面向对象有三大特性:封装,继承,多态

1.封装就是隐藏对象的内部实现细节,只提供公开的接口进行交互

2.继承就是一个类可以继承另一个类的属性,并且可以添加自己的属性,当然,私有的和静态的属性不能被继承

@3.多态就是同一个接口,不同的实现方式

拓展问题,如何实现多态呢?

**通过接口实现:基类定义的接口,然后在派生类里面有其他实现

**通过虚方法实现:基类用virtual关键字声明方法,派生类使用override关键字重写该方法

**用抽象类和抽象方法实现:使用abstract关键字定义抽象类和抽象方法,必须在派生类中提供具体的实现

4.讲一下抽象方法(abstract)和虚方法(virtual)?

**虚方法是C#中用于实现运行时多态的机制。基类中标记为virtual的方法可以在派生类中通过override关键字重写。
**抽象方法是定义在抽象类中的方法,不包含实现代码。抽象方法要求派生类必须重写这个方法。

**在这两种情况下,方法都可以在派生类中重写。不同点在于:

如果基类方法标记为虚拟,则不需要在派生类中显式地使用override关键字。

如果基类方法是抽象的,则派生类必须使用override关键字来重写该方法,否则基类将保持抽象状态。

**在实际应用中,选择虚方法还是抽象方法取决于需求:

如果你想要有一个默认的实现,并且允许子类以不同的方式实现它,那么使用虚方法。

如果你想要强制子类提供一个实现,那么使用抽象方法。

抽象方法不能使用staticvirtual修饰符,‌且只能定义在抽象类中

5.说一下拆箱和装箱?

**拆箱:引用类型向值类型转换的过程,拆箱过程会获取已装箱的值类型的值

在拆箱过程中,会检查对象引用是否确实指向一个装箱的值类型实例。

如果是,该值就从堆上复制回栈上的值类型变量中。如果引用的是null或者不是装箱的值类型,那么会抛出一个异常。

**装箱:值类型转换为引用类型,装箱过程会创建一个新的对象,将值类型的数据复制到该对象中

当值类型被装箱时,它会在托管堆上分配一个新的对象实例,并将该值复制到新分配的对象中。然后,这个对象的引用会被返回。装箱操作通常发生在以下情况:

当值类型作为参数传递给只接受引用类型参数的方法时。

当值类型被赋值给一个引用类型的变量时。

装箱和拆箱涉及到内存分配和值类型的转换,对性能有影响,所以在开发过程中应当注意避免不必要的装箱和拆箱的操作。

6.说一下事务的特性?

**原子性(Atomicity):原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部完成,要么全部不完成。

如果任何一个操作失败,整个事务就会回滚到事务开始前的状态,就像这个事务从来没有执行过一样。

**一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态转变到另一个一致性状态。一致性与业务规则有关,

比如转账操作,无论事务是否成功,参与转账的两个账户的总金额应该保持不变。

**隔离性(Isolation):多个事务并发执行时,一个事务的操作不应影响其他事务。隔离性通过锁和其他机制来实现,

以防止并发操作导致数据不一致。

**持久性(Durability):一旦事务完成(无论成功还是失败),其对数据库所做的更改就是永久的。即使系统崩溃或发生故障,

重新启动后数据库还能恢复到事务成功结束时的状态。

7.说一下锁?

**lock关键字:‌这是C#中最简单和最常用的锁机制。‌lock关键字用于获取对象的互斥锁‌确保同一时间只有一个线程能够执行特定的代码块。‌它用于实现线程同步和互斥

**Monitor类:‌提供了一种更灵活的同步机制,‌通过Monitor.EnterMonitor.Exit方法来获取和释放对象的锁。‌除了基本的互斥锁外,‌Monitor类还提供了等待和通知的功能,‌可以实现更复杂的线程同步方案

**Mutex类:‌这是一种操作系统级别的内核对象,‌用于进程间的同步。‌在C#中,‌Mutex类封装了操作系统提供的互斥体,‌可以用于实现跨进程的线程同步

**Semaphore类:‌也是一种操作系统级别的同步原语‌用于控制同时访问共享资源的线程数量。‌Semaphore类允许指定一个计数器,‌表示可访问共享资源的线程数量,‌适用于一些限流的场景‌

**AutoResetEvent和ManualResetEvent类:‌这些是基于事件的同步原语,‌用于线程间的信号通知和同步。‌它们允许一个或多个线程等待另一个线程发送信号后继续执行

//lock 锁代码示例,lock关键字自动管理锁得获取和释放
object lockObject = new object();
lock (lockObject)
{
    // 访问共享资源的代码
};
//Monitor锁代码示例
object _locker = new object();
//获取锁加上超时时间,避免死锁方法之一
TimeSpan timeout = TimeSpan.FromSeconds(5);
var isLock = Monitor.TryEnter(_locker, timeout);
if (isLock)
{
    try
    {
        //上锁代码
    }
    finally
    {
        Monitor.Exit(_locker);
    }
}
else
{
    //获取锁超时
}

//Mutex锁代码示例
//创建Mutex实例
Mutex m = new Mutex();
//加锁
m.WaitOne();
//访问共享资源
//解锁
m.ReleaseMutex();


//Semaphore 使用示例
Semaphore semaphore = new Semaphore(3, 3);//允许最大访问数
//访问共享资源前,等待Semaphore
semaphore.WaitOne();
//执行完之后,释放Semaphore
semaphore.Release();

 **Semaphore和Mutex有什么区别?

Semaphore和Mutex在并发编程中都是用于同步的机制,‌但它们在用途和操作上存在明显区别:‌

用途‌:‌Semaphore用于控制对多个同类资源的访问,‌允许多个线程同时访问不同资源;‌而Mutex主要用于实现线程间的互斥,‌确保同一时刻只有一个线程能访问共享资源或临界区,‌保护单个资源‌。‌

操作‌:‌Semaphore可以由一个线程获取并由另一个线程释放,‌灵活性更高;‌而Mutex通常由一个线程获取并最终释放,‌其他线程必须等待直到锁被释放‌。‌

计数器‌:‌Semaphore的计数器可以是任意非负整数,‌表示可用资源的数量;‌Mutex的计数器只有0和1两个值,‌类似于特殊的二元Semaphore‌。‌

总的来说,‌Semaphore适用于需要控制多个资源访问的场景,‌而Mutex适用于保护单个资源的场景‌

8.说一下数据库锁?

数据库锁主要包括以下几种类型,以及它们的作用如下:

**共享锁(Shared Lock):也称为读锁,允许多个事务同时读取同一资源。

一个事务持有共享锁时,其他事务可以继续获取该资源的共享锁,但不能获取排他锁。

主要用于支持并发的读取操作,确保读取数据时不会被其他事务修改,从而避免重复读的问题。

**排他锁(Exclusive Lock):也称为写锁,确保资源在某一时刻只被一个事务独占访问。

当一个事务持有排他锁时,其他事务既不能获取该资源的共享锁,也不能获取排他锁

主要用于数据的更新和删除操作,确保在修改数据时不会被其他事务读取或修改,从而避免脏数据和脏读的问题。

**意向锁(Intent Lock):是一种表明事务意图获取某种类型锁的锁。在获取行级锁或表级锁之前,事务可以先获取意向锁,以提高并发性能。

意向锁分为意向共享锁(IS)和意向排他锁(IX),分别表示事务想要获取的是共享锁还是排他锁。

**记录锁(Record Lock):锁定的是表中的某一行记录当事务对某条记录加上记录锁时,其他事务不能对这条记录进行更新或删除操作,直到锁被释放。

**表锁:锁定的是整个表当一个事务对表加上表锁时,其他事务必须等待该锁释放后才能对该表进行访问。

表锁的开销较小,但并发性能较低。

9.怎么启一个线程,有哪几种方式?

C# 支持多线程编程,这意味着你可以在单个进程中同时执行多个线程。多线程编程可以充分利用多核处理器,提高应用程序的响应性和性能

**Thread 类:这是最直接的方法,通过 System.Threading.Thread 类来创建线程。你可以通过继承 Thread 类并重写 Run 方法,或者直接实例化 Thread 对象并传递一个 ThreadStart 或 ParameterizedThreadStart 委托给其构造函数。

        // 使用 ThreadStart 委托

        Thread thread = new Thread(new ThreadStart(ThreadFunction));

        thread.Start();

        // 使用 ParameterizedThreadStart 委托

Thread threadWithParam = new Thread(new ParameterizedThreadStart(ThreadFunctionWithParam));

        threadWithParam.Start("Hello from Thread!");

**ThreadPool:线程池是一个由系统管理的线程集合,它允许你排队执行任务,而不需要显式地创建和管理线程。ThreadPool 类提供了在后台线程上异步执行操作的方法,如 QueueUserWorkItem。

ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadPoolFunction));

**Task类:从C# 4.0开始,引入了基于任务的异步模式(TAP),它使用System.Threading.Tasks.Task类来表示异步操作。Task类提供了更高级的抽象,允许更简洁的代码和更好的异常处理。

Task task = Task.Run(() => TaskFunction());

        Task task = Task.Factory.StartNew(() => TaskFunction());

        task.Wait(); // 等待任务完成

     static void TaskFunction()

     {

         // 异步执行的代码

     }

**异步编程模型 (Async/Await):从C# 5.0开始,你可以使用 async 和 await 关键字来编写异步代码,使异步操作看起来更像同步代码,且更容易编写和理解。用于简化异步编程模型。这种方式不会直接创建新线程,而是在现有线程上进行异步操作,使得线程可以在等待I/O操作等耗时任务时不会被阻塞,提高了线程的利用率。

using System.Threading.Tasks;

class Program

{

    static async Task Main()

    {

        await AsyncFunction();

    }

    static async Task AsyncFunction()

    {

        // 异步执行的代码

        await Task.Run(() => { /* 异步操作 */ });

    }

}

10.C#中List集合是线程安全的吗,线程安全的集合有哪些?

在C#中,List<T> 集合类本身不是线程安全的。这意味着在多线程环境中,如果你不对其进行同步访问,就可能会出现数据不一致的问题,如数据竞争、死锁等

为了确保线程安全,你需要采取额外的措施,如使用锁(lock)语句或其他同步机制(如信号量、互斥体等)来确保对List<T>的并发访问是同步的。

线程安全的集合类包括以下几种:

**ConcurrentBag**:这是一个无序的集合,用于存储唯一的元素。它提供了线程安全的添加和移除操作

**ConcurrentDictionary<TKey, TValue>**:这是一个线程安全的键值对集合,允许你进行并发的添加、移除和查找操作。

**ConcurrentQueue**:这是一个线程安全的先进先出(FIFO)队列

**ConcurrentStack**:这是一个线程安全的后进先出(LIFO)栈

**BlockingCollection**:这是一个线程安全的集合,它支持添加和移除操作,并提供阻塞和超时选项

ReaderWriterLockSlim:这是一个同步原语,它可以用来保护对集合的读取和写入操作。虽然它本身不是一个集合,但可以与各种集合结合使用来提供线程安全的访问。

使用这些线程安全的集合类,你可以在多线程环境中更安全地操作集合,而无需担心数据一致性问题。这些集合内部使用各种同步机制(如锁、信号量等)来确保并发访问的安全性。

11.C# 说一下多线程?

**. 线程基础

Thread 类:这是C#中表示线程的基础类。你可以通过继承 Thread 类并重写 Run 方法来创建线程,或者直接实例化 Thread 对象并传递一个 ThreadStart 或 ParameterizedThreadStart 委托给其构造函数来启动线程

线程状态:每个线程都有多种可能的状态,包括 Unstarted、Running、WaitSleepJoin、Stopped、Aborted、Suspended(已过时)和 Background

线程优先级:线程具有优先级,这决定了线程调度器如何在线程之间分配处理器时间。你可以使用 ThreadPriority 枚举设置线程的优先级

**. 线程同步

锁(lock):使用 lock 语句来确保一段代码在同一时间只被一个线程访问。这可以防止多个线程同时访问共享资源时发生的数据竞争和不一致

信号量(Semaphore):信号量用于限制对共享资源的并发访问数量

互斥体(Mutex):互斥体是一种同步原语用于保护共享资源免受多个线程的并发访问

监视器(Monitor):Monitor 类提供了一种机制,用于同步对共享资源的访问。你可以使用 Monitor.Enter 和 Monitor.Exit方法确保资源在任何时候都只被一个线程访问

**. 线程池

ThreadPool线程池是一个由系统管理的线程集合,用于执行异步操作。线程池可以显著减少线程的创建和销毁开销,提高应用程序的性能。你可以使用 ThreadPool 类来排队执行任务,而无需显式地创建和管理线程。

**. 异步编程模型(Async/Await)

异步方法(Async Methods)C# 5.0 引入了 async 和 await 关键字,它们使异步编程更加简单和直观。你可以使用这些关键字来编写异步方法,这些方法可以在不阻塞调用线程的情况下执行耗时操作

Task 类:Task 类表示一个异步操作。你可以使用 Task.Run 方法来在后台线程上执行操作,并使用 await 关键字来等待操作完成

**. 并行编程(Parallel)

Parallel 类:Parallel 类提供了一系列静态方法,用于简化并行执行循环和数据转换的任务

例如,Parallel.For 和 Parallel.ForEach 方法可以用于并行执行循环,而 Parallel.Invoke 方法可以用于并行执行多个委托

注意事项

线程安全:在多线程环境中,需要确保对共享资源的访问是线程安全的。这通常涉及到使用同步机制来避免数据竞争和不一致。

死锁和活锁:不恰当的同步可能导致死锁两个或更多线程无限期地等待对方释放资源)或活锁(两个或更多线程反复尝试获取资源但总是失败)。

性能考虑:虽然多线程可以提高性能,但过多的线程可能导致过多的上下文切换和资源争用,从而降低性能。因此,需要仔细考虑线程的数量和如何最有效地使用它们。

12.值类型和引用类型

类型直接存储其数据,值类型的数据在分配内存时,是在栈(stack)上进行的。值类型的数据在声明的同时必须初始化,因为它们直接包含了数据。常见的值类型有基本数据类型(int, float, double等),枚举(enum),结构(struct)等。

引用类型存储的是数据的引用,引用类型的数据在分配内存时,是在堆(heap)上进行的。

引用类型的数据在声明时可以不必初始化,因为它只是保存了一个引用地址,该地址指向实际的数据。

当没有任何变量引用这个地址时,这部分数据就变成了垃圾,可以被垃圾回收器回收。

常见的引用类型有类(class),接口(interface),数组(array),委托(delegate),字符串(string)等。

13.C#泛型

在定义的时候可以使用占位符不指定具体参数类型。然后具体使用,实例化的时候

再去指定想要实例化的类型,这里的类型可以是类,接口,事件,委托等类型

优点:

代码可重用

性能好,不需要装箱和拆箱

类型安全(协变和逆变)

14.C# 协变和逆变

(out T)使用协变的目的,是为了限制这个泛型类型参数不能作为方法的入参类型使用

使得泛型子类对象引用可以指向泛型基类的对象引用。保证了类型安全

(in T)使用逆变的目的,是为了限制这个泛型类型参数不能作为方法的返回值类型使用

使得泛型基类对象引用也可以安全的指向泛型子类的对象引用。保证了类型安全

15.依赖注入

依赖注入(Dependency Injection, DI)是一种软件设计模式,主要是为了解决代码之间的耦合问题。

在项目启动的时候,startup类里面。ConfigureServices来注册依赖注入的服务,通过调用 services.Add... 方法来注册所需的服务

依赖注入的基本思想是将一个对象所依赖的资源(‌即依赖项)‌注入到该对象中,‌而不是让对象自己去创建这些依赖项。‌在C#中,‌依赖注入通常通过以下方式实现:‌

构造函数注入‌:‌通过类的构造函数将依赖传递给类的实例,‌确保在对象创建时所有的依赖项都已准备好。‌

属性注入‌:‌通过属性将依赖注入到类的实例中,‌这种方式较为灵活,‌但在某些情况下可能导致类的实例在没有正确依赖的情况下被使用。‌

方法注入‌:‌通过方法将依赖注入到类的实例中。‌

16.怎么定位项目中的哪些sql需要优化

我们项目中大部分使用的是SQLSugar的方法去跟数据据交互的,我们用该ORM提供的日志功能,来查看生成的sql和执行时间

或者我们还可以在写完SQL后,把sql放在sqlserver里面去执行下,查看执行计划,里面也可以看到该sql的性能如何。

17.mysql 索引有哪几种

MySQL索引主要有以下几种类型:‌

普通索引‌:‌最基本的索引,‌没有任何限制,‌允许在定义索引的列中插入重复值和空值。‌

唯一索引‌:‌索引列的值必须唯一,‌但允许有空值。‌如果是组合索引,‌则列值的组合必须唯一。‌

主键索引‌:‌一种特殊的唯一索引,‌不允许有空值,‌一个表只能有一个主键索引。‌

全文索引‌:‌用于查找文本中的关键字,‌只能在CHAR、‌VARCHAR或TEXT类型的列上创建。‌

空间索引‌:‌对空间数据类型的字段建立的索引,‌主要用于地理空间数据类型。‌

组合索引‌:‌在多个字段上创建的索引,‌只有在查询条件中使用了创建索引时的第一个字段,‌索引才会被使用

18.抽象类和接口,抽象方法和虚方法

**抽象类:抽象类可以包含抽象方法和非抽象方法

抽象类不能被实例化,只能被继承

子类继承抽象类后,必须实现抽象类中的所有抽象方法,除非它们自己也是抽象类。

**接口:接口只能包含抽象方法

接口不能包含非抽象实现的方法

可以实现多个接口,(前提是抽象类)实现接口的部分或全部方法

**抽象方法:抽象方法必须在抽象类中,且没有具体的实现必须在子类中实现

**虚方法:虚方法可以有实现代码子类可以选择不重写虚方法,从而继承父类的实现

子类可以选择重写虚方法,从而覆盖父类的实现

19.跨类调用方法有什么方式

通过实例化对象调用:‌如果方法是实例方法,‌需要先创建类的实例,‌然后通过这个实例来调用方法。‌

通过静态方式调用:‌如果方法是静态的,‌可以直接通过类名来调用,‌无需创建类的实例。‌

通过接口调用:‌如果类实现了某个接口,‌可以通过接口类型来调用实现的方法。‌

通过继承调用:‌如果子类继承了父类,‌子类可以直接调用父类中非私有的方法。‌

20.项目中比较有挑战的问题

Redis 的消息队列可以通过以下几种方式实现:

List 类型:使用 LPUSH/RPUSH 命令将元素推入队列,使用 LPOP/RPOP 命令从队列中弹出元素。

Pub/Sub 模式:发布/订阅模式,使用 PUBLISH 发布消息,SUBSCRIBE 订阅频道。

Streams 类型:类似于 Kafka 的 message broker,可以使用 XADD 添加消息,XREAD 读取消息。

List 适合简单的队列操作,Pub/Sub 适合消息广播,Streams 提供了更高级的队列特性,比如消息的序列化和分组消费。

List 的缺点是不支持消息订阅,Pub/Sub 的缺点是消息一旦发布,订阅者不能重复消费,Streams 提供了acknowledging机制,但命令较为复杂。

21.订阅发布,发布的消息没接收者,这时候消息怎么处理。

没有订阅者时,Redis默认消息会丢失,如果想要消息不丢失的话可以将消息存到Redis缓存中,然后等有订阅者了,再去取出消息,再发布消息。

22.Redis的消息队列实现的方式以及特点

LPUSH/LPOP:‌这是基于Redis列表(‌List)‌结构实现的简单消息队列。‌LPUSH用于从列表左侧插入元素‌LPOP则从列表左侧弹出元素。‌这种方式适用于简单的生产者-消费者模型,‌但不支持复杂的消息确认机制或持久化保证。‌

发布订阅(‌Pub/Sub)‌:‌支持一个或多个发布者向多个订阅者发送消息。‌消息传递是一对多的关系,‌适用于实时通知和事件处理场景。‌但Pub/Sub不提供消息持久化‌且如果订阅者离线,‌将错过发布的消息。

Stream:‌Redis 5.0引入的一种高级消息队列实现。‌它支持消息的持久化、‌消费者组、‌消息确认等高级特性‌Stream允许一个或多个生产者向队列发送消息,‌同时支持多个消费者组以竞争或共享的方式消费消息。‌这种方式更接近于专业的消息队列系统,‌适用于需要高可靠性和复杂消息处理的场景。‌

23.Redis持久化消息的具体流程主要包括两种方式:‌RDB和AOF。

RDB(‌Redis DataBase)‌:‌通过定时或手动触发,‌将内存中的数据生成快照并保存到磁盘上。‌快照生成时,‌Redis会利用操作系统的COW(‌Copy On Write)‌机制,‌创建一个子进程来遍历内存数据并序列化到磁盘,‌期间主进程可以继续处理客户端请求。‌这种方式适合大规模数据恢复,‌但可能会丢失最后一次快照后的数据。

AOF(‌Append Only File)‌:‌以日志的形式记录每个写操作,‌并将这些操作追加到AOF文件中。‌Redis提供了多种写磁盘的策略,‌如Always(‌每条指令都同步写入磁盘)‌、‌EverySec(‌每秒同步一次)‌和No(‌由操作系统决定何时写入磁盘)‌。‌AOF文件会定期重写以压缩文件大小,‌提高恢复效率。‌这种方式数据丢失少,‌但恢复速度相对较慢。‌

24.String和StringBuilder区别

‌String是一个不可变的字符序列,‌一旦创建,‌就不能再被修改。‌这意味着每次对字符串的修改(‌如拼接、‌替换等)‌都会生成一个新的字符串对象‌这种不可变性使得String在处理常量或只读数据时非常有用,‌因为它提供了较高的安全性和易用性‌然而,‌对于需要频繁修改字符串的场景,‌String的性能会受到影响,‌因为它需要不断地创建新的对象来存储修改后的结果‌

StringBuilder是一个可变的字符序列,‌设计用于在单线程环境中频繁地修改字符串。‌它通过提供一个内部字符数组来实现字符串的拼接和修改,‌而不需要像String那样创建新的对象。‌这使得StringBuilder在性能上优于String,‌尤其是在需要进行大量字符串拼接或修改的操作时。‌然而,‌需要注意的是,‌StringBuilder不是线程安全的,‌如果在多线程环境中使用,‌可能会遇到并发问题

StringBuffer与StringBuilder类似,‌也是一个可变的字符序列,‌但它是为了多线程环境设计的。‌与StringBuilder相比,‌StringBuffer的方法是同步的,‌保证了线程安全。‌这意味着在多线程环境中对StringBuffer的操作是安全的,‌但这也带来了额外的性能开销。‌因此,‌如果不需要线程安全,‌使用StringBuilder通常会更高效

25.面向对象三大特性

封装是一种信息隐藏技术,将对象的状态(数据)和行为(方法)打包在一起,隐藏对象的内部实现细节,只提供公开的接口让其他对象与之交互。

继承是子类继承父类的特性(包括数据和方法),从而可以扩展或修改父类的行为。

多态是指一个对象可以有多种形态,在运行时,可以通过指向子类的父类指针,调用子类重写的方法。

26.说一下线程和进程

定义不同:

进程:是程序运行的一个实例,包括程序计数器、寄存器和变量的当前状态。

线程:是进程中的一个执行单元,也被称为轻量级进程。

系统资源分配不同:

进程:拥有独立的内存空间和系统资源

线程:与同属一个进程的其他线程共享内存和系统资源

调度和切换开销不同:

进程:由于拥有独立的内存空间,切换开销大,但相对更稳定。

线程:由于共享内存,切换开销小,但需要处理同步和互斥问题。

并发性不同:

进程:操作系统中有多进程并发执行。

线程:在同一进程中可以有多线程并发执行

27.如何解决多线程高并发问题

在C#中解决多线程高并发问题,通常涉及到以下几个方面:

使用lock关键字或Monitor类来同步访问共享资源。

使用Interlocked类处理原子操作

使用ConcurrentQueue, ConcurrentBag, ConcurrentDictionary等线程安全集合

使用Semaphore, Mutex, ReaderWriterLockSlim等同步机制

避免死锁,通过正确的锁顺序和锁层次结构。

使用异步编程(async/await)来避免线程阻塞

使用队列和工作线程池来管理并发任务。

对于数据库等资源,使用锁机制或乐观并发控制

28.说一下异步

异步编程主要是通过async和await关键字来实现的。async关键字用于声明异步方法,而await用于挂起方法的执行,直到等待的异步操作完成。这样可以在不阻塞调用线程的情况下执行操作

29.const和readonly的区别

静态常量是指编译器在编译时候会对常量进行解析,并将常量的值替换成初始化的那个值。而动态常量的值则是在运行的那一刻才获得的,编译器编译期间将其标示为只读常量,而不用常量的值代替,这样动态常量不必在声明的时候就初始化,而可以延迟到构造函数中初始化。

1.const默认是静态常量,readonly默认是动态常量,如果需要设置成静态,需要显示声明

修饰引用类型时不同const只能修饰基元类型,枚举类型或者字符串类型,readonly可以是任何类型。

2.const修饰的常量在声明的时候必须初始化;readonly修饰的常量则可以延迟到构造函数初始化

3.const修饰的常量在编译期间就被解析,即常量值被替换成初始化的值;readonly修饰的常量则延迟到运行的时候

4.const常量既可以声明在类中也可以在函数体内,但是static readonly常量只能声明在类

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值