Processes and Threads 入门

进程

进程是对正在运行程序的一个抽象。一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器和变量的当前值。从概念上说,每个进程拥有它自己的虚拟 CPU(实际真正的CPU在个进程之间来回切换)。各个进程有自己的内存空间、数据栈等,所以只能使用进程间通讯(interprocess communication,IPC),而不能直接共享信息。
进程是系统进行资源分配和调度的一个独立单位。

多进程

最直观的就是一个个pid,官方的说法就:进程是程序在计算机上的一次执行活动。一次main函数执行就是一个进程。
linux下创建子进程的调用是fork()。fork的作用是根据一个现有的进程复制出一个新进程,原来的进程称为父进程,新进程称为子进程。

fork函数“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。子进程中fork的返回值是0,而父进程中fork的返回值则是子进程的id(从根本上说fork是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当fork函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。

参考代码:

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h> 

void print_exit()
{
         printf("The exit pid:%d\n",getpid());
}

main()
{
         pid_t pid;
         pid=fork();     //创建新进程
         atexit(print_exit); //注册该进程退出时的回调函数
         if(pid<0)
                   printf("Error in fork!");
         else if (pid==0) //返回0,表示在子进程中运行
         {
                   printf("The return pid = %d\n",pid);
                   printf("I am the child process,my process id is %d\n",
                            getpid());
         }
         else  //返回子进程的pid,表示在父进程中运行
         {
                   printf("The return pid = %d\n",pid);
                   printf("I am the parent process,my process id is %d\n",
                         getpid());
                   sleep(2);
                   wait();
         }
}

wait和waitpid函数:

当进程退出时,向其父进程发送一个SIGCHLD信号,默认情况下总是忽略该信号,此时进程的状态一直保留在内存中,直到父进程调用wait函数手机状态信息,才会对它进行清理,父进程尚未调用wait或waitpid对它进行清理前,该进程状态称为僵尸进程。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。

可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进程间同步的作用。

进程间通信

在进行进程切换时,都要依赖于内核中的进程调度。

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。

主要有以下几种方式:
管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
管道是一种最基本的IPC机制,由pipe函数创建:

#include <unistd.h>
int pipe(int filedes[2]);

调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。

信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数signal外,还支持语义符合Posix.1标准的信号函数sigaction。

报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

 

线程

进程—独立分配资源的单位
线程—调度和分配的基本单位

p.s.进程的一个实体,基本不拥有资源,只有一点运行中必不可少的资源(如程序计数器、一组寄存器和栈),它可与同属一个进程的其他线程共享进程拥有的全部资源。
线程提高系统内程序并发执行的程度,从而进一步提高系统的吞吐量
p.s.每个进程有一个地址空间和一个控制线程,所以无法实现“在同一个地址空间中准并行运行多个控制线程的情形”。多线程加入了一种新的元素——并行实体共享同一个地址空间和所有可用数据的能力。这是多进程模型(它们具有不同的地址空间)所无法表达的。
线程比进程轻量级,所以更容易创建和撤销。
若多个线程都是CPU密集型的,那么并不能获得性能上的增强,但是若存在大量的计算和大量的IO处理,拥有多个线程允许这些活动彼此重叠进行,加快应用程序执行的速度。

多线程

线程ID类型:phtread_t
包含头文件:#include<pthread.h>
在Linux上线程函数位于libpthread共享库中,因此在编译时要加上-lpthread选项
创建线程函数:

int pthread_create(pthread_t *restrict tidp, //线程ID
                   const pthread_attr_t *restrict attr,//线程属性
                   void *(*start_routine)(void*),//新创建线程从start_routine开始执行
                   void *restrict arg);//执行函数的参数

返回值:成功-0;失败-返回错误编号(strerror(全局变量errno)得到错误信息)
获得线程ID函数:

pthread_t pthread_self(void);

线程Joining Threads函数:

int pthread_join(pthread_t thread, void **value_ptr);

若不存在pthread_join,主线程(main函数中)会很快return,从而导致整个进程结束,那么新创建的线程还没有执行结束。
调用该函数的线程将挂起等待,直到id为thread的线程终止,由value_ptr指向返回值,若对线程终止状态不关心,传NULL给参数value_ptr。
p.s.如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
3. 线程可以调用pthread_exit终止自己。
线程终止函数:

void pthread_exit(void *value_ptr);

pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

int pthread_cancel(pthread_t thread);

异步终结就是当其他线程调用pthread_cancel的时候,线程就立刻被结束。
同步终结则不会立刻终结,它会继续运行,直到到达下一个结束点(cancellation point)。
当一个线程被按照默认的创建方式创建,那么它的属性是同步终结。

线程实现:
在Linux中,新建的线程并不是在原先的进程中,而是系统通过 一个系统调用clone()。该系统copy了一个和原先进程完全一样的进程,并在这个进程中执行线程函数。不过这个copy过程和fork不一样。 copy后的进程和原先的进程共享了所有的变量,运行环境。这样,原先进程中的变量变动在copy后的进程中便能体现出来。


参考代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int num=0;
void *add(void *arg)
{        //线程执行函数,执行500次加法
    int  i = 0,tmp;
    for (; i <500; i++)
    {
           tmp=num+1;
           num=tmp;
           printf("add+1,result is:%d\n",num);
    }
    return ((void *)0);
}

void *sub(void *arg)
{      //线程执行函数,执行500次减法
    int i =0,tmp;
    for(;i<500;i++)
    {
           tmp=num-1;
           num=tmp;
           printf("sub-1,result is:%d\n",num);
    }
    return ((void *)0);
}

int main(int argc, char** argv)
{
    pthread_t tid1,tid2;
    int err;
    void *tret;
    //创建线程,返回线程号给tid1,线程函数入口add,参数NULL
    err=pthread_create(&tid1,NULL,add,NULL);
    if(err!=0)
    {
           printf("pthread_create error:%s\n",strerror(err));
           exit(-1);
    }
    err=pthread_create(&tid2,NULL,sub,NULL);
    if(err!=0)
    {
                     printf("pthread_create error:%s\n",strerror(err));
         exit(-1);
    }
    //阻塞等待线程id为tid1的线程,直到该线程退出,返回值赋给tret   
    err=pthread_join(tid1,&tret);
    if(err!=0)
    {
           printf("can not join with thread1:%s\n",strerror(err));
           exit(-1);
    }
         printf("After pthread1:return value = %d,
               pthread1 exit code = %d\n", err,(int)tret);       

    err=pthread_join(tid2,&tret);
    if(err!=0)
    {
           printf("can not join with thread2:%s\n",strerror(err));
           exit(-1);
    }
      printf("After pthread2:return value = %d,
             pthread2 exit code = %d\n", err,(int)tret);
    return 0;
}

线程间同步

A. 互斥量(mutex)——数据类型pthread_mutex_t
获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
Mutex初始化:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                    const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

如果Mutex变量是静态分配的(全局变量或static变量),可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。

pthread_mutex_t mylock=PTHREAD_MUTEX_INITIALIZER;

Mutex加解锁操作:

int pthread_mutex_lock (pthread_mutex_t *__mutex);
int pthread_mutex_unlock (pthread_mutex_t *__mutex);

如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。

int pthread_mutex_trylock(pthread_mutex_t *__mutex);

B. 读写锁Reader-Writer Lock——允许多个线程同时读,只能有一个线程同时写
Reader-Writer Lock比Mutex具有更好的并发性, 适用于读的次数远大于写的情况。

读写锁初始化:

int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock,
                __const pthread_rwlockattr_t *__restrict,
                __attr);
int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock);

读加锁:

int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock);

写加锁:

int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock);

解锁:

int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock);

C. 条件变量(Condition Variable)——数据类型pthread_cond_t
线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。
在pthread库中通过条件变量来阻塞等待一个条件,或者唤醒等待这个条件的线程。

条件变量初始化:

int pthread_cond_init (pthread_cond_t *__restrict __cond,
                  __const pthread_condattr_t *__restrict,
                  __cond_attr);
int pthread_cond_destroy (pthread_cond_t *__cond);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

条件等待,使用pthread_cond_wait等待条件为真:

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                       pthread_mutex_t *restrict mutex,
                       const struct timespec *restrict abstime)
int pthread_cond_wait (pthread_cond_t *__restrict __cond,
                  pthread_mutex_t *__restrict __mutex)

唤醒线程:

int pthread_cond_signal (pthread_cond_t *__cond);  //唤醒等待该条件的某个线程
int pthread_cond_broadcast (pthread_cond_t *__cond) //唤醒等待该条件的所有线程

p.s.一个Condition Variable总是和一个Mutex搭配使用的。

 

参考代码:生产者-消费者:LIFO

#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
/*LIFO*/
struct msg{
         struct msg *next;
         int num;
};

struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *consumer(void *p)
{
         struct msg *mp;
         for(;;)
         {
                   pthread_mutex_lock(&lock);
                   while(head == NULL)                            
                          pthread_cond_wait(&has_product,&lock);
                   mp = head;
                   head = mp->next;
                   pthread_mutex_unlock(&lock);
                   printf("Consume %d\n",mp->num);
                   free(mp);
                   sleep(rand()%5);
         }
}

void *producer(void *p)
{
         struct msg *mp;
         for(;;)
         {
                   mp = malloc(sizeof(struct msg));
                   mp->num = rand() % 1000 + 1;
                   printf("Produce %d\n",mp->num);
                   pthread_mutex_lock(&lock);
                   mp->next = head;
                   head = mp;
                   pthread_mutex_unlock(&lock);
                   pthread_cond_signal(&has_product);
                   sleep(rand()%5);
         }
}

int main(int argc,char *argv[])
{
         pthread_t pid,cid;
         srand(time(NULL));
         pthread_create(&pid,NULL,producer,NULL);
         pthread_create(&cid,NULL,consumer,NULL);
         pthread_join(pid,NULL);
         pthread_join(cid,NULL);
         return 0;
}

D. 信号量Semaphore
信号量(Semaphore)和Mutex类似,表示可用资源的数量,和Mutex不同的是这个数量可以大于1。这种信号量不仅可用于同一进程的线程间同步,也可用于不同进程间的同步。

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
int sem_destroy(sem_t * sem);

调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait()。调用sem_post()可以释放资源,使semaphore的值加1,同时唤醒挂起等待的线程。

 

Processes Vs. Threads

One thought on “Processes and Threads 入门”

  1. 感觉linux的进程跟线程界限不是那么清晰。不过还没写过linux下的多线程,给的例子很不错

Leave a Reply

Your email address will not be published. Required fields are marked *