13.进程和线程简介

Golang
378
0
0
2022-08-13
标签   Golang基础

进程和线程

注意事项

1.子进程会拷贝父进程的所有资源,变量。

注意:子进程拷贝了父进程数据空间、堆、栈等资源的副本,

2.父子进程间不共享这些存储空间,共享的空间只有代码段,

子进程修改一个全局变量,父进程的这个全局变量不会改变,因为是一个副本。

比较
1.进程是资源分配的基本单位。
2.线程是独立调度的基本单位。
3.在同一个进程中,线程的切换不会引起进程的切换。在不同的进程中进行线程切换,如从一个进程中的线程切换到另一个进程中的线程会引起进程的切换。
4.一个进程至少包含一个线程,线程共享整个进程的资源
5.进程结束后它所拥有的所有线程都将被销毁,但是线程结束并不响应其他线程
6.线程运行时一般都需要同步和互斥,因为他们共享进程的所有资源
7.线程有自己的私有TCB,线程id,进程也有自己的PCB,进程id
8.在开销方面:每个进程都有独立的数据空间,进程之间的切换会有较大的开销,线程是共享数据空间的,线程之间的切换开销会小很多。创建一个进程需要给他申请内存空间,创建线程则不需要,相比之下创建进程比创建线程的开销大很多。线程可分为用户级线程和内核级线程
进程和线程之间通讯
每个进程有自己的地址空间。两个进程中的地址即使值相同,实际指向的位置也不同。进程间通信一般通过操作系统的公共区进行。同一进程中的线程因属同一地址空间,可直接通信。进程不仅是系统内部独立运行的实体,而且是独立竞争资源的实体。
线程也被称为轻权进程,同一进程的线程共享全局变量和内存,使得线程之间共享数据很容易也很方便,但会带来某些共享数据的互斥问题。许多程序为了提高效率也都是用了线程来编写。父子进程的派生是非常昂贵的,而且父子进程的通讯需要ipc或者其他方法来实现,比较麻烦。而线程的创建就花费少得多,并且同一进程内的线程共享全局存储区,所以通讯方便。线程的缺点也是由它的优点造成的,主要是同步,异步和互斥的问题,值得在使用的时候小心设计。
线程间通信:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度)。

一、进程间的通信方式

1.管道( pipe ):

管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

2.有名管道 (namedpipe) :

有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

3.信号量(semophore ) :

信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

4.消息队列( messagequeue ) :

消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

5.信号 (sinal ) :

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

6.共享内存(shared memory ) :

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
套接字(socket ) :
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。

二、线程间的通信方式

1.锁机制

互斥锁提供了以排他方式防止数据结构被并发修改的方法。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用

2.信号量机制(Semaphore):

包括无名线程信号量和命名线程信号量

3.信号机制(Signal):类似进程间的信号处理

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

具体案例

以Go为例

一个进程在启动的时候,会创建一个主线程,这个主线程结束的时候,程序进程也就终止了,所以一个进程至少有一个线程,这也是我们在main函数里,使用goroutine的时候,要让主线程等待的原因,因为主线程结束了,程序就终止了,那么就有可能会看不到goroutine的输出。
go语言中并发指的是让某个函数独立于其他函数运行的能力,一个goroutine就是一个独立的工作单元,Go的runtime(运行时)会在逻辑处理器上调度这些goroutine来运行,一个逻辑处理器绑定一个操作系统线程,所以说goroutine不是线程,它是一个协程,也是这个原因,它是由Go语言运行时本身的算法实现的。

概念 说明 进程 一个程序对应一个独立程序空间 线程 一个执行空间,一个进程可以有多个线程 逻辑处理器 执行创建的goroutine,绑定一个线程 调度器 Go运行时中的,分配goroutine给不同的逻辑处理器 全局运行队列 所有刚创建的goroutine都会放到这里 本地运行队列 逻辑处理器的goroutine队列

当我们创建一个goroutine的后,会先存放在全局运行队列中,等待Go运行时的调度器进行调度,把他们分配给其中的一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中,最终等着被逻辑处理器执行即可。
这一套管理、调度、执行goroutine的方式称之为Go的并发。并发可以同时做很多事情,比如有个goroutine执行了一半,就被暂停执行其他goroutine去了,这是Go控制管理的。所以并发的概念和并行不一样,并行指的是在不同的物理处理器上同时执行不同的代码片段,并行可以同时做很多事情,而并发是同时管理很多事情,因为操作系统和硬件的总资源比较少,所以并发的效果要比并行好的多,使用较少的资源做更多的事情,也是Go语言提倡的。

案例1

func main() {
  //设置cpu核数
  runtime.GOMAXPROCS(runtime.NumCPU())
  //这里的sync.WaitGroup其实是一个计数的信号量,使用它的目的是要main函数等待两个goroutine执行完成后再结束,不然这两个goroutine还在运行的时候,程序就结束了,看不到想要的结果。
    var wg sync.WaitGroup
    //先是使用Add 方法设设置计算器为2,每一个goroutine的函数执行完之后,就调用Done方法减1。Wait方法的意思是如果计数器大于0,就会阻塞,所以main 函数会一直等待2个goroutine完成后,再结束。
    wg.Add(2)
    //创建一个goroutine是通过go 关键字的,其后跟一个函数或者方法即可
    go func(){
    //每一个goroutine的函数执行完之后,就调用Done方法减1
        defer wg.Done()
        for i:=1;i<4;i++ {
            fmt.Println("A:",i)
            time.Sleep(time.Second * 1)
        }
    }()
    go func(){
        defer wg.Done()
        for i:=1;i<4;i++ {
            fmt.Println("B:",i)
            time.Sleep(time.Second * 2)
        }
    }()
    wg.Wait()
}
//返回数据
//我们运行这个程序,会发现A和B前缀会交叉出现,并且每次运行的结果可能不一样,这就是Go调度器调度的结果。
//两个协程不会顺序执行的,是没有顺序的,之所以我们看到的有时候有顺序,是因为运行时间太短,加上休眠时间就可以看出来了

B: 1
A: 1
A: 2
A: 3
B: 2
B: 3