【Linux】进程与线程(区别、进程的状态、线程的实现方式、创建、同步方法)

    xiaoxiao2022-07-05  160

    一、线程与进程的概念

    进程:一个正在运行的程序;

    线程:进程内部的一条执行路径/序列。

     

    二、进程与线程的区别

    1、进程是资源分配的最小单位,线程是CPU调度执行的最小单位;

    2、多进程,进程间调度都是独立的,同一进程中的多线程,资源是共享的;

    3、线程更加轻便、更加小巧,线程创建调度、切换调度都比进程高。

    4、线程必须依赖与进程;

    5、同一个进程中线程都是并发进行,并且线程的执行顺序不一样,由系统决定;

    进程与线程的关系:

    1、线程是存在进程的内部,一个进程中可以有多个线程,一个线程只能存在一个进程中。

    2、一个线程的结束进程不一定会退出,但是进程的退出,线程将退出。

    进程的状态

    进程在其生命周期内可以存在多个状态,主要有:就绪、运行、阻塞状态。

    状态图如下:

    就绪:所有资源准备完成,等待CPU空闲的状态;

    运行:在CPU上真正执行的状态;

    阻塞:等待某些事件发生,事件未发生前,不能被CPU调度的状态;

    就绪--->运行:系统进行进程调度;

    运行--->就绪:分配给进程的时间片用完;

    运行--->阻塞:需要某些事件的发生才能运行;(阻塞函数)

    阻塞--->就绪:等待的事件已经发生,只能转到就绪态,不能直接到运行态。

     

    三、线程的实现方式:(用户级、内核级、混合模式)

    1、用户级——线程的创建、销毁、管理都在用户空间完成,内核只会识别为一个进程,一条线程。

                          优点:1、灵活性,操作系统不知道线程存在,在任何平台都可以运行;

                                    2、线程切换块,线程切换在用户空间完成,不需要陷入内核;

                                    3、不用修改操作系统,实现容易;

                          缺点:1、编程复杂,用户必须自己管理线程;

                                    2、如果一个线程阻塞,整个进程都会阻塞;

                                    3、不能使用对称多处理器

    2、内核级——线程的创建、销毁、管理都由操作系统内核完成。内核线程使得用户编程简单,但是每次切换都得陷入内核,所以效率低。

    3、混合模式——就是一部分以用户级线程创建,一部分由内核创建,是一个多对多的关系。(结合用户级和内核级的优点)

    线程库的使用——线程创建

    线程库包含在头文件pthread.h中:

    线程创建函数:  int pthread_create(pthread_t*id,pthread_attr_t*attr,void*(*pthread_fun)(void*),void*arg);

          pthread_creare函数会创建一条新的函数线程,线程从pthread_fun函数入口地址开始执行,到pthread_fun函数结束。

          arg参数为pthread_fun函数传递的参数。attr为线程属性。

          返回值:pthread_create函数成功,返回0,失败返回错误码。

    线程结束函数:int pthread_exit(void *)

          主线程结束默认调用exit函数,这个函数会结束进程。进程结束,所有的线程也会随之结束

    等待线程结束函数:int pthread_join(pthread_t id,void **);

    终止一个线程:int pthread_cancel(pthread_t id);

          由于线程是进程中的一条执行序列,所以一个进程中的所有线程共享全局、堆区数据、包括打开的文件描述符。只有每个线程栈区的数据是线程独享的。

    四、线程同步的方法

    信号量、互斥锁、读写锁、条件变量

    线程同步:线程之间协同工作,用于多线程访问临界资源时,必须按照一定的先后顺序访问执行。

    1、信号量

    与进程间的信号量作用相似,当线程访问一些有限的共享资源时,就必须做到线程间同步访问。

    信号量的使用方式:

           初始化:int sem_init(sem_t *sem,int shared,int val);

         信号量一般被定义在线程共享的全局数据区 ,初始化函数是将信号量sem的初始值设置为val,shared参数控制这个信号量是否可以在多个进程之间共享,但Linux对此不支持。

       P操作:int sem_wait(sem_t *sem);

                对信号量sem进行-1操作,如果结果小于0,则此函数调用会阻塞,直到有其他线程执行V操作。

       V操作:int sem_post(sem_t *sem);

               对信号量sem进行+1操作。

       销毁:int sem_destroy(sem_t *sem)

     

    2、互斥锁

            只能在线程之间使用的一种控制临界资源访问的机制,如果一个线程要访问临界资源,则必须先加锁,用完之后解锁。这样,在一个线程访问临界资源的过程中,其他线程加锁就会阻塞,不能进入访问临界资源的临界区,直到访问临界资源的线程用完后并解锁。

           一般被定义在线程共享的全局数据区。

    互斥锁的使用方式:

    初始化:int pthread_mutex_init(pthread_mutex_t *mutex,pthread_mutexattr_t *attr); 

                attr是锁的属性,

    加锁:int pthread_mutex_lock(pthread_mutex_t *mutex);        (会阻塞)

             int pthread_mutex_trylock(pthread_mutex_t *mutex);         (不会阻塞)

    解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex);

    销毁锁:int pthread_mutex_destroy(pthread_mutex_t *mutex);

    自旋锁与互斥锁的区别:

            若线程不能获取锁,互斥量是通过休眠的方式(线程被暂时取消调度,切换至其他可运行的线程)来阻塞线程;而自旋锁则是通过忙等的方式(线程不会被取消调度,一直处于运行状态)来阻塞线程。

    3、读写锁

    与互斥锁相似,但与互斥锁不同的是:互斥量会把试图进入已保护的临界区的线程都阻塞;然而读写锁会视当前进入临界区的线程和请求进入临界区的线程的属性来判断是否允许线程进入。

    相对互斥量只有加锁和不加锁两种状态,读写锁有三种状态:读模式下的加锁,写模式下的加锁,不加锁。

    使用规则:

       (1)只要没有写模式下的加锁,任意线程都可以进行读模式下的加锁;

       (2)只有读写锁处于不加锁状态时,才能进行写模式下的加锁;

    /* 初始化读写锁属性对象 */ int pthread_rwlockattr_init (pthread_rwlockattr_t *__attr);   /* 销毁读写锁属性对象 */ int pthread_rwlockattr_destroy (pthread_rwlockattr_t *__attr);   /* 获取读写锁属性对象在进程间共享与否的标识*/ int pthread_rwlockattr_getpshared (__const pthread_rwlockattr_t * __restrict __attr,                                           int *__restrict __pshared);   /* 设置读写锁属性对象,标识在进程间共享与否  */ int pthread_rwlockattr_setpshared (pthread_rwlockattr_t *__attr, int __pshared);                                                       返回值:成功返回0,否则返回错误代码

    /* 读模式下加锁 */ int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock); /* 非阻塞的读模式下加锁 */ int pthread_rwlock_tryrdlock (pthread_rwlock_t *__rwlock); # ifdef __USE_XOPEN2K /* 限时等待的读模式加锁 */ int pthread_rwlock_timedrdlock (pthread_rwlock_t *__restrict __rwlock, __const struct timespec *__restrict __abstime); # endif /* 写模式下加锁 */ int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock); /* 非阻塞的写模式下加锁 */ int pthread_rwlock_trywrlock (pthread_rwlock_t *__rwlock); # ifdef __USE_XOPEN2K /* 限时等待的写模式加锁 */ int pthread_rwlock_timedwrlock (pthread_rwlock_t *__restrict __rwlock, __const struct timespec *__restrict __abstime); # endif /* 解锁 */ int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock); 返回值:成功返回0,否则返回错误代码

    应用:

           读写锁也称为共享-独占(shared-exclusive)锁,当读写锁以读模式加锁时,它是以共享模式锁住,当以写模式加锁时,它是以独占模式锁住。读写锁非常适合读数据的频率远大于写数据的频率从的应用中。这样可以在任何时刻运行多个读线程并发的执行,给程序带来了更高的并发度。

    4、条件变量

         (1)互斥锁是用来给资源上锁的,而条件变量是用来等待而不是用来上锁的。

         (2)条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。

         (3)通常条件变量和互斥锁同时使用。

    初始化:int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);

    销毁:int pthread_cond_destroy(pthread_cond_t *cond);

    等待:int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restric mutex);(阻塞等待)

              int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout); (超时等待)

            等待条件函数等待条件变为真, 传递给pthread_cond_wait的互斥量对条件进行保护, 调用者把锁住的互斥量传递给函数. 函数把调用线程放到等待条件的线程列表上, 然后对互斥量解锁, 这两个操作是原子的. 这样便关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道, 这样线程就不会错过条件的任何变化.

        当pthread_cond_wait返回时, 互斥量再次被锁住.

    通知条件:

          int pthread_cond_signal(pthread_cond_t *cond);    int pthread_cond_broadcast(pthread_cond_t *cond);

           这两个函数用于通知线程条件已经满足. 调用这两个函数, 也称向线程或条件发送信号. 必须注意, 一定要在改变条件状态以后再给线程发送信号.

    问题:

    1、同一进程中的所有线程对于数据共享问题

        ①栈区:不共享(创建线程时,将栈区数据的地址传递函数线程)

        ②全局:共享 (虚拟地址相等,同一进程中的多个线程共享进程的4G虚拟地址空间,页表→进程)

        ③堆区:共享(同上)

        ④文件描述符

    2、进程间通讯

       ①将数据定义到全局

       ②将数据保存到堆区

       ③栈区→创建线程时,将栈区数据的地址传递给函数线程

    最新回复(0)