Linux线程
作者:下家山
一:线程主函数
1.1 代码说明
1.2 运行结果
1.3 实例解析
首先,我们定义了在创建线程时所需要的一个新线程函数原型;如下所示:
Void *thread_function(void *arg);
根据pthread_create的要求,它只有一个指向void的指针作为参数,返回的也是指向void的指针。稍后,我们将介绍这个函数。
在main函数中,我们首先定义了几个变量,然后调用pthread_created开始运行新线程。如下所示:
pthread_t a_thread;
void *thread_result;
res = pthread_create(&a_thread, NULL, thread_function, (void *)message);
我们向pthread_create函数传递了一个pthread_t类型对象的地址,今后可以用它来引用这个新的线程。我们不想改变默认的线程属性,所以设置了第二个参数为NULL。最后两个参数分别为将要调用的函数和一个传递给该函数的参数。
如果这个调用成功了,就会有两个线程在运行。原先的线程main继续执行pthread_create后面的代码,而新线程开始执行thread_function函数。
原先的线程在查明新线程已经启动后,将调用pthread_join函数,如下所示:
Res = pthread_join(a_thread,&thread_result);
我们给该函数传递了两个参数,一个是正在等待其结束的线程的标识符,另一个是指向线程返回值的指针。这个函数将等到它所指定的线程终止后才返回。然后主线程将打印新线程的返回值和全局变量message的值,最后退出。
新线程在thread_function函数中开始执行,它先打印出自己的参数,休眠一会儿,然后更新全局变量,最后退出并向主线程返回一个字符串。新线程修改了数组message,而原先的线程也可以访问该数组。如果我们调用的是fork而不是pthread_create,就不会有这样的效果。
1.4 线程创建函数
#include <pthread.h>
Int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
1.4.1 函数参数:
这个函数定义看起来很复杂,其实用起来很简单。
第一个参数 :
是指向pthread_类型数据的指针。当线程创建时,这个指针指向的变量中将被写入一个标识符,我们用该标识符来引用新线程。
第二个参数:
用于设置线程的属性,我们一般不需要特殊的属性,所以只需要设置该参数为NULL。我们将在本章后面介绍如何使用这些属性。
第三个参数:
要创建的线程函数名。
Pthread_create函数难以理解,难就难在第三个参数的理解。
Void *(*start_routine)(void *)
这个形参告诉我们必须传递一个函数地址【*(start_routine)】,该函数以一个指向void的指针为参数【void *】,返回的也是一个指向void的指针【void *】。
因此,可以传递一个任意类型的参数并返回一个任意类型的指针。
用fork生成新进程时,父子进程将在同一位置继续执行下去,只是fork调用的返回值是不同的;但对于新线程来说,我们必须明确地提供给它一个函数指针,新线程将在这个新位置开始执行。
第四个参数:
传递给新创建线程的参数。
1.4.2 返回值
该函数调用成功时返回值是0,如果失败则返回错误代码。
Pthread_create和大多数pthread_系统函数一样,在失败时并未遵循UNIX函数的惯例返回-1,这种情况在unix函数中属于一少部分。所以,除非你很有把握,在对错误代码进行检查之前一定要仔细阅读使用手册中的相关内容。
1.5 线程退出函数
线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。
注意:绝不能用它来返回一个指向局部变量的指针,因为线程调用该函数后,这个局部变量就不存在了,这将引起严重的程序漏洞。
Pthread_exit函数定义如下:
#include <pthread.h>
Int pthread_exit(void *retval);
例程中返回的是一个字符串。而且这个字符串返回给了
1.6 线程等待函数
#include <pthread.h>
Pthread_join(pthread_t th, void **thread_return);阻塞当前的线程,直到另外一个线程运行结束
1.6.1 参数
第一个参数指定了将要等待的线程,线程通过pthread_create返回的标识符来指定。
第二个参数是一个指针,它指向另一个指针,而后者指向线程的返回值。
1.6.2 返回值
与pthread_create函数类似,pthread_exit函数调用成功返回0,失败返回错误代码。
八:线程属性
我们在1.4节讲创建线程函数的时候,其中的第二个参数,设置的都是NULL,如下图所示。
其实,我们可以控制的线程属性非常多。
8.1 线程属性的必要性
在前面的所有实例中,我们都在程序的退出之前用pthread_join对线程再次进行同步,如果我们需要所创建的线程返回数据(子线程向主线程返回数据),那么必须这么做。
但是,有时也会有这种情况,我们既不需要第二个线程向主线程返回信息,也不想让主线程等待它的结束。
我们可以创建这一类型的线程,它们称为脱离线程(detached thread)。可以通过修改线程属性实现。
8.2 线程属性初始化和销毁
#include <pthread.h>
Int pthread_attr_init( pthread_attr_t *attr);
这个函数的作用是初始化一个线程属性对象,成功返回0,失败返回错误代码。
Pthread_attr_t 是一个结构体:
Int pthread_attr_destroy( pthread_attr_t *attr);
这个函数的作用是,对属性对象进程清理和回收。一旦对象被收回了,除非它被重新初始化,否则就不能被再次使用了。
8.3 设置和获取分离属性
初始化一个线程属性对象后,我们可以调用许多其他的函数来设置不同的属性行为。
8.4 实例代码:
8.5 执行结果
8.6 实例解析
8.7 思考问题
1:去掉主线程中的sleep函数,发生什么现象,怎么解释?
2:把主线程的exit函数改成pthread_exit看看,发生什么现象,怎么解释。
二,两个线程协调工作
2.1 实例代码
2.2 运行结果
为什么执行结果要停顿那么久,然后一次输出出来?
2.3 实例解析
上面实例说明了两个线程是同时执行的(当然,在一单核系统中,线程的同时执行需要CPU在线程之间快速切换来实现)。在这个程序中我们使用了在两个线程之间的【轮询技术】,所以它的效率非常低。
在这里,我们要明白这个事实:即除了局部变量外,所有其他变量都将在一个进程中的所有线程之间共享。
这个例程是在上一节代码上修改过来的。我们定义了一个全局变量run_now并初始化为1.
Int run_now = 1;
我们计划在主线程(main)中把run_now置为2,在新线程thread_function中把run_now置为1.
在主线程中判断run_now,如果为1则打印一个单个字符“1”,并且改变run_now到2.如果,判断run_now不为1,那么就休息1秒钟在做检查。我们不断的检查run_now来等待它的值变为1,这种方式称为【忙等待】。
在新线程中,所做事情与主线程相似。只是把run_now的值颠倒了。
我们看到运行结果:两个线程很有规律的交替执行。121212121212121212
三:线程同步-信号量
在第二章,我们看到两个线程同时执行的情况,但我们采用的是轮询技术,这种技术是一种非常笨拙,效率低下的一种方法。
幸运的是Linux提供了专门访问代码临界区的方法——信号量。
它的作用相当于看守一段代码的看门人。
有两组接口函数用于信号量。一组取自于POSIX的实时扩展,用于线程。另一组被称为系统V信号量,常用于进程的同步。这两种接口函数很相似,但函数调用各不同,并不能互换。
信号量是一个特殊类型的变量,它可以被增加或减少,但对其访问必须是原子操作,即使在一个多线程程序中也是如此。这意味着如果一个程序中有两个(或更多)的线程试图改变一个信号量的值,系统将保证所有的操作都依次进行。但如果是普通变量,来自同一程序中的不同线程的冲突操作将导致不确定的结果。
信号量有两种:二进制信号量,只有0和1两种取值;
计数信号量,它可以有更大范围的取值。
信号量一般用来保护一段代码,使其每次只能被一个执行线程使用,要完成这个工作,就要使用二进制信号量。
3.1 信号量函数说明:
信号量函数名字都以sem_开头,而不像大多数线程函数以pthread_开头。线程中使用的基本信号量函数有4个:
#include <semaphore.h>
Int sem_init(sem_t *sem, int pshared, unsigned int value);
这个函数初始化由sem指向的信号量对象,设置它的共享选项,并给它一个初值。Pshared参数控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则,这个信号量就可以在多个进程之间共享。我们通常设置为0,
接下来的两个函数控制信号量的值,他们的定义如下:
#include <semaphore.h>
Int sem_wait(sem_t * sem);
Int sem_post(sem_t * sem);
这两个函数都以一个指针为参数,该指针所指向的对象是sem_init调用时初始化的信号量。
Sem_post函数的作用是以原子操作的方式给信号量的值加1.
所谓原子操作:是指如果两个线程企图同时给一个信号量加1,他们之间不会互相干扰,信号量的值总是会正确的加2,因为有两个线程试图改变它。
Sem_wait函数的作用是以原子操作的方式给信号量的值减1,但它会等待直到信号量有个非零值才会开始减法操作。因此,对值为2的信号量调用sem_wait,线程将继续执行,但是信号量的值会减1.如果对值为0的信号量调用sem_wait,这个函数就会等待,直到其他线程增加了该信号量的值,使其不再为0为止。
最后一个信号量函数是sem_destroy。这个函数的作用是,用完信号量后对它进行清理,定义如下:
#include <semaphore.h>
Int sem_destroy(sem_t * sem);
这个函数与其他几个函数一样,以一个信号量指针为参数,并清理该信号量拥有的所有资源。如果企图清理的信号量被一些线程等待,就会收到一个错误。
与大多Linux函数一样,这些函数在成功时都返回0。
3.2 实例代码
3.3 运行结果
3.4 实例解析
3.5 fgets函数说明:
Fgets通常与gets相提并论:
#include <stdio.h>
Char *fgets(char *s, int n, FILE *stream);
Char *gets(char *);
Fgets把读到的字符写到s所指向的字符串里面,直到出现下面某种情况:
1:遇到换行符;【它会把遇到的换行符也传递到接收字符串里面,还会再加上一个表示结尾的空字符\0】
2:已经传输了n-1个字符;【一次传递最多只能传递n-1个字符,因为它必须把空字符加进去以结束字符串】
3:到达了文件【stream】尾;
当成功调用,fgets返回一个指向字符串s的指针。如果文件流已经到达文件尾,fgets会设置这个文件流的EOF标识并返回一个空指针。如果出现错误,fgets返回一个空指针并设置errno以指出错误类型。
Gets函数类似于fgets,只不过它从标准输入读取数据并丢弃遇到的换行符。它在接受字符串的尾部加上一个null字节。
注意:Gets函数对传输字符的个数并没有限制,所以它可能会溢出自己的传输缓冲区。因此,你应该避免使用它,而用fgets来替代。许多安全问题都可以追溯到在程序中使用了可能造成各种缓冲区溢出的函数,gets就是典型的一个,所以要小心使用。
3.6 strlen和sizeof的区别
a) sizeof是一个操作符,strlen是库函数。
b) sizeof的参数可以是数据类型,也可以是变量,而strlen只能以结尾’\0’的字符串做参数。
c) 编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来,并且sizeof计算的是数据类型占内存的大小,而strlen计算的字符串实际的长度。
d) 数组做sizeof的参数不退化,传递给strlen就退化为指针了。
e) Sizeof和strlen的结果类型都是size_t.
3.7信号量的缺陷
3.7.1 解决方法
这个例子告诉我们,在多线程程序设计中,我们需要对时序考虑得非常仔细。为了解决上面程序中的问题,我们可以再加一个信号量,让主线程等到统计线程完成字符个数的统计后再继续执行。但更简单的一种方式是使用互斥量。
四:线程同步-互斥量
4.1 互斥量函数说明
互斥量允许程序员锁住某个对象,使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作后解锁它。
用于互斥量的基本函数和用于信号量的函数非常相似,他们的定义如下:
#include <pthread.h>
Int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * mutexattr);
Int pthread_mutex_lock(pthread_mutex *mutex);
Int pthread_mutex_unlock(pthread_mutex_t *mutex);
Int pthread_mutex_destroy(pthread_mutex *mutex);
与其他函数一样,成功时返回0,失败时将返回错误代码,但这些函数并不设置errno,你必须对函数的返回代码进行检查。
于信号量类似,这些函数的参数都是一个先前声明过的对象的指针。对互斥量来说,这个类型为pthread_mutex_t。Pthread_mutex_init函数中的属性参数允许我们设置互斥量的属性,而属性类型默认为fast。我们不改变它,传NULL进去。
4.2 实例代码
4.3 执行结果
4.4 实例解析
主【main】线程
注意:读线程和写线程的sleep(1);各位试着去掉,会出现什么问题。
4.5 有兴趣的可以看一下《线程---互斥量附件.doc》
八:线程函数大全
参考
http://zh.wikipedia.org/wiki/POSIX线程
五:线程标识
5.1 这两个函数的作用
线程引入 pthread_self 和 pthread_equal 原因 ——解决不同平台的问题!
1、引入pthread_equal的原因:
在线程中,线程ID的类型是pthread_t类型,由于在Linux下线程采用POSIX标准,所以,在不同的系统下,pthread_t的类型是不同的,比如在ubuntn下,是unsigned long类型,而在solaris系统中,是unsigned int类型。而在FreeBSD上才用的是结构体指针。 所以不能直接使用==判读,而应该使用pthread_equal来判断。
2、引入pthread_self的原因:
在使用pthread_create(pthread_t *thread_id,NULL,void* (*fun) (void *),void * args);虽然第一个参数中已经保存了线程ID,但是,前提是主线程首先执行时,才能实现的,而如果不是,那么thread指向一个未出划的变量。那么子线程想使用时,应该使用pthread_self();
5.2 实例代码
5.3 执行结果:
注意:各位怎么判断红框里面的值,是结构体指针,还是unsigned long,unsigned int
六:线程的主动终止和被动终止
6.1 主动取消
#include <pthread.h>
Int pthread_exit(void *retval);//主动取消,结束自己
6.2 被动取消
有时,我们想让一个线程可以要求另一个线程终止,就像给它发送一个信号一样,线程有方法可以做到。
#include <pthread.h>
Int pthread_cancel(pthread_t thread);//被动取消,被别人结束
这个函数的定义简单易懂,提供一个线程标识符,我们就可以发送请求来取消它。
6.3 取消状态
但在接受到请求的一端,事情会稍微复杂一点,不过也不是非常复杂,线程可以用pthread_setcancelstate设置自己的取消状态。
#include <pthread.h>
Int pthread_setcancelstate(int state, int *oldstate);
第一个参数的取值可以是PTHREAD_CANCEL_ENABLE【默认状态】,这个值允许线程接收取消请求;或者是PTHREAD_CANCEL_DISABLE,它的作用是忽略取消请求。
第二个参数,是一个指针,用户获取先前的取消状态。如果你对它没有兴趣,只需传递NULL给它。
6.4 取消类型
如果取消请求被接受了,线程就进入第二个控制层次,即取消类型。
#include <pthread.h>
Int pthread_setcanceltype(int type, int *oldtype);
Type参数可以有两种取值:
1>:PTHREAD_CANCEL_ASYNCHRONOUS,它将使得在接收到取消请求后立即采取行动;
2>:PTHREAD_CANCEL_DEFERRED【默认状态】,它将使得接收端收到取消请求后,一直等待,直到线程执行了下列函数之一后才采取行动【真正退出】。具体函数如下:
Pthread_join,pthread_cond_wait,pthread_cond_timewait,pthread_testcancel,sem_wait或sigwait。【也叫取消点】
练习:
写一个程序,让子线程一直打印*号,主控线程等待10秒后,删除子线程
6.5 实例代码
6.6 执行结果
6.7 实例解析
6.8 return 与pthread_exit;
感兴趣的可以参考《return_pthread_exit.doc》
七:线程清理和控制函数【选学内容】
7.1 这两个函数的作用
一般来说,Posix的线程终止有两种情况:
正常终止和非正常终止。
线程主动调用pthread_exit()或者从线程函数中return都将使线程正常退出,这是可预见的退出方式;
非正常终止是线程在其他线程的干预下【pthread_cancel】,或者由于自身运行出错(比如访问非法地址)而退出,这种退出方式是不可预见的。
不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,在不考虑因运行出错【比如访问非法地址】而退出的前提下,如何保证线程终止时能顺利的释放掉自己所占用的资源,特别是锁【pthread_mutex 即互斥量】资源,就是一个必须考虑解决的问题。
最经常出现的情形是资源独占锁的使用:线程为了访问临界资源而为其加上锁,但在访问过程中被外界取消,如果线程处于响应取消状态,且采用异步方式响应,或者在打开独占锁以前的运行路径上存在取消点,则该临界资源将永远处于锁定状态得不到释放。
我们回去看4.2节例程:
【外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程。】
7.2 如何使用这个函数
先看一个一般使用案例:
7.3.清理函数执行时机
1,显示的调用pthread_exit();
或
2,在cancel点线程被cancel。
或
3,pthread_cleanup_pop()的参数不为0时。
以上动作都限定在push/pop涵盖的代码内。
前面的2个比较好理解,关键是pthread_cleanup_pop参数问题,其实int那是因为c没有bool,这里的参数只有0与非0的区别,对pthread_cleanup_pop,参数是5和10都是一样的,都是非0。
我们经常会看到这样的代码:
void child(void *t)
{
pthread_cleanup_push(pthread_mutex_unlock,&mutex);
pthread_mutex_lock(&mutex);
..............
pthread_mutex_unlock(&mutex);
pthread_cleanup_pop(0);
}
为啥pthread_cleanup_pop是0呢,他根本就不会执行push进来的函数指针指向的函数,没错,是不执行,真要执行了就麻烦了。
那为啥还得写这句呢?
那是因为push和pop是必须成对出现的,不写就是语法错误。
这么写的目的主要是为了保证mutex一定可以被unlock,因为,在pthread_mutex_lock和 pthread_mutex_unlock之间可能发生任何事情,比如,存在N个cancel点导致线程被main或者其他线程给cancel,而 cancel动作是不会释放互斥锁的,这样就锁死啦。
通过pthread_cleanup_push加入一个函数pthread_mutex_unlock,参照上面执行时机的说明,在线程被cancel的时候,就可以作释放锁的清理动作。如果线程正常执行,知道运行到pthread_cleanup_pop,那么锁已经被中间代码里的 pthread_mutex_unlock给释放了,这时候如果pthread_cleanup_pop的参数不为0,则会再次释放,错就错在这里了,释放了两次。
所以,pthread_cleanup_pop(0)是必须的,因为,首先要成对出现,其次,我们不希望他真的执行到这里释放两次。
同样道理:
void *exit1(void *t)
{
printf("exit1\n");
}
void *child(void *t)
{
pthread_cleanup_push(exit1,NULL);
.....
pthread_exit(NULL);
pthread_cleanup_pop(0);
}
exit1函数是在pthread_exit(NULL)的时候执行的,pthread_cleanup_pop的参数是不是0没有关系,因为根本执行不到这里。
而换成这样:
pthread_cleanup_push(exit1,NULL);
......
pthread_cleanup_pop(0);
pthread_exit(NULL);
则exit1不会执行,因为pop的参数是0,如果把pop的参数修改为1则会执行exit1,那会执行两次吗?NO,因为pthread_exit在push/pop block的外面,他不会触发exit1.
pthread_cleanup_push(exit1,NULL);
pthread_cleanup_push(exit2,NULL);
........
pthread_cleanup_pop(0);
pthread_cleanup_pop(1);
那0和1分别控制的是谁?配对原则,从外到里一对一对的拔掉就可以了,显然,0控制的是exit2.
pthread_cleanup_push(void(*fun)(void* arg),void* arg);
pthread_cleanup_pop(int);
注意pthread_cleanup_pop(int)用法
如果pthread_cleanup_push 和pthread_cleanup_pop 包含的代码正常执行了,那么在遇到
pthread_cleanup_pop这句时,如果参数为0,将不会执行push进的函数,否则执行
7.4 注意事项
需要注意的问题有几点:
1,push与pop一定是成对出现的,其实push中包含"{"而pop中包含"}",少一个不行。
pthread_cleanup_push()/pthread_cleanup_pop()是以宏方式实现的,这是pthread.h中的宏定义:
#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)); }
可见,pthread_cleanup_push()带有一个"{",而pthread_cleanup_pop()带有一个"}",因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。
2,push可以有多个,同样的pop也要对应的数量,遵循"先进后出原则"。
3, 在线程宿主函数中主动调用return,如果return语句包含在pthread_cleanup_push()/pthread_cleanup_pop()对中,则不会引起清理函数的执行,反而会导致segment fault。
7.5 实例源码
7.6 执行结果
九:线程读写锁rwlock
读写锁比mutex有更高的适用性,可以多个线程同时占用读模式的读写锁,但是只能一个线程占用写模式的读写锁。
1. 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞;
2. 当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行枷锁的线程将阻塞;
3. 当读写锁在读模式锁状态时,如果有另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求长期阻塞;
这种锁适用对数据结构进行读的次数比写的次数多的情况下,因为可以进行读锁共享。【并发读】
API接口说明:
1) 初始化和销毁
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
成功则返回0, 出错则返回错误编号.
2) 读加锁和写加锁
获取锁的两个函数是阻塞操作
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
成功则返回0, 出错则返回错误编号.
3) 非阻塞获得读锁和写锁
非阻塞的获取锁操作, 如果可以获取则返回0, 否则返回错误的EBUSY.
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
成功则返回0, 出错则返回错误编号.