unix线程通信方式总结

来源:互联网 发布:蚂蚁花呗提现淘宝店铺 编辑:程序博客网 时间:2024/05/22 13:59

        首先来看看线程的介绍:

线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。在实际运行过程中,线程中还应该包含的信息有调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。线程和进程有很多类似的地方,人们习惯上把线程称为轻量级进程,这个所谓的轻量级是指线程并不拥有自己的系统资源,线程依附于创建自己的进程。我们可以从l两个个方面来理解线程的轻量级:
1. 调度
由于进程之间的线程共享同一个进程地址空间,因此在进程的线程之间做进程切换,并不会引起进程地址空间的切换,从而避免了昂贵的进程切换。当然不同进程组之间是需要进程切换的
2. 拥有资源
进程是操作系统中拥有资源的独立单位,在创建和撤销进程时,操作系统都会为进程分配和回收资源,资源包括地址空间,文件,IO,页表等。但是由于线程是依附与创建进程的,线程的代码段,数据段,打开文件,IO资源,地址空间,页表等都是和进程的所有线程共享的。
进程的创建使用fork函数,线程的创建使用pthread_create函数。

#include int pthread_create(pthread_t *restrict thread,const pthread_attr_t *restrict attr,void *(*start_routine)(void *),void *restrict arg);
该函数会在进程中创建一个新线程,函数的各参数描述如下:
第一个参数thread是一个指向pthread_t对象的指针,当线程成功创建后,新线程的ID会存储在thread指向的pthread_t对象中。
第二个参数attr,指定新建线程的属性。如果attr为NULL,则创建线程时使用默认的属性。线程创建完成之后仍可使用相关函数来改变线程的属性,
第三个参数start_routine是一个函数指针(即函数的首地址),它是新建线程的入口,新创建的线程从start_routine这个地址开始运行。从上面的定义来看,
该函数指针所指向的函数有两个特征:
1.函数返回值为 void * 类型
2.该函数有一个类型为 void * 类型的参数
第四个参数arg。start_routine所指向的函数有一个类型为 void * 的参数,即为这里的 void *arg
返回值:如果线程创建成功,则pthread_create函数返回0;否则返回一个非0值,该非0值是代表一个错误码来指示出错类型。
返回值的错误码有如下几种:
1.EAGAIN 不能获取(gain)到新线程所需的资源,或进程中的线程数量已达到了所允许的最大值(PTHREAD_THREADS_MAX)
2.EINVAL 创建时指定的属性attr无效(invalid)
3.EPERM 线程的创建者没有相应的权限

 在<unix环境高级编程》线程的同步方式有:

        (1).互斥量:互斥量的本质是一把锁,在访问共享资源对互斥量进行设置(加锁),在访问完成后对互斥量进行释放(解锁)。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁,如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对该互斥量加锁,其它进程就会看到互斥量依然是锁着的。只能回去再次等待它重新变为可用。以下互斥量的相关操作函数:

#include <pthread.h>  int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);  int pthread_mutex_lock(pthread_mutex_t *mutex);    int pthread_mutex_unlock(pthread_mutex_t *mutex);    int pthread_mutex_destroy(pthread_mutex_t *mutex); 

        (2).读写锁:读写锁与互斥量相似,不过读写锁允许更高的并行性。互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有3种状态,读模式下加锁,写模式下加锁,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式下的读写锁。

在一些程序中存在读者写者问题,也就是说,对某些资源的访问会  存在两种可能的情况,一种是访问必须是排它行的,就是独占的意思,这称作写操作;另一种情况就是访问方式可以是共享的,就是说可以有多个线程同时去访问某个资源,这种就称作读操作。这个问题模型是从对文件的读写操作中引申出来的。读写锁比起mutex具有更高的适用性,具有更高的并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁,读写锁的三种状态:
1.当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞
2.当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程将会被阻塞
3.当读写锁在读模式的锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁的请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求则长期阻塞。
                                                                                                                                                                                                        读写锁最适用于对数据结构的读操作次数多于写操作的场合,因为,读模式锁定时可以共享,而写模式锁定时只能某个线程独占资源,因而,读写锁也可以叫做个共享-独占锁。处理读者-写者问题的两种常见策略是强读者同步(strong reader synchronization)和强写者同步(strong writer synchronization).    在强读者同步中,总是给读者更高的优先权,只要写者当前没有进行写操作,读者就可以获得访问权限;而在强写者同步中,则往往将优先权交付给写者,而读者只能等到所有正在等待的或者是正在执行的写者结束以后才能执行。关于读者-写者模型中,由于读者往往会要求查看最新的信息记录,所以航班订票系统往往会使用强写者同步策略,而图书馆查阅系统则采用强读者同步策略。 读写锁机制是由posix提供的,如果写者没有持有读写锁,那么所有的读者多可以持有这把锁,而一旦有某个写者阻塞在上锁的时候,那么就由posix系统来决定是否允许读者获取该锁。

相关的系统函数如下:


以上两个函数是在定义读写锁之后对其进行初始化和清理工作,因为在释放读写锁占用内存之前,需要调pthread_wrlock_destroy进行清理,如果pthread_wrlock_init为读写锁分配了资源,pthread_rwlock_destory将释放这些资源,如果在调用pthread_rwlock_destroy之前就释放了读写锁占用的内存空间,那么分配给这些锁的资源就会丢掉!!!!!!!




带有超时的读写锁:


        (3).条件变量:条件变量给多个线程提供了一个回合场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。相关函数如下:






关于条件变量这里要仔细讲解一下:

pthread_cond_wait()的工作流程如下(以MAN中的EXAMPLE为例):
              int x,y;
              pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
              pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
       Waiting until x is greater than y is performed as follows:
              pthread_mutex_lock(&mut);
              while (x <= y) {
                      pthread_cond_wait(&cond, &mut);
              }
              /* operate on x and y */
              pthread_mutex_unlock(&mut);
       Modifications on x and y that may cause x to become greater than y should signal the con-
 dition if needed:
              pthread_mutex_lock(&mut);
              /* modify x and y */
              if (x > y) pthread_cond_singal(&cond);
              pthread_mutex_unlock(&mut);
     这个例子的意思是,两个线程要修改X和 Y的值,第一个线程当X<=Y时就挂起,直到X>Y时才继续执行(由第二个线程可能会修改X,Y的值,当X>Y时唤醒第一个线程),即首先初始化一个普通互斥量mut和一个条件变量cond。之后分别在两个线程中分别执行如下函数体:
              pthread_mutex_lock(&mut);
              while (x <= y) {
                      pthread_cond_wait(&cond, &mut);
              }
              /* operate on x and y */
              pthread_mutex_unlock(&mut);
和:       pthread_mutex_lock(&mut);
              /* modify x and y */
              if (x > y) pthread_cond_signal(&cond);
              pthread_mutex_unlock(&mut);
    其实函数的执行过程非常简单,在第一个线程执行到pthread_cond_wait(&cond,&mut)时,此时如果X<=Y,则此函数就将mut互斥量解锁,再将cond条件变量加锁 ,此时第一个线程挂起 (不占用任何CPU周期)。
    而在第二个线程中,本来因为mut被第一个线程锁住而阻塞,此时因为mut已经释放,所以可以获得锁mut,并且进行修改X和Y的值,在修改之后,一个IF语句判定是不是X>Y,如果是,则此时pthread_cond_signal()函数会唤醒第一个线程,并在下一句中释放互斥量mut。然后第一个线程开始pthread_cond_wait()执行,首先要再次锁mut如果锁成功,再进行条件的判断 (至于为什么用WHILE,即在被唤醒之后还要再判断,后面有原因分析),如果满足条件,则被唤醒进行处理,最后释放互斥量mut 。

引用下POSIX的RATIONALE:
1,pthread_cond_signal在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续 wait,while循环的意义就体现在这里了,而且规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程.
2,某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐此处使用while循环.
       其实说白了很简单,就是pthread_cond_signal()也可能唤醒多个线程,而如果你同时只允许一个线程访问的话,就必须要使用while来进行条件判断,以保证临界区内只有一个线程在处理。
     

(4).自旋锁:自旋锁与互斥量相似,但它并不是通过休眠使进程阻塞,而是在获得锁之前一直处于忙等状态(自旋阻塞)。




   自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处:自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。
两种锁的加锁原理
互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销(因为没有获得锁资源的进程会进入睡眠状态,需要内核将其唤醒,然后检测锁是否被释放)。
自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。
两种锁的区别:
互斥锁的起始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。
互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑:
1 临界区有IO操作
2 临界区代码复杂或者循环量大
3 临界区竞争非常激烈
4 单核处理器
至于自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张(因为调用自旋锁进程在等待锁的释放的时候一直处于死循环检测)的情况下。

        (5).屏障:屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待直到所有的合作线程都到达某一点,然后从该点继续执行。其中pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。



我们可以看到屏障的初始化函数pthread_barrier_init中有一个屏障计数器,当调用pthread_barrier_wait的线程在屏障计数未满足条件时,会进入休眠(sleep)状态,如果该线程是最后一个调用pthread_barrier_wait的线程,就满足了条件,所有的线程都会被唤醒.让我们来看一个例子:

#include "apue.h"#include <pthread.h>#include <limits.h>#include <sys/time.h>#define NTHR   8/* number of threads */#define NUMNUM 8000000L/* number of numbers to sort */#define TNUM   (NUMNUM/NTHR)/* number to sort per thread */long nums[NUMNUM];long snums[NUMNUM];pthread_barrier_t b;#ifdef SOLARIS#define heapsort qsort#elseextern int heapsort(void *, size_t, size_t, int (*)(const void *, const void *));#endifint complong(const void *arg1, const void *arg2){long l1 = *(long *)arg1;long l2 = *(long *)arg2;if (l1 == l2)return 0;else if (l1 < l2)return -1;elsereturn 1;}void * thr_fn(void *arg){//排序线程longidx = (long)arg;heapsort(&nums[idx], TNUM, sizeof(long), complong);pthread_barrier_wait(&b);return((void *)0);}void merge(){longidx[NTHR];longi, minidx, sidx, num;for (i = 0; i < NTHR; i++)idx[i] = i * TNUM;for (sidx = 0; sidx < NUMNUM; sidx++) {num = LONG_MAX;for (i = 0; i < NTHR; i++) {if ((idx[i] < (i+1)*TNUM) && (nums[idx[i]] < num)) {num = nums[idx[i]];minidx = i;}}snums[sidx] = nums[idx[minidx]];idx[minidx]++;}}int main(){unsigned longi;struct timevalstart, end;long longstartusec, endusec;doubleelapsed;interr;pthread_ttid;srandom(1);for (i = 0; i < NUMNUM; i++)nums[i] = random();gettimeofday(&start, NULL);pthread_barrier_init(&b, NULL, NTHR+1);for (i = 0; i < NTHR; i++) {err = pthread_create(&tid, NULL, thr_fn, (void *)(i * TNUM));if (err != 0)err_exit(err, "can't create thread");}pthread_barrier_wait(&b);merge();//k路归并排序gettimeofday(&end, NULL);/* Print the sorted list*/startusec = start.tv_sec * 1000000 + start.tv_usec;endusec = end.tv_sec * 1000000 + end.tv_usec;elapsed = (double)(endusec - startusec) / 1000000.0;printf("sort took %.4f seconds\n", elapsed);for (i = 0; i < NUMNUM; i++)printf("%ld\n", snums[i]);exit(0);}
上面的程序对8百万个数进行排序,一开始启动八个线程利用堆排序将8百万个数分成8份。屏障计数器的值设置为9,当执行pthread_barrier_wait(&b)的时候,所有线程启动并返回退出,然后利用k路归并排序进行处理。


0 0