Operation System: Multi-thread 多线程 v.s. 多进程

篇初,问一个很经典的问题,线程和进程的区别是啥呀?


首先,从做的事情而言,线程之间通常会share共同的逻辑代码,而进程之间却很少会share逻辑的。这从Java的job - worker模式的多线程环境便可得知。

其次,从资源角度而言,线程没有自己独立的地址空间,属于同一个进程的线程共享一个线程的地址空间。而每一个单独的进程都有自己的地址空间。

进程之间的通信需要通过进程间通信(Inter-process communication,IPC)。与之相对的,同一进程的各线程间之间可以直接通过传递地址或全局变量的方式传递信息。


最后,提醒一点,每个线程有自己的线程 id,有自己的用来保存局部变量的栈,但是其它的线程是可以修改的。(一个线程的局部变量并不是受保护的)。


对于IPC的方法(来源:http://wdxtub.com/interview/14520847747820.html):


管道(PipeLine):管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。(标准的生产者-消费者模式)


无名管道:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系(通常是指父子进程关系)的进程间使用。
命名管道:命名管道也是半双工的通信方式,在文件系统中作为一个特殊的设备文件而存在,但是它允许无亲缘关系进程间的通信。当共享管道的进程执行完所有的I/O操作以后,命名管道将继续保存在文件系统中以便以后使用。


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


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


信号:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。(就好像终端进程收到crtl + c的信号)。


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


套接字(Socket):套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。


并行与同步


提及多线程,并行与同步一直是一个永恒的话题。同步的对象是多个线程共享的变量。在Java当中,共享的变量指的是static区域和堆区域的变量。而在C/C++中,指的是全局变量或者局部静态变量。


对于全局(静态/ 非静态)变量、局部变量和局部静态变量的区别如下:


全局变量:在函数外声明的变量
虚拟内存中有全局唯一的一份实例

局部静态变量:在函数内用 static 关键字声明的变量
虚拟内存中有全局唯一的一份实例


局部变量:在函数内声明,且没有用 static 关键字
每个线程的栈中都保存着对应线程的局部变量


首先,在c语言当中,static的含义与java是不同的。static在java中是代表这个变量、方法是属于整个类的(存储方式是只有一份,两者在这一点上是相同的),但是c中表示这个变量只是属于这份源代码。全局变量(非静态)的作用域一般是整份源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。但是如果加了static的话,只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。


线程的分类


线程实际上分为两类:用户级线程(User Level)和内核级线程 (Kernal Level)。只有内核级线程是真正的分核运行的,


用户级线程操作系统的根本不可见的。对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线“程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。 用户级线程的好处是非常高效,不需要进入内核空间。


内核级线程的管理工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核级线程的好处是,内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。


线程锁的分类:


当用户创立多个线程/进程时,如果不同线程/进程同时读写相同的内容,则可能造成读写错误,或者数据不一致。此时,需要通过加锁的方式,控制核心区域(critical section)的访问权限。对于semaphore而言,在初始化变量的时候可以控制允许多少个线程/进程同时访问一个critical section,其他的线程/进程会被堵塞,直到有人解锁。


Mutex相当于只允许一个线程/进程访问的semaphore,是最简单的锁类型。


Semaphore 在C语言中用P/ V信号量实现。当信号量为0时,禁止任何的进程进入核心区域。而当一个线程离开核心区域的时候,调用V增加信号量。进入核心区域时,调用P,减小信号量。

代码来源:http://wdxtub.com/2016/04/16/thin-csapp-9/

// 先定义信号量
volatile long cnt = 0;
sem_t mutex;
Sem_init(&mutex, 0, 1);
// 在线程中用 P 和 V 包围关键操作
for (i = 0; i < niters; i++)
{
    P(&mutex);
    cnt++;
    V(&mutex);
}

volatile的含义是让编译器知道,每一次都要从内存取值,更新后也要及时更改内存。


读写锁(Read-Write lock)读写锁允许多个线程同时进行读访问,但是在某一时刻却最多只能由一个线程执行写操作。对于多个线程需要同时读共享数据却并不一定进行写操作的应用来说,读写锁是一种高效的同步机制。


旋转锁(Spin Lock) 旋转锁是一种非阻塞锁,由某个线程独占。采用旋转锁时,等待线程并不静态地阻塞在同步点,而是必须“旋转”,不断尝试直到最终获得该锁。旋转锁多用于多处理器系统中。这是因为,如果在单核处理器中采用旋转锁,当一个线程正在“旋转”时,将没有执行资源可供另一释放锁的线程使用。旋转锁适合于任何锁持有时间少于将一个线程阻塞和唤醒所需时间的场合。


生产者-消费者问题


生产者消费者主要指这么一种场景:生产者有一堆线程,而消费者也有一堆线程。消费者的线程如狼似虎般等待着生产者生产的内容。


那么比较理想的状态是:生产者能够按照消费者的线程数目和节奏,每次刚刚好生产那么多内容给消费者消化,同时也不让消费者闲着。(我们想达到的状态)


然而,现实的却是出现:生产者经过过量生产,没有足够的消费者去消化,这时候需要一种机制去通知生产者别生产了。也有时候,生产者生产不够,有消费者线程在瞎忙活。


所以,这个producer和consumer的问题的处理关键是:通知对方的问题。


模型如图(来源:http://wdxtub.com/2016/04/16/thin-csapp-9/):



具体的同步模型为:


生产者等待缓冲区空的 slot,把 item 存储到 buffer,并通知消费者
消费整等待缓冲区 item,从 buffer 中移除 item,并通知生产者

现实场景的应用例子:


多媒体处理

生产者生成 MPEG 视频帧,消费者进行渲染


来自小土刀的博客(http://wdxtub.com/2016/04/16/thin-csapp-9/)给出的做法是利用一个Mutex去保护共享的缓冲区,而使用semaphore去实现item和slot的计数。在每次修改缓冲区的内容之前,都要先经过semaphore的确认后,再获得Mutex来更新缓冲区。


代码如下:


#include "csapp.h"
typedef struct {
    int *buf;    // Buffer array
    int n;       // Maximum number of slots
    int front;   // buf[(front+1)%n] is first item
    int rear;    // buf[rear%n] is the last item
    sem_t mutex; // Protects accesses to buf
    sem_t slots; // Counts available slots
    sem_t items; // Counts available items
} sbuf_t;
void sbuf_init(sbuf_t *sp, int n);
void sbuf_deinit(sbuf_t *sp);
void sbuf_insert(sbuf_t *sp, int item);
int sbuf_remove(sbuf_t *sp);

// sbuf.c
// Create an empty, bounded, shared FIFO buffer with n slots
void sbuf_init(sbuf_t *sp, int n) {
    sp->buf = Calloc(n, sizeof(int));
    sp->n = n;                  // Buffer holds max of n items
    sp->front = sp->rear = 0;   // Empty buffer iff front == rear
    Sem_init(&sp->mutex, 0, 1); // Binary semaphore for locking
    Sem_init(&sp->slots, 0, n); // Initially, buf has n empty slots
    Sem_init(&sp->items, 0, 0); // Initially, buf has 0 items
}
// Clean up buffer sp
void sbuf_deinit(sbuf_t *sp){
    Free(sp->buf);
}
// Insert item onto the rear of shared buffer sp
void sbuf_insert(sbuf_t *sp, int item) {
    P(&sp->slots);                        // Wait for available slot
    P(&sp->mutext);                       // Lock the buffer
    sp->buf[(++sp->rear)%(sp->n)] = item; // Insert the item
    V(&sp->mutex);                        // Unlock the buffer
    V(&sp->items);                        // Announce available item
}
// Remove and return the first tiem from the buffer sp
int sbuf_remove(sbuf_f *sp) {
    int item;
    P(&sp->items);                         // Wait for available item
    P(&sp->mutex);                         // Lock the buffer
    item = sp->buf[(++sp->front)%(sp->n)]; // Remove the item
    V(&sp->mutex);                         // Unlock the buffer
    V(&sp->slots);                         // Announce available slot
    return item;
}

关于Java当中如何实现mutex, semaphore, read-writer lock以及生产者-消费者问题,如看另外一篇文章:


本文完。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值