进程与线程之间的关系与区别
线程是计算机中独立运行的最小单位,运行时占用很少的系统资源。由于每个线程占用的CPU时间是由系统分配的。因此可以把线程看成系统分配CPU时间的基本单位。在用户看来,多个线程是同时执行的,但从操作系统调度上看,对于单核CPU的话,各个线程是交替执行的。系统不停地在各个线程之间切换,每个线程只有在系统分配给它的时间内才能取得CPU的控制权,执行线程中的代码。
在支持多进程的情况下引入多线程原因:
(1)在多进程情况下,每个进程都有自己独立的地址空间,而在多线程的情况下,同一进程内的线程共享进程的地址空间。因此,创建一个新的进程时要耗费时间来为其分配系统资源而且进程个数是十分有限的,而创建一个新的线程花费的时间就少的多。
(2)由于进程地址空间独立而线程共享地址空间,线程间的切换速度要远快过进程间的切换速度
(3)进程间的数据空间相互独立,进程间通信要以专门的通信方式进行,通信时必须经过操作系统。而同一进程内的多个线程共享数据空间,一个线程的数据可以直接提供给其他的线程使用,而不必经过操作系统。只需要注意同步问题即可。
可以提高多处理器效率,现在许多计算机都是采用多核技术,在这种情况下。可以多个线程在不同的处理器上同时运行。从而大大提高程序执行速度。因此多线程更能发挥硬件潜力
可以改善程序结构。对于要处理多个命令的应用程序。可以将对每个命令的处理设计成一个线程。从而避免设计成大程序时造成的程序结构复杂。
线程虽然分享地址空间,打开的文件描述符等资源,但是也私有一些数据信息:
1.线程号 2.寄存器(包括程序计数器和堆栈指针)
3.堆栈 4.信号掩码
5.优先级 6.线程私有的存储空间(下面会说到私有数据的问题)
创建线程:
创建线程的函数: int pthread_create (pthread_t *thread, pthread_attr_t * attr, void *(start_routine)(void*), void *arg)
第一个参数是一个返回类型为线程ID的指针
第二个参数是一个用于指定指定线程的属性,NULL表示默认属性.其结构体定义如下:
typedef struct{ int detachstate; //表示新创建的线程是否与进程中其他线程脱离同步。detachstate的缺省值为PTHREAD_CREATE_JOINABLE状态,这个属性也可以用函数pthread_detach()来设置。如果将detachstate设置为PTHREAD_CREATE_DETACH状态,则detachstate不能再恢复到PTHREAD_CREATE_JOINABLE状态。 int schedpolicy; //表示新线程的调度策略,主要包括SCHED_OTHRE(正常,非实时),SCHED_RR(实时,轮转法)和SCHED_FIFO(实时,先入先出)3种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效 struct sched_param schedparam; //一个struct sched_param结构,其中有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,缺省为0 int inheritsched; //有两种值可供选择,PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程显示指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED int scope; //表示线程间竞争CPU的范围,也就是说线程优先级的范围。POSIX的标准中定义了两个值,PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU,后者表示仅与同进程中的线程竞争CPU size_t guardsize; //警戒堆栈的大小 int stackaddr_set; //堆栈地址集 void * stackaddr; //堆栈的地址 size_t stacksize; //堆栈的大小 }pthread_attr_t;设置其属性linux给出了一系列函数,这里不做详述.
pthread_create的第三个参数为一个函数指针,指向线程创建后要调用的函数.这个函数也叫做线程函数
第四个参数指向我们要传递给线程函数的参数.
如果创建成功,该函数返回0,若不为0则返回失败.
如果想使某一个线程函数的执行次数为一次,而不管调用几次的话,需要使用pthread_once函数.
pthread_once(pthread_once_t *once, void*(start_routine))
第一个参数是pthread_once_t类型的指针变量,一般为PTHREAD_ONCE_INIT,也就是0.倘若初值不是0的话,pthread_once的行为就会不正常
调用此函数后,函数指针指向的函数不管被多少线程用pthread_once调用多少次只会执行一次.
终止线程
Linux下有两种方式可以让线程终止,第一种通过return从线程函数返回,第二种通过调用函数pthread_exit()使线程退出,pthread_exit在头文件pthread_.h中声明
void pthread_exit(void * retval)
参数是一个空类型的指针.保存退出时返回的值
为什么不能用exit呢,因为我们知道线程是依赖性的,使用exit会导致整个进程退出,所有的线程也就死亡了.当然这个函数需要注意的地方就是虽然其可以返回一个比如结构体之类的值,但是问题是这个值不能在线程函数结束之后其空间就被释放了.否则其返回的值就不正确.
如果主线程调用此函数,则主线程退出,其他线程不会终止.直到所有进程结束,进程才会结束.
线程终止的一个重要的需要考虑的问题是资源释放的问题,特别是临界资源,如果一个线程已经结束,但是却没有释放其占用的临界资源,如写文件,则其他线程如果也想使用这个临界资源,就会无限等待下去.
为了解决这个问题,Linux提供了一对函数,其是以函数形式定义的.如下:
#define pthread_cleanup_push(routine,arg) { struct _pthread_cleanup_buffer _buffer; _pthread_cleanup_push (&_buffer, (routine), (arg)); #define pthread_cleanup_pop(execute) _pthread_cleanup_pop (&_buffer, (execute)); }由两个大括号可以知道两个函数必须成对存在.当然现在的系统都十分的智能,如果你没有加入这两个函数,实际上也不会有影响,线程结束后,系统会自动释放临界资源.
线程终止还需要考虑同步的问题,所以就有了类似进程中的wait的函数,那就是pthread_join
pthread_join(pthread_t th, void * thread_return)
该函数第一个参数是指线程ID,第二个参数指线程函数调用时调用pthread_eixt中的参数.
此函数会阻塞主线程,等待指定线程的结束.它的要求是被等待的线程应处于join状态,也就是非DETACHED状态.处于DETACHED状态的线程无法由pthread_join同步
私有数据
私有数据对于线程分两种,第一种是在线程中定义的局部变量,因为我们知道线程的堆栈是私有的,所以这个他是不与别的线程分享的.
第二种是指全局变量对于特定的线程有不同的值,简单来说,比方说errno,如果没有私有数据,则当某个线程出错根据errno值打印的时候,打印的可能是别的线程出的错误的原因.所以要用一键多值技术,这个技术的原理如下:
系统为每个进程维护了一个称为key的结构的结构数组,一个元素有标志和析构函数之指针两个,其中一开始标志都是未被使用,对应每个线程,如果调用pthread_key_create,就会在数组中开始找第一个未被使用的元素,然后返回给线程的就是这个下标.也就是键位.
获得键位以后,在线程中可以设置与key相关联的多个数据,即函数pthread_setspecific(pthread_key_t *key, const void *pointer)第一个是key值,第二个参数是指向变量的指针。用pthread_setspecific为一个键指定新的线程数据时,线程必须先释放原有的线程数据用以回收空间。
通过函数pthread_getspecific函数可以得到与key关联的变量。
线程同步
线程最大的特点是资源的共享性,处理线程间的同步,最常用的有互斥锁,条件变量和异步信号三种技术。
互斥锁:
互斥对象是这样工作的。如果线程 a 试图锁定一个互斥对象,而此时线程 b 已锁定了同一个互斥对象时,线程 a 就将进入睡眠状态。一旦线程 b 释放了互斥对象(通过 pthread_mutex_unlock() 调用),线程 a 就能够锁定这个互斥对象(换句话说,线程 a 就将从 pthread_mutex_lock() 函数调用中返回,同时互斥对象被锁定)。同样地,当线程 a 正锁定互斥对象时,如果线程 c 试图锁定互斥对象的话,线程 c 也将临时进入睡眠状态。对已锁定的互斥对象上调用 pthread_mutex_lock() 的所有线程都将进入睡眠状态,这些睡眠的线程将“排队”访问这个互斥对象。
这其中涉及两个函数:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);
分别是上锁和解锁的函数,在互斥锁使用前必须进行初始化操作,对应的函数为:pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
其中第一个参数是锁,第二个参数是互斥锁的属性,如果为NULL的话则相当于默认属性。
也可以这样利用宏来初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
当一个互斥锁使用完毕后,需要对其进行清除,使用函数:int pthread_mutex_destroy(pthread_mutex_t mutex)
成功返回0,若锁处于锁定状态返回EBUSY
条件变量:
使用条件变量可以以原子方式阻塞线程,知道某个特定条件为真为止。条件变量始终与互斥锁一起使用,对条件的测试实在互斥锁的保护下进行的。
如果条件为假,线程通常会鲫鱼条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该县称可能会向相关的条件变量发出信号,从而使一个或多个线程执行唤醒,再次获得互斥锁和重新评估条件的过程。
对于这个过程需要以下几个函数:
pthread_cond_t cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);或者是pthread_cond_t cond = PTHREAD_COND_INITIALIZER
通过初始化函数或者宏来初始化条件变量
等待条件变量需要这个函数:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 等待条件的成立,这个函数解掉互斥锁,并让当前线程沉睡,直到受到信号
发送信号的函数:int pthread_cond_signal(pthread_cond_t * cond) 激活一个等待条件成立的线程。存在多个线程的时候,依据队列顺序来发送
int pthread_cond_broadcast(pthread_cond_t *cond)激活全部等待进程。
最后销毁条件变量:pthread_cond_destroy(pthread_cond_t *cond),当有线程在等待的时候,不能执行成功。
异步信号:
异步信号与条件变量类似,等待与接受信号