进程与线程

核能气质少年 提交于 2020-01-10 13:26:26

进程与线程

概念:什么是线程:是操作系统能够进行运算调度的最小单位。什么是进程:是计算机中某一次数据集合的运行活动,是操作系统分配资源的最小单位。线程依赖于进程,进程就是线程的容器。

上面的描述比较官方,我的理解就是:想要通过计算机来完成某件事件那么你就得有一个进程来帮助你完成这个任务,也就是说,进程等价于利用计算机完成任务的一种手段,操作系统会为进程去分配各种资源,内存什么的,那么线程就是把进程分配成一个个小任务去执行,也就是说是进程执行的最小单位。

两个问题:

  • 进程就是一个个跑在操作系统上的程序,那么如果想让两个进程或者多个进程之间通信怎么办?
  • 线程是进程运行的最小单位,那么如果两个线程用到了同一个资源,怎么进行同步,否则这个资源的使用就乱套了

也就是说:进程要解决通信问题,否则就是单击小程序,线程要解决同步问题,也就是资源共享在线程中的问题。

进程通信

在Linux中进程通信有六种基本的方式:

无名管道:
  • 是一种不属于任何文件系统的特殊文件,可以使用read和write
  • 原型#include <unistd.h>2 int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1

创建了两个文件描述符,一个读一个写,通常用来进行父子进程间通信。

命名管道:FIFO

这个是一种文件类型,可以在无关进程之间交换数据,以一种特殊设备文件存在于文件系统之中

1 #include <sys/stat.h>
2 // 返回值:成功返回0,出错返回-1
3 int mkfifo(const char *pathname, mode_t mode);

mode就是和open函数的那种mode是一样的,指定文件的I/O方式

其实这种方式很好理解,两个进程借助这个文件进行通信,就是往里面读写数据的过程。

消息队列:

消息队列这种方式的应用很广泛,肯定都知道MQ这种异步任务处理机制。

  • 是消息的链表,存放在内核之中,一个消息队列用一个标识符即(队列ID)来标识。
  • 消息队列会记录消息
  • 消息队列是独立于进程的,它们结束时,消息队列也不会被删除
  • 消息队列可以实现随机查询,不一定非要按次序
#include <sys/msg.h>
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

很好理解,一个消息队列用一个唯一key标识,消息有消息类型,添加读取按照消息类型或者按顺序读取都是可以的。

信号量:

信号量与之前的思路都不一样了,之前都是一种IPC的结构,也就是通过一块中间部件完成进程之间的通信,而信号量是一个计数器,主要用于进程和线程的同步与互斥操作。

  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
  2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
  4. 支持信号量组。
#include <sys/sem.h>
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);  
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);

struct sembuf 
{
    short sem_num; // 信号量组中对应的序号,0~sem_nums-1
    short sem_op;  // 信号量值在一次操作中的改变量
    short sem_flg; // IPC_NOWAIT, SEM_UNDO
}
共享内存:

共享内存亦是一种IPC的通信方式

  1. 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

  2. 因为多个进程可以同时操作,所以需要进行同步。

  3. 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

    #include <sys/shm.h>
    // 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
    int shmget(key_t key, size_t size, int flag);
    // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
    void *shmat(int shm_id, const void *addr, int flag);
    // 断开与共享内存的连接:成功返回0,失败返回-1
    int shmdt(void *addr); 
    // 控制共享内存的相关信息:成功返回0,失败返回-1
    int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
    

Socket通信:

这个就是网络通信,我们现在使用的不同计算机之间的通信基本都是通过socket完成的,不是三言两语就能说清楚的。

线程之间的同步

线程之间同步的方式:

互斥锁:互斥锁就是一种锁机制
  • 原子性:互斥锁就是把一个互斥量锁定的操作,这种操作是原子操作,如果一个线程锁定了这个互斥量,其它线程就无法再锁定了。
  • 非繁忙的等待:一个线程已经锁定一个互斥量的情况下,另一个线程在尝试锁定时就会被挂起,等待其它线程解锁。

这么一说线程在使用进程的共享资源时,先加锁,然后使用结束就进行解锁,这样就实现了线程之间的同步。

#include <pthread.h>
#include <time.h>
// 初始化一个互斥锁。
int pthread_mutex_init(pthread_mutex_t *mutex, 
						const pthread_mutexattr_t *attr);

// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,
// 直到互斥锁解锁后再上锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 调用该函数时,若互斥锁未加锁,则上锁,返回 0;
// 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
int pthread_mutex_trylock(pthread_mutex_t *mutex);

// 当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量
// 原语允许绑定线程阻塞时间。即非阻塞加锁互斥量。
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);

// 对指定的互斥锁解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 销毁指定的一个互斥锁。互斥锁在使用完毕后,
// 必须要对互斥锁进行销毁,以释放资源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);

这就是对互斥量操作的函数原型。

那么这里有一个问题,对于一个临界资源,有两个线程访问int num,线程A先加锁操作了,然后它在执行操作时判断num > 0就进行后续操作num–,否则会一直循环,可以理解为它是消费者,线程B是一个生产者它内部需要num++,但是现在锁被A霸占,它这边加锁阻塞。好了,这就是死锁了,即你在等待我,我在等待你,永远也不会成功的。那么就轮到条件变量登场了。

条件变量

这时候,应该怎么用呢?

A在判断条件不满足时不应该循环判断,这时候应该先设置一个条件变量,然后解锁,在这里等待条件变量即可,这时候B就能获得锁了,然后操作完以后,通过条件变量通知A条件满足了,A就可以解阻塞了。

#include <pthread.h>
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond,
						pthread_condattr_t *cond_attr);

// 阻塞等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

// 超时等待
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,
						const timespec *abstime);

// 解除所有线程的阻塞
int pthread_cond_destroy(pthread_cond_t *cond);

// 至少唤醒一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);

// 唤醒等待该条件的所有线程,广播的方式
int pthread_cond_broadcast(pthread_cond_t *cond);  

注意我们要使用:

while(xxx == 1)
{
    pthread_cond_wait(cond,mutex);
}
//不能使用
if(xxx == 1)
{
    pthread_cond_wait(cond,mutex);
}

因为条件变量返回时,不一定是条件满足了,所以需要再次判断,也就是虚假唤醒线程。

读写锁

读写锁与互斥索类似

  • 如果有其它线程读操作时,允许其它线程进行读操作,但是不允许写操作。
  • 如果其它线程写操作时,则不允许其它线程进行读写操作。

其实很好理解,以一个变量为例,如果我正在读,那么你也是能来读的,但是你不能来改,如果我在写,那你们就等我弄好再操作,读写都不行了

规则就是根据特点来的

  • 如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁;
  • 如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。

原型,通过这些特点我们可以知道,读写锁适合读操作比写操作多很多的情况:

#include <pthread.h>
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, 
						const pthread_rwlockattr_t *attr); 

// 申请读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); 

// 申请写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock ); 

// 尝试以非阻塞的方式来在读写锁上获取写锁,
// 如果有任何的读者或写者持有该锁,则立即失败返回。
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 

// 解锁
int pthread_rwlock_unlock (pthread_rwlock_t *rwlock); 

// 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

自旋锁

与互斥锁基本一样,但是注意:自旋锁在进行锁等待的时候不会让出CPU,也就是说它是繁忙等待,但是互斥锁会让出CPU,一般用户态很少用自旋锁,在内核中使用较多。自旋锁一般在锁持有时间比较短或者小于两次上下文切换情况下使用。自旋锁的接口就是把互斥锁的mutex换成spin

信号量

信号量本质就是一个非负的整数计数器,被广泛的用在进程和线程的同步和互斥中。在编程时,使用信号量就像在使用一个flag的标记位一样,在访问共享资源时,如果信号量大于0则可以访问,否则不能访问并且阻塞。对信号量的操作叫做PV操作,即-1,+1操作。

同步问题:依次打印abcdef…

// 信号量用于同步实例
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
 
sem_t sem_g,sem_p;   //定义两个信号量,用于同步操作
char ch = 'a';
 
void *pthread_g(void *arg)  //此线程改变字符ch的值
{
    while(1)
    {
        sem_wait(&sem_g);//阻塞
        ch++;
        sleep(1);
        sem_post(&sem_p);//让另一个解阻塞
    }
}
 
void *pthread_p(void *arg)  //此线程打印ch的值
{
    while(1)
    {
        sem_wait(&sem_p);//第一句执行
        printf("%c",ch);
        fflush(stdout);
        sem_post(&sem_g);//让另一个解阻塞
    }
}
 
int main(int argc, char *argv[])
{
    pthread_t tid1,tid2;
    sem_init(&sem_g, 0, 0); // 初始化信号量为0
    sem_init(&sem_p, 0, 1); // 初始化信号量为1,则先执行这个下面的
    
    // 创建两个线程
    pthread_create(&tid1, NULL, pthread_g, NULL);
    pthread_create(&tid2, NULL, pthread_p, NULL);
    
    // 回收线程
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    return 0;
}

**总结:**P操作减1,V操作加1,两个线程同步,线程A先对第一个信号量a进行减1操作,然后打印字符,然后再对信号量b进行V操作,然后发信a信号量阻塞,线程B开始信号量b为0阻塞,A进行V操作后,B就可以解阻塞了,然后为字符加1,然后对信号量a进行V操作,让A线程解阻塞,这样就实现了两个线程的同步。

信号量用于互斥:

这个就类似与锁,初始化一个信号量a=1,然后,两个线程都进行对a进行P操作,如果有一个成功,另一个必定阻塞,当用完后再进行V操作,那么另一个阻塞的就会解阻塞。这就和互斥锁原理一样的。

总结:

进程之间应该解决通信问题,也需要互斥和同步,可以通过信号量和共享内存配合解决,线程主要问题就是同步,使用互斥锁,信号量,条件变量解决。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!