多线程系列篇章计划内容:
iOS多线程编程(一) 多线程基础
iOS多线程编程(二) Pthread
iOS多线程编程(三) NSThread
iOS多线程编程(四) GCD
iOS多线程编程(五) GCD的底层原理
iOS多线程编程(六) NSOperation
iOS多线程编程(七) 同步机制与锁
iOS多线程编程(八) RunLoop
Pthreads
是操作系统级的线程标准。
Pthreads
是 POSIX threads 的缩写,而 POSIX 是 Portable Operating System Interface (可移植操作系统接口)的缩写。
它定义了创建和操作线程的一套API。基于C语言实现,使用难度较大,需要手动管理线程生命周期。
Pthreads
API中的函数调用,全部是以pthread_
开头,并可以分为四类:
- 线程管理
例如创建线程、等待线程 (join)、查询线程状态等。 - 互斥锁(Mutex)
创建、销毁、锁定、解锁、设置属性等操作。 - 条件变量 (Condition Variable)
创建、销毁、等待、通知、设置与查询属性等操作。 - 同步管理
使用了互斥锁的线程间的同步
管理
POSIX 的
semaphore
API 可以和Pthreads
协同工作,但这并不是Pthreads
的标准。因而这部分API是以“sem_
”开头,而非“pthread_
”。
一、Pthreads的数据类型
1.1 pthread_t
线程句柄。用于表示线程ID。
出于移植目的,不能把它作为整数处理,也可能是一个Structure。在比较时,应使用函数pthread_equal()
对两个线程ID进行比较,获取自身所在线程ID时,使用pthread_self()
函数。
1.2 pthread_attr_t
线程属性。
主要包括scope属性、detach属性、堆栈地址、堆栈大小、优先级等。
1.3 pthread_barrier_t
同步屏障数据类型
1.4 pthread_mutex_t
mutex数据类型
1.5 pthread_cond_t
条件变量数据类型
二、 Pthreads的函数及使用
在使用函数前需导入头文件
#import
2.1 创建线程
创建线程使用pthread_create()
函数
pthread_create (pthread_t *restrict newthread,
const pthread_attr_t *restrict attr,
void *(*start_routine) (void *),
void *restrict arg);
参数说明:
参数1:线程句柄。当一个新的线程创建成功之后,就会通过这个参数将线程的句柄返回给调用者,以便对这个线程进行管理。
参数2:线程属性。用于设置线程的属性。这个参数是可选的,设置为NULL时,使用线程默认属性。
参数3:入口函数例程。线程的入口函数。如果线程创建成功,这个接口会返回0。
参数4:入口函数参数。作为入口函数的参数。这种设计可以在线程创建之前就帮他准备好一些专有数据,最典型的用法就是使用C++编程时的this指针。
示例: 创建线程
// 打印线程id
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid(); // 获取进程id
tid = pthread_self(); // 获取线程id
printf("%s pid:%lu, tid:%lu\n", s, (long unsigned)pid, (long unsigned)tid);
}
// 线程入口函数
void *threadRoutine (void *arg) {
printf( "This is a thread start routine and arg = %d.\n", *(int*)arg);
printids("new~~~");
* (int *)arg = 0; // 将传入的参数由10修改为0
return arg;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
//返回最后创建出来的Thread的Thread ID
pthread_t *restrict tidp;
//指定线程的attributes,可以用NULL使用默认属性
const pthread_attr_t *restrict attr = NULL;
int arg = 10; // 入口函数参数
int thread = pthread_create(&tidp, attr, threadRoutine, &arg);
if (thread != 0) { // 返回0表示创建成功,否则返回正整数
NSLog(@"create thread fail");
}
int *thread_ret = NULL;
pthread_join(tidp, (void **)& thread_ret); // 等待线程tidp的完成
printf( "thread_ret = %d.\n", *thread_ret );
printids("main~~~");
}
return 0;
}
输出结果为:
This is a thread start routine and arg = 10.
new~~~ pid:22258, tid:123145305821184
thread_ret = 0
main~~~ pid:22258, tid:4614421952
说明:
- ① 参数4可以作为线程与线程之间进行数据通讯的手段,如上例中主线程向新建的子线程中传入了参数10,在子线程可以获取该参数。需要注意的是,这个参数类型是void * ,这样做的目的是为了保证线程能够接受任意类型的参数,到时候再进行强制转换就好了。
- ②
pthread_join
,它的作用是阻塞当前线程,直到合并的线程执行完毕。第一个参数为线程句柄,第二个参数接受线程的返回值。在示例代码中主线程等待子线程执行完毕后才继续执行后面的代码。如果不添加此行代码,程序会先执行main~~, 再执行new~~。并且,线程线程入口函数中将入口函数参数修改为0并且return,所以打印的thread_ret 也为0。
2.2 线程的合并与分离
那么pthread_join()
接口做了什么呢?
2.2.1 线程的合并
首先要明确的一个问题就是什么是线程的合并。从前面的叙述中已经了解到,pthread_create()
接口负责创建了一个线程。那么线程也属于系统的资源,这跟内存没什么两样,而且线程本身也要占据一定的内存空间。
众所周知的一个问题就是C/C++编程中如果要通过malloc()
或new
分配了一块内存,就必须使用free()
或delete
来回收这块内存,否则就会产生著名的内存泄漏问题。
既然线程和内存没什么两样,那么有创建就必须得有回收,否则就会产生另外一个著名的资源泄漏问题,这同样也是一个严重的问题。那么线程的合并就是回收线程资源了。
线程的合并是一种主动回收线程资源的方案。当一个进程或线程调用了针对其它线程的pthread_join()
接口,就是线程合并了。这个接口会阻塞调用进程或线程,直到被合并的线程结束为止。当被合并线程结束,pthread_join()
接口就会回收这个线程的资源,并将这个线程的返回值返回给合并者。
2.2.2 线程的分离
与线程合并相对应的另外一种线程资源回收机制是线程分离,调用接口是pthread_detach()
。
线程分离是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。因为线程分离是启动系统的自动回收机制,那么程序也就无法获得被分离线程的返回值,这就使得pthread_detach()
接口只要拥有一个参数就行了,那就是被分离线程句柄。
线程合并和线程分离都是用于回收线程资源的,可以根据不同的业务场景酌情使用。不管有什么理由,你都必须选择其中一种,否则就会引发资源泄漏的问题,这个问题与内存泄漏同样可怕。
2.3 线程的属性
线程是有属性的,这个属性由一个线程属性对象来描述。线程属性对象由pthread_attr_init()
接口初始化,并由pthread_attr_destory()
来销毁,它们的完整定义是:
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destory(pthread_attr_t *attr);
线程拥有哪些属性呢?
一般地,Linux下的线程有:绑定属性、分离属性、调度属性、堆栈大小属性和满占警戒区大小属性。
2.3.1 绑定属性(scope)
说到这个绑定属性,就不得不提起另外一个概念:轻进程(Light Weight Process,简称LWP)。
轻进程和Linux系统的内核线程拥有相同的概念,属于内核的调度实体。一个轻进程可以控制一个或多个线程。
在计算机操作系统中,轻量级进程(LWP)是一种实现多任务的方法。与普通进程相比,LWP与其他进程共享所有(或大部分)它的逻辑地址空间和系统资源;与线程相比,LWP有它自己的进程标识符,并和其他进程有着父子关系;这是和类Unix操作系统的系统调用vfork()生成的进程一样的。另外,线程既可由应用程序管理,又可由内核管理,而LWP只能由内核管理并像普通进程一样被调度。Linux内核是支持LWP的典型例子。
默认情况下,对于一个拥有n个线程的程序,启动多少轻进程,由哪些轻进程来控制哪些线程由操作系统
来控制,这种状态被称为非绑定
的。那么绑定的含义就很好理解了,只要指定了某个线程“绑”在某个轻进程上,就可以称之为绑定
的。
被绑定的线程具有较高的响应速度,因为操作系统的调度主体是轻进程,绑定线程可以保证在需要的时候它总有一个轻进程可用。绑定属性就是干这个用的。
设置绑定属性的接口是pthread_attr_setscope()
,它的完整定义是:
int pthread_attr_setscope(pthread_attr_t *attr, int scope);
它有两个参数,第一个就是线程属性对象的指针,第二个就是绑定类型,拥有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面代码演示了这个属性的使用。
示例:设置线程绑定属性
……
int main( int argc, char *argv[] )
{
pthread_attr_t attr;
pthread_t th;
……
pthread_attr_init( &attr );
pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM );
pthread_create( &th, &attr, thread, NULL );
……
}
值得注意的是:
Linux的线程永远都是绑定的,所以PTHREAD_SCOPE_PROCESS在Linux中不管用,而且会返回ENOTSUP错误。如果只是在Linux下编写多线程程序,可以完全忽略这个属性。
2.3.2 分离属性(detach)
表示新线程是否与进程中其他线程脱离同步。
线程能够被合并和分离,分离属性就是让线程在创建之前就决定它应该是分离的。如果设置为PTHREAD_CREATE_DETACHED,就没有必要调用pthread_join()
或pthread_detach()
来回收线程资源了,在退出时自行释放所占用的资源。
设置分离属性的接口是pthread_attr_setdetachstate()
,它的完整定义是:
pthread_attr_setdetachstat(pthread_attr_t *attr, int detachstate);
它的第二个参数有两个取值:PTHREAD_CREATE_DETACHED(分离的)和PTHREAD_CREATE_JOINABLE(可合并的,也是默认属性)。下面代码演示了这个属性的使用。
示例:设置线程分离属性
……
int main( int argc, char *argv[] )
{
pthread_attr_t attr;
pthread_t th;
……
pthread_attr_init( &attr );
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create( &th, &attr, thread, NULL );
……
}
2.3.3 调度属性
线程的调度属性有三个,分别是:算法、优先级和继承权。
算法(schedpolicy)
Linux提供的线程调度算法有三个:轮询
、先进先出
和其它
。
其中轮询和先进先出调度算法是POSIX标准所规定,而其他则代表采用Linux自己认为更合适的调度算法,所以默认的调度算法也就是其它了。
轮询和先进先出调度算法都属于实时调度算法。
轮询指的是时间片轮转,当线程的时间片用完,系统将重新分配时间片,并将它放置在就绪队列尾部,这样可以保证具有相同优先级的轮询任务获得公平的CPU占用时间;
先进先出就是先到先服务,一旦线程占用了CPU则一直运行,直到有更高优先级的线程出现或自己放弃。
设置线程调度算法的接口是pthread_attr_setschedpolicy()
,它的完整定义是:
pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
它的第二个参数有三个取值:SCHED_RR(实时、轮转法)、SCHED_FIFO(实时、先入先出)和SCHED_OTHER(正常、非实时)。
优先级
Linux的线程优先级与进程的优先级不一样。
Linux的线程优先级是从1到99的数值,数值越大代表优先级越高。
而且要注意的是,只有采用SHCED_RR或SCHED_FIFO调度算法时,优先级才有效。对于采用SCHED_OTHER调度算法的线程,其优先级恒为0。
设置线程优先级的接口是pthread_attr_setschedparam()
,它的完整定义是:
struct sched_param {
int sched_priority;
}
int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param *param);
sched_param
结构体的sched_priority
字段就是线程的优先级了。
此外,即便采用SCHED_RR或SCHED_FIFO调度算法,线程优先级也不是随便就能设置的。首先,进程必须是以root账号运行的;其次,还需要放弃线程的继承权。什么是继承权呢?
继承权(inheritsched)
继承权就是当创建新的线程时,新线程要继承父线程(创建者线程)的调度属性。如果不希望新线程继承父线程的调度属性,就要放弃继承权。
设置线程继承权的接口是pthread_attr_setinheritsched()
,它的完整定义是:
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
它的第二个参数有两个取值:PTHREAD_EXPLICIT_SCHED(放弃继承权)和 PTHREAD_INHERIT_SCHED(拥有继承权)。前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。新线程在默认情况下是拥有继承权。
下面代码能够演示不同调度算法和不同优先级下各线程的行为,同时也展示如何修改线程的调度属性。
示例:设置线程调度属性
#include
#include
#include
#include
#define THREAD_COUNT 12
void show_thread_policy( int threadno )
{
int policy;
struct sched_param param;
pthread_getschedparam( pthread_self(), &policy, param );
switch( policy ){
case SCHED_OTHER:
printf( "SCHED_OTHER %d\n", threadno );
break;
case SCHED_RR:
printf( "SCHDE_RR %d\n", threadno );
break;
case SCHED_FIFO:
printf( "SCHED_FIFO %d\n", threadno );
break;
default:
printf( "UNKNOWN\n");
}
}
void* thread( void *arg )
{
int i, j;
long threadno = (long)arg;
printf( "thread %d start\n", threadno );
sleep(1);
show_thread_policy( threadno );
for( i = 0; i < 10; ++i ) {
for( j = 0; j < 100000000; ++j ){}
printf( "thread %d\n", threadno );
}
printf( "thread %d exit\n", threadno );
return NULL;
}
int main( int argc, char *argv[] )
{
long i;
pthread_attr_t attr[THREAD_COUNT];
pthread_t pth[THREAD_COUNT];
struct sched_param param;
for( i = 0; i < THREAD_COUNT; ++i )
pthread_attr_init( &attr[i] );
for( i = 0; i < THREAD_COUNT / 2; ++i ) {
param.sched_priority = 10;
pthread_attr_setschedpolicy( &attr[i], SCHED_FIFO );
pthread_attr_setschedparam( &attr[i], param );
pthread_attr_setinheritsched( &attr[i], PTHREAD_EXPLICIT_SCHED );
}
for( i = THREAD_COUNT / 2; i < THREAD_COUNT; ++i ) {
param.sched_priority = 20;
pthread_attr_setschedpolicy( &attr[i], SCHED_FIFO );
pthread_attr_setschedparam( &attr[i], param );
pthread_attr_setinheritsched( &attr[i], PTHREAD_EXPLICIT_SCHED );
}
for( i = 0; i < THREAD_COUNT; ++i )
pthread_create( &pth[i], &attr[i], thread, (void*)i );
for( i = 0; i < THREAD_COUNT; ++i )
pthread_join( pth[i], NULL );
for( i = 0; i < THREAD_COUNT; ++i )
pthread_attr_destroy( &attr[i] );
return 0;
}
2.3.4 堆栈大小属性
线程的主函数与程序的主函数main()
有一个很相似的特性,那就是可以拥有局部变量
。虽然同一个进程的线程之间是共享内存空间的,但是它的局部变量确并不共享。原因就是局部变量存储在堆栈中,而不同的线程拥有不同的堆栈。Linux系统为每个线程默认分配了8MB的堆栈空间,如果觉得这个空间不够用,可以通过修改线程的堆栈大小属性进行扩容。
修改线程堆栈大小属性的接口是pthread_attr_setstacksize()
,它的完整定义为:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
它的第二个参数就是堆栈大小了,以字节为单位。需要注意的是,线程堆栈不能小于16KB,而且尽量按4KB(32位系统)或2MB(64位系统)的整数倍分配,也就是内存页面大小的整数倍。此外,修改线程堆栈大小是有风险的,如果你不清楚你在做什么,最好别动它。
2.3.5 满栈警戒区属性
既然线程是有堆栈的,而且还有大小限制,那么就一定会出现将堆栈用满的情况。线程的堆栈用满是非常危险的事情,因为这可能会导致对内核空间的破坏,一旦被有心人士所利用,后果也不堪设想。为了防治这类事情的发生,Linux为线程堆栈设置了一个满栈警戒区
。这个区域一般就是一个页面,属于线程堆栈的一个扩展区域。一旦有代码访问了这个区域,就会发出SIGSEGV信号进行通知。
虽然满栈警戒区可以起到安全作用,但是也有弊病,就是会白白浪费掉内存空间,对于内存紧张的系统会使系统变得很慢。所有就有了关闭这个警戒区的需求。同时,如果我们修改了线程堆栈的大小,那么系统会认为我们会自己管理堆栈,也会将警戒区取消掉,如果有需要就要开启它。
修改满栈警戒区属性的接口是pthread_attr_setguardsize()
,它的完整定义为:
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
它的第二个参数就是警戒区大小,以字节为单位。与设置线程堆栈大小属性相仿,应该尽量按照4KB或2MB的整数倍来分配。当设置警戒区大小为0时,就关闭了这个警戒区。
虽然栈满警戒区需要浪费掉一点内存,但是能够极大的提高安全性,所以这点损失是值得的。而且一旦修改了线程堆栈的大小,一定要记得同时设置这个警戒区。
2.4 线程的终止
2.4.1 线程的终止
线程退出的条件,下面任一种都可以:
- 1.线程正常执行完毕后返回
- 2.调用
pthread_exit
函数退出
void pthread_exit(void * rval_ptr); // rval_ptr指向返回值
- 3.线程被同一进程的其他线程
pthread_cancel
void pthread_cancel(pthread_t tid)
pthrea_cancel
不等待线程终止,而是提出请求。该函数会使指定线程如同调用了pthread_exit(PTHREAD_CANCELLED)。不过,指定线程可以选择忽略或者进行自己的处理。此外,该函数不会导致Block,只是发送Cancel这个请求。
- 4.创建线程的进程
exec()
或exit()
- 5.
main()
先完成,且没有显示调用pthread_exit
。
如果没有显式地调用 pthread_exit()
, main()
就会在它产生的线程之前完成,那么所有线程都将终止。
显示调用 pthread_exit()
,则main()
会在结束前等待所有线程执行完毕。
示例:线程终止
// 打印线程id
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid:%lu, tid:%lu\n", s, (long unsigned)pid, (long unsigned)tid);
}
// 线程return
void *return_thread(void * arg)
{
printids("thread returning~~~");
return (void*)0;
}
// 线程exit
void *exit_thread(void * arg)
{
printids("thread exiting~~~");
pthread_exit((void*) 2); // 参数可以返回结构体,但是这个结构体必须返回后还能使用(不是在栈上分配)
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
int error;
pthread_t *restrict tidp1, tidp2;
void * rVal;
error = pthread_create(&tidp1, NULL, return_thread, NULL);
if (error != 0) {
NSLog(@"create thread - tidp1 fail");
}
error = pthread_create(&tidp2, NULL, exit_thread, NULL);
if (error != 0) {
NSLog(@"create thread - tidp2 fail");
}
pthread_join(tidp1, &rVal);
printf("return_thread return:%ld\n", (long)rVal);
pthread_join(tidp2, &rVal);
printf("exit_thread return:%ld\n", (long)rVal);
printids("main~~~");
}
return 0;
}
输出结果为:
thread returning~~~ pid:23857, tid:123145461620736
thread exiting~~~ pid:23857, tid:123145462157312
return_thread return:0
exit_thread return:2
main~~~ pid:23857, tid:4382375360
2.4.2 线程的清理
线程可以安排在它退出的时候,某些函数自动被调用,类似atexit()
函数。 需要调用如下函数:
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
这两个函数维护一个函数指针的Stack,可以把函数指针和函数参数值push/pop。执行的顺序则是从栈顶到栈底,也就是和push的顺序相反。
在下面情况下pthread_cleanup_push
所指定的thread cleanup handlers
会被调用:
- a.调用
pthread_exit
- b.响应
cancel
请求 - c.以非0参数调用
pthread_cleanup_pop()
。(如果以0调用pthread_cleanup_pop(),那么handler不会被调用,只是删除清理函数。
void *thread_func(void *arg)
{
pthread_cleanup_push(cleanup, “handler”)
// do something
Pthread_cleanup_pop(0);
return((void *)0);
}
2.5 线程本地存储
内线程之间可以共享内存地址空间,线程之间的数据交换可以非常快捷,这是线程最显著的优点。但是多个线程访问共享数据,需要昂贵的同步开销
,也容易造成与同步相关的BUG,更麻烦的是有些数据根本就不希望被共享。
C程序库中的errno是个最典型的一个例子。errno是一个全局变量,会保存最后一个系统调用的错误代码。在单线程环境并不会出现什么问题。但是在多线程环境,由于所有线程都会有可能修改errno,这就很难确定errno代表的到底是哪个系统调用的错误代码了。这就是有名的“非线程安全(Non Thread-Safe)”的。
此外,从现代技术角度看,在很多时候使用多线程的目的并不是为了对共享数据进行并行处理。更多是由于多核心CPU
技术的引入,为了充分利用CPU
资源而进行并行运算(不互相干扰)。换句话说,大多数情况下每个线程只会关心自己的数据而不需要与别人同步。
为了解决这些问题,可以有很多种方案。比如使用不同名称的全局变量。但是像errno这种名称已经固定了的全局变量就没办法了。在前面的内容中提到在线程堆栈中分配局部变量是不在线程间共享的。但是它有一个弊病,就是线程内部的其它函数很难访问到。
目前解决这个问题的简便易行的方案是线程本地存储,即Thread Local Storage,简称TLS。利用TLS,errno所反映的就是本线程内最后一个系统调用的错误代码了,也就是线程安全的了。
Linux提供了对TLS的完整支持,通过下面这些接口来实现:
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
int pthread_key_delete(pthread_key_t key);
void* pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);
-
pthread_key_create()
接口用于创建一个线程本地存储区。
- 第一个参数用来返回这个存储区的句柄,需要使用一个全局变量保存,以便所有线程都能访问到。
- 第二个参数是线程本地数据的回收函数指针,如果希望自己控制线程本地数据的生命周期,这个参数可以传递NULL。
-
pthread_key_delete()
接口用于回收线程本地存储区。其唯一的参数就要回收的存储区的句柄。 -
pthread_getspecific()
和pthread_setspecific()
这两个接口分别用于获取和设置线程本地存储区的数据。这两个接口在不同的线程下会有不同的结果(相同的线程下就会有相同的结果),这也就是线程本地存储的关键所在。
下面代码展示了如何在Linux使用线程本地存储,注意执行结果,分析一下线程本地存储的一些特性,以及内存回收的时机。
示例:使用线程本地存储
#include
#include
#include
#define THREAD_COUNT 10
pthread_key_t g_key;
typedef struct thread_data{
int thread_no;
} thread_data_t;
void show_thread_data()
{
thread_data_t *data = pthread_getspecific( g_key );
printf( "Thread %d \n", data->thread_no );
}
void* thread( void *arg )
{
thread_data_t *data = (thread_data_t *)arg;
printf( "Start thread %d\n", data->thread_no );
pthread_setspecific( g_key, data );
show_thread_data();
printf( "Thread %d exit\n", data->thread_no );
}
void free_thread_data( void *arg )
{
thread_data_t *data = (thread_data_t*)arg;
printf( "Free thread %d data\n", data->thread_no );
free( data );
}
int main( int argc, char *argv[] )
{
int i;
pthread_t pth[THREAD_COUNT];
thread_data_t *data = NULL;
pthread_key_create( &g_key, free_thread_data );
for( i = 0; i < THREAD_COUNT; ++i ) {
data = malloc( sizeof( thread_data_t ) );
data->thread_no = i;
pthread_create( &pth[i], NULL, thread, data );
}
for( i = 0; i < THREAD_COUNT; ++i )
pthread_join( pth[i], NULL );
pthread_key_delete( g_key );
return 0;
}
2.6 线程的同步
虽然线程本地存储可以避免线程访问共享数据,但是线程之间的大部分数据始终还是共享的。在涉及到对共享数据进行读写操作时,就必须使用同步机制,否则就会造成线程们哄抢共享数据的结果,这会把你的数据弄的七零八落理不清头绪。
Linux提供的线程同步机制主要有互斥锁
和条件变量
。
2.6.1 互斥锁
首先我们看一下互斥锁。所谓的互斥就是线程之间互相排斥,获得资源的线程排斥其它没有获得资源的线程。Linux使用互斥锁来实现这种机制。
既然叫锁,就有加锁和解锁的概念。当线程获得了加锁的资格,那么它将独享这个锁,其它线程一旦试图去碰触这个锁就立即被系统“拍晕”。当加锁的线程解开并放弃了这个锁之后,那些被“拍晕”的线程会被系统唤醒,然后继续去争抢这个锁。至于谁能抢到,只有天知道。但是总有一个能抢到。于是其它来凑热闹的线程又被系统给“拍晕”了……如此反复。
从互斥锁的这种行为看,线程加锁和解锁之间的代码相当于一个独木桥,同一时刻只有一个线程能执行。从全局上看,在这个地方,所有并行运行的线程都变成了排队运行了。比较专业的叫法是同步执行,这段代码区域叫临界区
。同步执行就破坏了线程并行性的初衷了,临界区越大破坏得越厉害。所以在实际应用中,应该尽量避免有临界区出现。实在不行,临界区也要尽量的小。如果连缩小临界区都做不到,那还使用多线程干嘛?
初始化和销毁互斥锁的接口是pthread_mutex_init()
和pthead_mutex_destroy()
,
对于加锁和解锁则有pthread_mutex_lock()
、pthread_mutex_trylock()
和pthread_mutex_unlock()
。
这些接口的完整定义如下:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destory(pthread_mutex_t *mutex );
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
从这些定义中可以看到,互斥锁也是有属性的。只不过这个属性在绝大多数情况下都不需要改动,所以使用默认的属性就行。方法就是给它传递NULL。
phtread_mutex_trylock()
比较特别,用它试图加锁的线程永远都不会被系统“拍晕”,只是通过返回EBUSY来告诉程序员这个锁已经有人用了。至于是否继续“强闯”临界区,则由程序员决定。系统提供这个接口的目的可不是让线程“强闯”临界区的。它的根本目的还是为了提高并行性,留着这个线程去干点其它有意义的事情。当然,如果很幸运恰巧这个时候还没有人拥有这把锁,那么自然也会取得临界区的使用权。
下面代码演示了在Linux下如何使用互斥锁。
示例: 使用互斥锁
#include
#include
#include
#include
#include
pthread_mutex_t g_mutex;
int g_lock_var = 0;
void* thread1( void *arg )
{
int i, ret;
time_t end_time;
end_time = time(NULL) + 10;
while( time(NULL) < end_time ) {
ret = pthread_mutex_trylock( &g_mutex );
if( EBUSY == ret ) {
printf( "thread1: the varible is locked by thread2.\n" );
} else {
printf( "thread1: lock the variable!\n" );
++g_lock_var;
pthread_mutex_unlock( &g_mutex );
}
sleep(1);
}
return NULL;
}
void* thread2( void *arg )
{
int i;
time_t end_time;
end_time = time(NULL) + 10;
while( time(NULL) < end_time ) {
pthread_mutex_lock( &g_mutex );
printf( "thread2: lock the variable!\n" );
++g_lock_var;
sleep(1);
pthread_mutex_unlock( &g_mutex );
}
return NULL;
}
int main( int argc, char *argv[] )
{
int i;
pthread_t pth1,pth2;
pthread_mutex_init( &g_mutex, NULL );
pthread_create( &pth1, NULL, thread1, NULL );
pthread_create( &pth2, NULL, thread2, NULL );
pthread_join( pth1, NULL );
pthread_join( pth2, NULL );
pthread_mutex_destroy( &g_mutex );
printf( "g_lock_var = %d\n", g_lock_var );
return 0;
}
最后需要补充一点,互斥锁在同一个线程内,没有互斥的特性。也就是说,线程不能利用互斥锁让系统将自己“拍晕”。解释这个现象的一个很好的理由就是,拥有锁的线程把自己“拍晕”了,谁还能再拥有这把锁呢?但是另外情况需要避免,就是两个线程已经各自拥有一把锁了,但是还想得到对方的锁,这个时候两个线程都会被“拍晕”。一旦这种情况发生,就谁都不能获得这个锁了,这种情况还有一个著名的名字——死锁
。死锁是永远都要避免的事情,因为这是严重损人不利己的行为。
2.6.2 条件变量
条件变量关键点在“变量”上。与锁的不同之处就是,当线程遇到这个“变量”,并不是类似锁那样的被系统给“拍晕”,而是根据“条件”来选择是否在那里等待。等待什么呢?等待允许通过的“信号”。这个“信号”是系统控制的吗?显然不是!它是由另外一个线程来控制的。
如果说互斥锁可以比作独木桥,那么条件变量这就好比是马路上的红绿灯。车辆遇到红绿灯肯定会根据“灯”的颜色来判断是否通行,那么谁来控制“灯”的颜色呢?一定是交警啊,至少你我都不敢动它(有人会说那是自动的,可是间隔多少时间变换也是交警设置不是?)。那么“车辆”和“交警”就是马路上的两类线程,大多数情况下都是“车”多“交警”少。
更深一步理解,条件变量是一种事件机制。由一类线程来控制“事件”的发生,另外一类线程等待“事件”的发生。为了实现这种机制,条件变量必须是共享于线程之间的全局变量。而且,条件变量
也需要与互斥锁同时使用
。
初始化和销毁条件变量的接口是pthread_cond_init()
和pthread_cond_destory()
;
控制“事件”发生的接口是pthread_cond_signal()
或pthread_cond_broadcast()
;
等待“事件”发生的接口是pthead_cond_wait()
或pthread_cond_timedwait()
。
它们的完整定义如下:
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destory(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex, const timespec *abstime);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
对于等待“事件”的接口从其名称中可以看出,一种是无限期等待
,一种是限时等待
。后者与互斥锁的pthread_mutex_trylock()
有些类似,即当等待的“事件”经过一段时间之后依然没有发生,那就去干点别的有意义的事情去。
而对于控制“事件”发生的接口则有“单播”和“广播”之说。所谓单播就是只有一个线程会得到“事件”已经发生了的“通知”,而广播就是所有线程都会得到“通知”。对于广播情况,所有被“通知”到的线程也要经过由互斥锁
控制的独木桥。
对于条件变量的使用,可以参考如下代码,它实现了一种生产者与消费者的线程同步方案。
示例 : 使用条件变量
#include
#include
#include
#define BUFFER_SIZE 5
pthread_mutex_t g_mutex;
pthread_cond_t g_cond;
typedef struct {
char buf[BUFFER_SIZE];
int count;
} buffer_t;
buffer_t g_share = {"", 0};
char g_ch = 'A';
void* producer( void *arg )
{
printf( "Producer starting.\n" );
while( g_ch != 'Z' ) {
pthread_mutex_lock( &g_mutex );
if( g_share.count < BUFFER_SIZE ) {
g_share.buf[g_share.count++] = g_ch++;
printf( "Prodcuer got char[%c]\n", g_ch - 1 );
if( BUFFER_SIZE == g_share.count ) {
printf( "Producer signaling full.\n" );
pthread_cond_signal( &g_cond );
}
}
pthread_mutex_unlock( &g_mutex );
}
printf( "Producer exit.\n" );
return NULL;
}
void* consumer( void *arg )
{
int i;
printf( "Consumer starting.\n" );
while( g_ch != 'Z' ) {
pthread_mutex_lock( &g_mutex );
printf( "Consumer waiting\n" );
pthread_cond_wait( &g_cond, &g_mutex );
printf( "Consumer writing buffer\n" );
for( i = 0; g_share.buf[i] && g_share.count; ++i ) {
putchar( g_share.buf[i] );
--g_share.count;
}
putchar('\n');
pthread_mutex_unlock( &g_mutex );
}
printf( "Consumer exit.\n" );
return NULL;
}
int main( int argc, char *argv[] )
{
pthread_t ppth, cpth;
pthread_mutex_init( &g_mutex, NULL );
pthread_cond_init( &g_cond, NULL );
pthread_create( &cpth, NULL, consumer, NULL );
pthread_create( &ppth, NULL, producer, NULL );
pthread_join( ppth, NULL );
pthread_join( cpth, NULL );
pthread_mutex_destroy( &g_mutex );
pthread_cond_destroy( &g_cond );
return 0;
}
这段代码存在一个潜在的问题:
如果producer线程并行执行的比consumer快,producer线程会先获取锁,之后向consumer发出信号,但此时consumer没办法获取锁,也就执行不到pthead_cond_wait()
处,那么程序就陷入尴尬的境地,发生死锁。
简单的,可以在pthread_create( &cpth, NULL, consumer, NULL )
; 和pthread_create( &ppth, NULL, producer, NULL )
; 之间加入一个长的延时函数usleep(100),确保consumer线程先行执行到pthead_cond_wait()
处。
从代码中会发现,等待“事件”发生的接口都需要传递一个互斥锁
给它。而实际上这个互斥锁还要在调用它们之前加锁
,调用之后解锁
。不单如此,在调用操作“事件”发生的接口之前也要加锁,调用之后解锁。这就面临一个问题,按照这种方式,等于“发生事件”和“等待事件”是互为临界区的。也就是说,如果“事件”还没有发生,那么有线程要等待这个“事件”就会阻止“事件”的发生。更干脆一点,就是这个“生产者”和“消费者”是在来回的走独木桥。但是实际的情况是,“消费者”在缓冲区满的时候会得到这个“事件”的“通知”,然后将字符逐个打印出来,并清理缓冲区。直到缓冲区的所有字符都被打印出来之后,“生产者”才开始继续工作。
为什么会有这样的结果呢?这就要说明一下pthread_cond_wait()
接口对互斥锁做什么。答案是:解锁
。pthread_cond_wait()
首先会解锁互斥锁
,然后进入等待
。这个时候“生产者”就能够进入临界区
,然后在条件满足的时候向“消费者”发出信号。
当pthead_cond_wait()
获得“通知”之后,它还要对互斥锁加锁
,这样可以防止“生产者”继续工作而“撑坏”缓冲区。另外,“生产者”在缓冲区不满的情况下才能工作的这个限定条件是很有必要的。因为在pthread_cond_wait()
获得通知之后,在没有对互斥锁加锁
之前,“生产者”可能已经重新进入临界区
了,这样“消费者”又被堵住了。也就是因为条件变量这种工作性质,导致它必须与互斥锁联合使用。
此外,利用条件变量和互斥锁,可以模拟出很多其它类型的线程同步机制,比如:event
、semaphore
等。
附:Pthreads常见函数释义
线程操作函数
pthread_create():创建一个线程
pthread_exit():终止当前线程
pthread_cancel():请求中断另外一个线程的运行。被请求中断的线程会继续运行,直至到达某个取消点。取消点是线程检查是否被取消并按照请求进行动作的一个位置。POSIX 的取消类型有两种,一种是
延迟取消
(PTHREAD_CANCEL_DEFERRED),这是系统默认的取消类型,即在线程到达取消点之前,不会出现真正的取消;另外一种是异步取消
(PHREAD_CANCEL_ASYNCHRONOUS),使用异步取消时,线程可以在任意时间取消。系统调用的取消点实际上是函数中取消类型被修改为异步取消至修改回延迟取消的时间段。几乎可以使线程挂起的库函数都会响应CANCEL信号,终止线程,包括sleep
、delay
等延时函数。pthread_join():阻塞当前的线程,直到另外一个线程运行结束
pthread_kill():向指定ID的线程发送一个[信号],如果线程不处理该信号,则按照信号默认的行为作用于整个进程。信号值0为保留信号,作用是根据函数的返回值判断线程是不是还活着。
pthread_cleanup_push():线程可以安排异常退出时需要调用的函数,这样的函数称为
线程清理程序
,线程可以建立多个清理程序。线程清理程序的入口地址使用栈保存,实行先进后处理
原则。由pthread_cancel
或pthread_exit
引起的线程结束,会次序执行由pthread_cleanup_push
压入的函数。线程函数执行return
语句返回不会引起线程清理程序被执行。pthread_cleanup_pop():以非0参数调用时,引起当前被弹出的线程清理程序执行。
pthread_setcancelstate():允许或禁止取消另外一个线程的运行。
pthread_setcanceltype():设置线程的取消类型为延迟取消或异步取消。
线程属性函数
pthread_attr_init():初始化线程属性变量。运行后,
pthread_attr_t
结构所包含的内容是操作系统支持的线程的所有属性的默认值。pthread_attr_setdetachstate():设置线程属性变量的
detachstate
属性(决定线程在终止时是否可以被joinable
)pthread_attr_getdetachstate():获取
detachstate
的属性pthread_attr_setscope():设置线程属性变量的
__scope
属性pthread_attr_setschedparam():设置线程属性变量的
schedparam
属性,即调用的优先级。pthread_attr_getschedparam():获取线程属性变量的
schedparam
属性,即调用的优先级。pthread_attr_destroy():删除线程的属性,用无效值覆盖
mutex函数:
pthread_mutex_init():初始化互斥锁
pthread_mutex_destroy():删除互斥锁
pthread_mutex_lock():占有互斥锁(阻塞操作)
pthread_mutex_trylock():试图占有互斥锁(不阻塞操作)。即,当互斥锁空闲时,将占有该锁;否则,立即返回。
pthread_mutex_unlock(): 释放互斥锁
pthread_mutexattr_(): 互斥锁属性相关的函数
条件变量函数
pthread_cond_init():初始化条件变量
pthread_cond_destroy():销毁条件变量
pthread_cond_signal(): 发送一个[信号]给正在当前条件变量的线程队列中处于阻塞等待状态的线程,使其脱离阻塞状态,唤醒后继续执行。如果没有线程处在阻塞等待状态,
pthread_cond_signal
也会成功返回。一般只给一个阻塞状态的线程发信号。假如有多个线程正在阻塞等待当前条件变量,则根据各等待线程优先级
的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间
的长短来确定哪个线程获得信号。但pthread_cond_signal
在多处理器上可能同时唤醒多个线程,当只能让一个被唤醒的线程处理某个任务时,其它被唤醒的线程就需要继续wait
。POSIX规范要求pthread_cond_signal
至少唤醒一个pthread_cond_wait
上的线程,有些实现为了简便,在单处理器上也会唤醒多个线程。所以最好对pthread_cond_wait()
使用while
循环对条件变量是否满足做条件判断。pthread_cond_wait(): 等待条件变量的特殊条件发生;
pthread_cond_wait()
必须与一个pthread_mutex
配套使用。该函数调用实际上依次做了3件事:对当前pthread_mutex解锁
、把当前线程挂起到当前条件变量的线程队列、被其它线程的信号唤醒后对当前pthread_mutex
申请加锁
。如果线程收到一个信号被唤醒,将被配套的互斥锁重新锁住,pthread_cond_wait()
函数将不返回直到线程获得配套的互斥锁。需要注意的是,一个条件变量不应该与多个互斥锁配套使用
。pthread_cond_broadcast(): 某些应用,如线程池,
pthread_cond_broadcast
唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait
.pthread_condattr_(): 条件变量属性相关的函数
线程私有存储(Thread-local storage):
pthread_key_create(): 分配用于标识进程中线程特定数据的
pthread_key_t
类型的键
pthread_key_delete(): 销毁现有线程特定数据
键
pthread_setspecific(): 为指定线程的特定数据
键
设置绑定的值
pthread_getspecific(): 获取调用线程的
键
绑定值
,并将该绑定存储
在 value 指向的位置中
同步屏障函数
pthread_barrier_init(): 同步屏障初始化
pthread_barrier_wait():
pthread_barrier_destory():
其它多线程同步函数:
- pthread_rwlock_*(): 读写锁
工具函数:
pthread_equal(): 对两个线程的线程标识号进行比较
pthread_detach(): 分离线程
pthread_self(): 查询线程自身线程标识号
pthread_once(): 某些需要仅执行一次的函数。其中第一个参数为
pthread_once_t
类型,是内部实现的互斥锁,保证在程序全局仅执行一次。
信号量函数,
包含在semaphore.h中:
sem_open:创建或者打开已有的命名信号量。可分为
二值信号量
与计数信号量
。命名信号量可以在进程间共享使用。sem_close:关闭一个信号灯,但没有将它从系统中删除。命名信号灯是随内核持续的,即使当前没有进程打开着某个信号灯,它的值仍然保持。
sem_unlink:从系统中删除信号灯。
sem_getvalue:返回所指定信号灯的当前值。如果该信号灯当前已上锁,那么返回值或为0,或为某个负数,其
绝对值
就是等待该信号灯解锁的线程数
。sem_wait:申请共享资源,所指定信号灯的值如果
大于0
,那就将它减1
并立即返回,就可以使用申请来的共享资源了。如果该值等于0
,调用线程就被进入睡眠
状态,直到该值变为大于0,这时再将它减1,函数随后返回。sem_wait
操作必须是原子操作
。sem_trywait:申请共享资源,当所指定信号灯的值
已经是0时
,后者并不
将调用线程投入睡眠
。相反,它返回一个EAGAIN错误。sem_post:释放共享资源。与
sem_wait
恰相反。sem_init:初始化非命名(内存)信号量
sem_destroy:销毁非命名信号量
共享内存函数
包含在sys/mman.h中,链接时使用rt库:
mmap:把一个文件或一个POSIX共享内存区对象
映射
到调用进程的地址空间
。使用该函数的目的: 1.使用普通文件以提供内存映射I/O 2.使用特殊文件以提供匿名内存映射。 3.使用shm_open
以提供无亲缘关系进程间的POSIX共享内存区。munmap: 删除一个映射关系
msync:
文件
与内存
同步函数shm_open:创建或打开共享内存区
shm_unlink:删除一个共享内存区对象的名字,删除一个名字仅仅防止后续的
open
,msq_open
或sem_open
调用取得成功。ftruncate:调整文件或共享内存区大小
fstat:来获取有关该对象的信息
文章参考:https://blog.csdn.net/jiajun2001/article/details/12624923
拓展阅读:
http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html
https://randu.org/tutorials/threads/