linux 多线程编程笔记
一, 线程基础知识
1,线程的概念
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行
中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
2,线程的优点
(1) 通过为每种事件类型的处理分配单独的线程,能够简化处理异步时间的代码。
(2) 多个线程可以自动共享相同的存储地址空间和文件描述符。
(3) 有些问题可以通过将其分解从而改善整个程序的吞吐量。
(4) 交互的程序可以通过使用多线程实现相应时间的改善,多线程可以把程序中处理用户输入输出的部分与其它部分分开。
3,线程的缺点
线程也有不足之处。编写多线程程序需要更全面更深入的思考。在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良
影响的可能性是很大的。调试一个多线程程序也比调试一个单线程程序困难得多。
4,线程的结构
线程包含了表示进程内执行环境必需的信息,其中包括进程中标识线程的线程ID,一组寄存器值、栈、调度优先级和策略、信号屏蔽子,errno变量以及线
程私有数据。进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本,程序的全局内存和堆内存、栈以及文件描述符。
进程ID在整个系统中是唯一的,但线程不同,线程ID只在它所属的进程环境中有效。
5,线程的创建
使用pthread_create函数。
#include<pthread.h>int pthread_create (pthread_t *__restrict tidp,//新创建的线程ID __const pthread_attr_t *__restrict __attr,//线程属性 void *(*__start_routine) (void *),//新创建的线程从start_routine开始执行 void *__restrict __arg)//执行函数的参数
新创建的线程从__start_routine函数的地址开始运行,该函数只有一个无类型指针参数arg,如果需要向__start_routine函数传递的参数不止一个,那么需要把这些参数放到
一个结构中,然后把这个结构的地址作为arg参数传入。返回值:成功-0,失败-返回错误编号,可以用strerror(errno)函数得到错误信息.
6,.线程的终止
线程是依进程而存在的,当进程终止时,线程也就终止了。当然也有在不终止整个进程的情况下停止它的控制流。
(1)线程只是从启动例程中返回,返回值是线程的退出码。
(2)线程可以被同一进程中的其他线程取消。
(3)线程调用pthread_exit.
7,终止线程, 怎样正确处理线程终止。
调用 pthread_exit
#include <pthread.h>void pthread_exit(void *rval_ptr);rval_prt是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以调用pthread_join函数访问到这个指针。
在 Linux 平台下,当处理线程结束时需要注意的一个问题就是如何让一个线程善始善终,让其所占资源得到正确释放。在 Linux 平台默认情况下,虽然各个线程之间是
相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用 pthread_join() 来获得另一
个线程的终止状态并且释放该线程所占的资源。
pthread_join:获得进程的终止状态
#include <pthread.h>int pthread_join(pthread_t thread,void **rval_ptr);
返回值:若成功返回0,否则返回错误编号。
当一个线程通过调用pthread_exit退出或者简单地从启动历程中返回时,进程中的其他线程可以通过调用pthread_join函数获得进程的退出状态。调用pthread_join进程
将一直阻塞,直到指定的线程调用pthread_exit,从启动例程中或者被取消。如果线程只是从它的启动历程返回,rval_ptr将包含返回码。(此时线程应该出现非分离状态)。
在默认情况下,线程的终止状态会保存到对该线程调用pthread_join(即默认情况下线程处于非分离状态),如果线程已经处于分离状态,线程的底层存储资源可以在线程终
止时立即被收回。当线程被分离时,并不能用pthread_join函数等待它的终止状态。对分离状态的线程进行pthread_join的调用会产生失败,返回EINVAL.
如果你压根儿不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而来让操作系统在该线程结束时来回收它所占的资源。将一个线程设置为
detached 状态可以通过两种方式来实现。
一种是调用 pthread_detach() 函数,可以将线程 设置为 detached 状态。
pthread_detach:使线程进入分离状态。
#include <pthread.h>int pthread_detach(pthread_t tid);返回值:若成功则返回0,否则返回错误编号。
另一种方法是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。方法如下代码:
pthread_t tid; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_create(&tid, &attr, THREAD_FUNCTION, arg); ... pthread_attr_destroy(&attr);
detached状态,否则就需要调用 pthread_join() 函数来对其进行资源回收。
好了,说这么多,举个例子先。
#include <stdio.h>#include <string.h>#include <stdlib.h>#include <pthread.h>#include <unistd.h>#include <errno.h>#define THREADS 2 // 线程个数int num = 0;void *run(void *arg)// 线程执行函数,线程0执行五次减,线程1执行五次加{ int flag = (int)arg; if(flag %2) // 根据线程参数执行相应动作 { for(int i = 0; i < 5; i++) { num++; printf("running in thread %d, num: %d\n", flag, num); } } else { for(int i = 0; i < 5; i++) { num--; printf("running in thread %d, num: %d\n", flag, num); } } return (void *)arg;}int main(){ int i,err; void * ret; pthread_t tid[THREADS]; for(i = 0; i < THREADS; i++) // 创建线程 { if(0 != (err = pthread_create(&tid[i], NULL, run, (void *)i))) { printf("thread create error %s\n",strerror(err)); exit(-1); } } for(i = 0; i < THREADS; i++)////阻塞等待线程,直到该线程退出 { if(0 != (err = pthread_join(tid[i], &ret))) { printf("thread join error %s\n",strerror(err)); exit(-1); } else { printf("thread %d exit code %d\n", i, (int)ret); // 打印线程退出代码 } } return 0;}
编译 此程序别忘了加上加上-lpthread参数。pthread库不是linux默认的库,所以在编译时候需要指明libpthread.a库。
g++ -o thread_test thread_test.c -lpthread
程序执行结果:
两个线程分别执行自减 5次 自加 5次最后结果为0,我们可以看到 thread 1最后输出为 0 结果是对的。但是仔细看所有的输出,你会发现有异样的东西。
thread 0 怎么输出连续2个 -1,thread1 怎么输出0然后直接 输出-3 。原因是两个线程可以同时操作 num 。 而且 num--(++),printf 不是一个原子操作 。
比如:当thread0 执行时 设此时num=-1 ,然后执行 num--, 此时 num的值变为 -2 而此时还没打印 printf num的值 转而线程thread执行了num++,此时
num的值又变回 -1,所以会出现打印连续两个 -1 。
当多个线程对共享区域进行修改时,应该采用同步的方式 才能达到我们有时候的需要,后面再叙述。
1,线程属性
线程具有属性,用pthread_attr_t表示,在对该结构进行处理之前必须进行初始化,在使用后需要对其去除初始化。我们用pthread_attr_init函数对其初始化,用
pthread_attr_destroy对其去除初始化。
线程属性结构如下:
typedef struct{ int detachstate; //线程的分离状态 int schedpolicy; //线程调度策略 struct sched_param schedparam; //线程的调度参数 int inheritsched; //线程的继承性 int scope; //线程的作用域 size_t guardsize; //线程栈末尾的警戒缓冲区大小 int stackaddr_set;//堆栈地址集 void * stackaddr; //线程栈的位置 size_t stacksize; //线程栈的大小}pthread_attr_t;
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性值不能直接设置,每个属性都对应一些函数对其查看或修改。初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。
线程属性修改函数介绍如下:
A,线程的分离状态
线程的分离状态决定一个线程以什么样的方式来终止自己。在默认情况下线程是非分离状态的,这种情况下,只有当pthread_join()函数返回时,
创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,
马上释放系统资源。
获取/修改线程的分离状态属性:
int pthread_attr_getdetachstate(const pthread_attr_t * attr,int *detachstate);int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate);
参数: attr 线程属性变量 detachstate 线程的分离状态属性 ,返回值: 若成功返回0,若失败返回-1。
可以使用pthread_attr_setdetachstate函数把线程属性detachstate设置为下面的两个合法值之一:
设置为PTHREAD_CREATE_DETACHED,以分离状态启动线程;或者设置为PTHREAD_CREATE_JOINABLE,正常启动线程。
可以使用pthread_attr_getdetachstate函数获取当前的datachstate线程属性。
B,线程的继承性
函数pthread_attr_setinheritsched和pthread_attr_getinheritsched分别用来设置和得到线程的继承性,这两个函数的定义如下:
#include <pthread.h>Int pthread_attr_getinheritsched(const pthread_attr_t *attr,int *inheritsched);int pthread_attr_setinheritsched(pthread_attr_t *attr,int inheritsched);
参数: attr 线程属性变量 inheritsched 线程的继承性 返回值:若成功返回0,若失败返回-1。
这两个函数具有两个参数,第1个是指向属性对象的指针,第2个是继承性或指向继承性的指针。继承性决定调度的参数是从创建的进程中继承还是使用在
schedpolicy和schedparam属性中显式设置的调度信息。Pthreads不为inheritsched指定默认值,因此如果你关心线程的调度策略和参数,必须先设置该属性。
继承性的可能值是PTHREAD_INHERIT_SCHED(表示新线程将继承创建线程的调度策略和参数)和PTHREAD_EXPLICIT_SCHED(表示使用在schedpolicy
和schedparam属性中显式设置的调度策略和参数)。如果你需要显式的设置一个线程的调度策略或参数,那么你必须在设置之前将inheritsched属性设置为 THREAD_EXPLICIT_SCHED.
C,线程的调度策略
函数pthread_attr_setschedpolicy和pthread_attr_getschedpolicy分别用来设置和得到线程的调度策略。
#include <pthread.h>int pthread_attr_getschedpolicy(const pthread_attr_t *attr,int *policy);int pthread_attr_setschedpolicy(pthread_attr_t *attr,int policy);
参数:attr 线程属性变量policy 调度策略返回值:若成功返回0,若失败返回-1。
这两个函数具有两个参数,第1个参数是指向属性对象的指针,第2个参数是调度策略或指向调度策略的指针。调度策略可能的值是先进先出(SCHED_FIFO)、
轮转法(SCHED_RR),或其它(SCHED_OTHER)。SCHED_FIFO策略允许一个线程运行直到有更高优先级的线程准备好,或者直到它自愿阻塞自己。
在SCHED_FIFO调度策略下,当有一个线程准备好时,除非有平等或更高优先级的线程已经在运行,否则它会很快开始执行。
SCHED_RR(轮循)策略是基本相同的,不同之处在于:如果有一个SCHED_RR策略的线程执行了超过一个固定的时期(时间片间隔)没有阻塞,而另外的SCHED_RR或SCHBD_FIPO策略的相同优先级的线程准备好时,运行的线程将被抢占以便准备好的线程可以执行。
当有SCHED_FIFO或SCHED_RR策赂的线程在一个条件变量上等持或等持加锁同一个互斥量时,它们将以优先级顺序被唤醒。即,如果一个低优先级的SCHED_FIFO
线程和一个高优先织的SCHED_FIFO线程都在等待锁相同的互斥且,则当互斥量被解锁时,高优先级线程将总是被首先解除阻塞。
D,线程的调度参数
函数pthread_attr_getschedparam 和pthread_attr_setschedparam分别用来设置和得到线程的调度参数。
#include <pthread.h>int pthread_attr_getschedparam(const pthread_attr_t *attr,struct sched_param *param);int pthread_attr_setschedparam(pthread_attr_t *attr,const struct sched_param *param);
参数: attr 线程属性变量 param sched_param结构 返回值: 若成功返回0,若失败返回-1。
这两个函数具有两个参数,第1个参数是指向属性对象的指针,第2个参数是sched_param结构或指向该结构的指针。结构sched_param在文件/usr/include /bits/sched.h中定义如下:
struct sched_param{ int sched_priority;};
结构sched_param的子成员sched_priority控制一个优先权值,大的优先权值对应高的优先权。系统支持的最大和最小优先权值可以用sched_get_priority_max函数和sched_get_priority_min函数分别得到。结构sched_param的子成员sched_priority控制一个优先权值,大的优先权值对应高的优先权。系统支持的最大和最小优先权值可以用sched_get_priority_max函数和sched_get_priority_min函数分别得到。
注意:如果不是编写实时程序,不建议修改线程的优先级。因为,调度策略是一件非常复杂的事情,如果不正确使用会导致程序错误,从而导致死锁等问题。如:在多线程应用程序中为线程设置不同的优先级别,有可能因为共享资源而导致优先级倒置。
#include <pthread.h>int sched_get_priority_max(int policy);int sched_get_priority_min(int policy);
参数:policy 系统支持的线程优先权的最大和最小值 返回值:若成功返回0,若失败返回-1。
下面是有关线程属性的程序例子:
可以和先前的那个进行相应的比较,发现是不是用互斥量后程序正常了!
应用互斥量需要注意的几点:
1、互斥量需要时间来加锁和解锁。锁住较少互斥量的程序通常运行得更快。所以,互斥量应该尽量少,够用即可,每个互斥量保护的区域应则尽量大。2、互斥量的本质是串行执行。如果很多线程需要领繁地加锁同一个互斥量,则线程的大部分时间就会在等待,这对性能是有害的。如果互斥量保护的
数据(或代码)包含彼此无关的片段,则可以特大的互斥量分解为几个小的互斥量来提高性能。这样,任意时刻需要小互斥量的线程减少,线程等待时间
就会减少。所以,互斥量应该足够多(到有意义的地步),每个互斥量保护的区域则应尽量的少。
2,条件变量(cond)
什么是条件变量?
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。
条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程
间的线程同步。
利用线程间共享的全局变量进行同步的一种机制。
可以看到,在当 #define 1 开启,即debug输出我们自定义的调试信息时,thread 0 刚开始满足条件输出A,num加1 解锁互斥量,此时无等待该条件的线程。此时num=1 ,id=0 进入while循环 显然thread 0进入等待状态,然后执行thread 1 即此时 id=1,而此时
num也为1 此时跳过while循环输出 B。此时num++, thread 1进入 waititng,然后解锁互斥量,最后唤醒等待该条件的线程thread 0。
输出 A ....
注释掉// #define DE 即可输出 :ABABABABABABABABABAB
3, 信号量
如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。
信号量函数的名字都以"sem_"打头。线程使用的基本信号量函数有四个。
#include <semaphore.h>
int sem_init (sem_t *sem , int pshared, unsigned int value);
这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux 只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE。
两个原子操作函数:
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
这两个函数都要用一个由sem_init调用初始化的信号量对象的指针做参数。
sem_post:给信号量的值加1;
sem_wait:给信号量减1;对一个值为0的信号量调用sem_wait,这个函数将会等待直到有其它线程使它不再是0为止。
int sem_destroy(sem_t *sem);
这个函数的作用是再我们用完信号量后都它进行清理。归还自己占有的一切资源。
条件变量与互斥锁、信号量的区别:
到这里,我们把posix的互斥锁、信号量、条件变量都接受完了,下面我们来比较一下他们。
a.互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行。一个线程可以等待某个给定信号灯,而另一个线程可以挂出该信号灯。
b.互斥锁要么锁住,要么被解开(二值状态,类型二值信号量)。
该条件变量上,那么该信号将丢失。
c.由于信号量有一个与之关联的状态(它的计数值),信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在
d.互斥锁是为了上锁而优化的,条件变量是为了等待而优化的,信号灯即可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。
待续...
参考资料:
<APUE>
<UNP,第二卷>
http://blog.csdn.net/lanyan822/article/details/7587972