想必大家在使用计算机时都知道可以同时打开多个软件,比如Word、Visual Studio、QQ音乐。通常在办公的时候或者程序员在编程的时候,一边开发软件,一边听着歌曲。其实,这是操作系统为这三款不同的程序开辟了彼此独立的内存,以保证它们的良好运行。每一个程序都代表一个进程(Process)。进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程(Thread),一个进程可以运行多个线程,多个线程可共享数据。

进程与线程的概念_数据

笔者工作之地是深圳某某科技园区,里面入驻了上百家企业。如果把这个科技园区看成是一台计算机的话,那么科技园区的所属管理单位就是操作系统,里面的每家企业就是一个程序,也就是一个进程,而每个企业都可能有研发部、销售部、财务部、生产部等等,那么这些部门就是线程。每个部门都有自己的小金库——团队建设费。我们可以把每个线程的小金库理解成线程堆栈(Stack Memory),这部分内存是每个部门的自留地,私有财产神圣不可侵犯,所以线程堆栈只为线程服务,用于保存自身的一些数据,如函数中定义的局部变量、函数调用时传送的参数值等。所以值类型的数据都会保存在线程堆栈中,换句话说,程序员申请的所有值类型都会在线程堆栈中得到分配。

进程与线程的概念_堆栈_02

有时候,部门和部门之间也是需要共享数据的,比如我们部门经常要借用生产部的设备材料。那这些设备材料就不能放在小金库里面了,于是,它们必须要放在大家都看得见够得着的地方——堆内存(Heap Memory)。在.NET这种托管环境下,堆由CLR(Common Language Runtime)管理,所以又称托管堆Managed Heap。例如使用new关键字创建类的对象实例时,分配给对象的内存单元就位于托管堆中。

我们在这里讲解进程和线程的概念,是因为将来开发程序时,往往要在一个程序中创建多个线程。C#程序拥有一个主线程,通常指UI线程,但是我们不能把所有代码都写在主线程中。如果某个执行单元需要耗费大量的时间,这时我们可以把这个执行单元看成是一个任务,然后创建一个子线程,在子线程中去执行这个任务。这样做的好处是,UI线程不会出现卡顿的情况,它会继续响应用户的键盘或鼠标操作,而背后的子线程同时也执行着我们需要处理的业务需求。

我们把开发拥有多个线程的程序称为多线程开发。在多线程开发中,往往需要访问同一片内存区域,比如对某个对象进行读写操作,由于堆内存上的数据可以共享,所以往往有多个线程在同时写堆内存上的数据时,会出现资源抢夺。意思是说,当两个线程在同一时钟周期尝试写入同一个内存地址时,可能会发生竞态条件(Race Condition)。

竞态条件是指多个线程或进程同时访问共享资源,并且最终的结果依赖于它们执行的相对速度或顺序。假如线程1和线程2同时写入某个内存地址,结果是不确定的:这可能导致内存中保存的是线程1和线程2的写入值的组合,或者是其中一个线程的写入值。这种竞态条件可能导致程序的行为不一致、数据损坏或其他错误。为了避免竞态条件,可以使用同步机制,如互斥锁(Mutex)或信号量(Semaphore),来确保在同一时间只有一个线程可以访问共享资源。这样可以保证线程按照正确的顺序进行写入操作,避免竞态条件的发生。