Linux 的多线程编程的高效开发经验:https://www.ibm.com/developerworks/cn/linux/l-cn-mthreadps/
linux线程的实现:http://www.cnblogs.com/zhaoyl/p/3620204.html
线程概念经典解析:http://blog.chinaunix.net/uid-29613952-id-4214771.html
linux线程浅析:http://os.51cto.com/art/201408/448270.htm
http://blog.csdn.net/ctthuangcheng/article/details/8914712
Linux下多线程详解pdf文档下载:点击这里!
BOOST 线程完全攻略:http://www.cnblogs.com/renyuan/p/6613638.html
本文主要是关于Linux下多线程,关于boost 库的多线程暂未整理
1. 线程概念
什么是进程?
直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,也就是说:进程是资源分配的最小单位。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。线程是进程的一个实体,是执行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程 是CPU调度和分派的基本单位,它是比进程更小的能独立运行的 执行实体。在许多经典的操作系统教科书中, 总是把进程定义为程序的执行实例,它并不执行什么,只是维护应用程序所需的各种资源。 而线程则是真正的执行实体。 为了让进程完成一定的工作, 进程必须至少包含一个线程。
什么是线程?
线程存在与进程当中,是操作系统调度执行的最小单位。说通俗点,线程就是干活的。是CPU调度的最小单位。一个线程可以创建和撤销另一个线程。同一个进程中的多个线程之间可以并发执行。相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
Linux并不存在真正的线程,Linux的线程是使用进程模拟的。当我们需要在一个进程中同时运行多个执行流时,我们并不可以开辟多个进程执行我们的操作(32位机器里每个进程认为它 独享 4G的内存资源),此时便引入了线程,例如当我们既需要下载内容,又需要浏览网页时,此时多线程便起了作用。一个进程可拥有多个线程,它的执行力度比进程更加细致,线程资源共享。
linux内核并没有线程的概念。每一个执行实体都是一个task_struct结构, 通常称之为进程。进程是一个执行单元, 维护着执行相关的动态资源. 同时, 它又引用着程序所需的静态资源。通过系统调用clone创建子进程时, 可以有选择性地让子进程共享父进程所引用的资源.。这样的子进程通常称为轻量级进程.
但是, 一组线程并不仅仅是引用同一组资源就够了, 它们还必须被视为一个整体.
对此, POSIX标准提出了如下要求:
1.查看进程列表的时候, 相关的一组task_struct应当被展现为列表中的一个节点;
2.发送给这个"进程"的信号(对应kill系统调用), 将被对应的这一组task_struct所共享, 并且被其中的任意一个"线程"处理;
3.发送给某个"线程"的信号(对应pthread_kill), 将只被对应的一个task_struct接收, 并且由它自己来处理;
4.当"进程"被停止或继续时(对应SIGSTOP/SIGCONT信号), 对应的这一组task_struct状态将改变;
5. 当"进程"收到一个致命信号(比如由于段错误收到SIGSEGV信号), 对应的这一组task_struct将全部退出;
6.等等(以上可能不够全);
为什么引入线程?
为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?
多线程和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
多线程间方便通信。但是对不同进程来说,进程具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
线程特点?
由于同一进程的多个线程共享同一地址空间,所以代码段、数据段是共享的,如果定义一个函数(存储在代码段),则各线程都可以进行调用。如果定义个全局变量(存储在数据段),在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:
1.文件描述符表
2.每种信号的处理方式(SIG_IGN,SIG_DFL,用户自定义)
3.当前工作目录
4.用户id和组id
5.内存地址空间
但有些资源是线程独享的:
1.线程id
2.上下文,包括各种寄存器的值,程序计数器和栈指针
3.栈空间
4.errno变量
5.信号屏蔽字
6.调度优先级
进程所维护的是程序所包含的资源(静态资源), 如: 地址空间, 打开的文件句柄集, 文件系统状态, 信号处理handler等;
线程所维护的运行相关的资源(动态资源), 如: 运行栈, 调度相关的控制信息, 待处理的信号集等;
下面两副图展示了Linux下典型的进程环境和线程环境:
进程和线程的区别与联系?
如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。线程就是个无产阶级,但无产阶级干活,总得有自己的劳动工具吧,这个劳动工具就是栈,线程有自己的栈,这个栈仍然是使用进程的地址空间,只是这块空间被线程标记为了栈。每个线程都会有自己私有的栈,这个栈是不可以被其他线程所访问的。
其实进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
Linux下进程和线程的创建都是通过clone实现的. clone函数功能强大,带了众多参数,clone可以让你有选择性的继承父进程的资源,你可以选择想vfork一样和父进程共享一个虚存空间,从而创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是 兄弟关系。先有必要说下这个函数的结构
使用 man clone 可以查看 clone 函数的帮助信息。
int clone(int (*fn)(void *), void *child_stack,int flags, void *arg, .../* pid_t *ptid, void *newtls, pid_t *ctid */ );fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,
child_stack是为子进程分配系统堆栈空间(linux下堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的 值)。
flags就是标志用来描述你需要从父进程继承那些资源,
arg就是传给子进程的参数关于更过 clone 函数可以查看 man clone 。或者 网上查 clone函数资料。
下面的例子是创建一个线程(子进程共享了父进程虚存空间,没有自己独立的虚存空间不能称其为进程)。
父进程被挂起当子线程释放虚存资源后再继续执行。
#include <stdio.h>
#include <sched.h>
#include <signal.h> #define FIBER_STACK 8192 int a;
void * stack; int do_something()
{ printf("This is son, the pid is:%d, the a is: %d\n", getpid(), ++a); free(stack); exit(1);
} int main() { void * stack; a = 1; //为子进程申请系统堆栈 stack = malloc(FIBER_STACK); if(!stack) { printf("The stack failed\n"); exit(0); } printf("creating son thread!!!\n"); clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM|CLONE_VFORK, 0); printf("This is father, my pid is: %d, the a is: %d\n", getpid(), a); exit(1);
}
pthread_create是基于clone实现的, 创建出来的其实是进程, 但这些进程与父进程共享很多东西, 共享的东西都不用复制给子进程, 从而节省很多开销。因此,这些子进程(线程)又叫叫轻量级进程(light-weight process),简称LWP。 每个LWP都会与一个内核线程绑定, 这样它就可以作为独立的调度实体参与cpu竞争. 如下图所示:
察看LWP号:ps -Lf pid 或者 ps -eLf
LWP被pthread封装后, 以线程面目示人, 它有自己的id, 这里要区分 phtread_create 给LWP分配的id, 和操作系统给LWP分配的进程id. 这两者是不同的, 前者用于标志线程,可以暴露给用户, 后者其实就是进程的id, 它没有暴露出来, 必须通过系统调用来得到.
下面的例子演示了如何获取LWP的进程id
/* lwp.c */#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <linux/unistd.h> #define N 5 pid_t gettid()
{ //直接使用224代替__NR_gettid也可以 return syscall(__NR_gettid);
} void *thread_func(void *arg)
{ printf("thread started, pid = %d\n", gettid()); while (1) { sleep(1); }
} int main(void)
{ int i; pthread_t tid[N]; for (i = 0; i < N; i++) { pthread_create(&tid[i], NULL, thread_func, NULL); } sleep(1); for (i = 0; i < N; i++) { printf("tid = %lu\n", tid[i]); } while (1) { sleep(1); } return 0;
}
gcc lwp.c -o lwp -lpthread
此程序创建了5个线程, 但有6个LWP, 因为主线程也包含在里面, 主线程的线程id和进程id相同, 然后这6个线程的进程id都相同, 因为他们属于同一个进程.
关于内核态和用户态
核心态可以执行特权指令,但用户态只能执行非特权指令。这是如何实现的呢?
Linux将内核程序和用户程序分开处理,分别运行在用户态和核心态。以32位x86架构为例,虚拟空间共4G,高地址的1G为系统程序运行的核心栈,低地址的3G空间为用户程序运行的用户栈。Linux进程的4GB地址空间,3G-4G部分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。用户运行一个程序,该程序所创建的进程开始是运行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过write,send等系统调用,这些系统调用会调用内核中的代码来完成操作,这时,必须切换到Ring0,然后进入3GB-4GB中的内核地址空间去执行这些代码完成操作,完成后,切换回Ring3,回到用户态。这样,用户态的程序就不能 随意操作内核地址空间,具有一定的安全保护作用。
关于内核线程:ps命令列出来的线程, 如果被"[]"括起来了, 这就是内核线程
再论fork
fork后, 子进程复制哪些东西?
一句话总结,就是所有 writeable 的东西都会复制.包括:堆,栈, 数据段, 未初始化数据段, 打开的文件描述符,信号安装过的handler, 共享库, ipc(共享内存,消息队列,信号量)
注意:未决信号不会继承过来, 新进程会重置它的未决信号链。
子进程和父进程共享哪些东西?
一句话总结,就是所有 read-only 的东西都不用复制, 父子进程共享, 包括:正文段和字符串常量
哪些东西, 子进程不会从父进程继承?
进程id, 各种锁(内存锁,文件锁), 定时器, 未决信号
其实上面可以总结为:写(可写 writeable) 时 复制,读(只读 read-only) 时 共享。
2. 线程示例解析
linux系统支持POSIX多线程接口,称为pthread。编写linux下的多线程程序,需要包含头文件pthread.h,链接时需要使用 libpthread.so 动态库。
1.创建线程
在Linux下创建的线程的API接口是pthread_create(),它的完整定义是:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);参数:thread: 参数是一个指针,当线程成功创建时,返回创建线程ID。attr: 用于指定线程的属性start_routine: 该参数是一个函数指针,指向线程创建后要调用的函数。arg: 传递给线程函数的参数。
返回值:成功返回0,失败返回错误号。在一个线程中调pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行。
新的线程所执行的代码由传给pthread_create的函数指针start_routine决定。
创建线程简单示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>int * thread_handle(void *arg)
{printf("线程 ID: %d\n",pthread_self());return NULL;
}int main()
{pthread_t tid;printf("主线程 ID: %d \n",pthread_self());if(!pthread_create(&tid, NULL, (void *)thread_handle, NULL)){printf("create thread succeed\n");return 0;}else{printf("create thread fail");return -1;}
}
ldd 命令可以查看所依赖的库。
另一个示例:
#include <stdio.h>
#include <pthread.h>
void* thread( void *arg )
{ printf( "This is a thread and arg = %d.\n", *(int*)arg); *(int*)arg = 0; return arg;
}
int main( int argc, char *argv[] )
{ pthread_t th; int ret; int arg = 10; int *thread_ret = NULL; ret = pthread_create( &th, NULL, thread, &arg ); if( ret != 0 ){ printf( "Create thread error!\n"); return -1; } printf( "This is the main process.\n" ); pthread_join( th, (void**)&thread_ret ); printf( "thread_ret = %d.\n", *thread_ret ); return 0;
}
线程创建:
#include <stdio.h>
#include <pthread.h>struct WORK {pthread_t tid;void *(*dowork)(void *);void *arg;
};
struct STU {int id;char name[10];
};void *do1(void *arg)
{while (1) {printf("********do1\n");sleep(1);}
}void *do2(void *arg)
{int *p = (int *)arg;while (1) {printf("********do2 %d\n", (int)p);sleep(1);}
}void *do3(void *arg)
{struct STU *p = (struct STU *)arg;while (1) {printf("********do3 %d %s\n", p->id, p->name);sleep(1);}
}int main(void)
{struct STU a = {100, "itcast"};struct WORK job[3] = {{0, do1, NULL},{0, do2, (void *)5},{0, do3, (void *)&a},};int i;for (i = 0; i< 3; i++) {pthread_create(&job[i].tid, NULL, job[i].dowork, job[i].arg);}while (1) {printf("main thread.....\n");sleep(3);}return 0;
}
2.线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
第一种:通过 return 从线程函数返回,这种方法法对主线程不适用,从main函数 return 相当于调用 exit。
第二种:一个线程可以调用pthread_cancel终止同一进程中的另一个线程。pthread_cancel终止一个线程分同步和异步两种情况
第三种:线程可以调用 pthread_exit()函数 终止自己退出。注意:1. 如果主线程中如果从main函数返回或是调用了exit函数退出主线程,则整个进程终止,此时所有的其他线程也将终止。2. 如果主线程调用pthread_exit函数,则仅仅是主线程消亡,进程不会结束,其他线程也不会结束,至到所有的线程都结束时,进程才结束
void pthread_exit(void *retval) // 用于线程终止,传入一个id,调用exit()的话,主进程会终止。retval是void *类型,其它线程可以调pthread_join获得这个指针。
如果任意一个线程调用了exit 或_exit, 则整个进程的所有线程都终止,由于从main函数return也相当于调用exit。为了防止新创建的线程还没有得到执行就终止。可以使用下面的线程等待。
注意:pthread_exit 或者return 返回的指针所指向的内存单元必须是全局的或者是malloc分 配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
3.线程等待
线程等待是为了正确处理线程终止。
在 Linux 平台下,当处理线程结束时需要注意的一个问题就是如何让一个线程善始善终,让其所占资源得到正确释放。在 Linux 平台默认情况下,虽然各个线程之间是相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用 pthread_join() 来获得另一个线程的终止状态并且释放该线程所占的资源。
线程只能被一个线程等待终止(第一个能正常返回)。并且应处于join状态(非DETACHED)。
#include <pthread.h>void pthread_exit(void *retval);
int pthread_join(pthread_t thread,void **retval); // 成功返回0,失败返回错误号。 调用该函数的线程将被挂起,然后等待 thread 线程结束。线程的等待是以阻塞方式等待的,如果线程不等待,会就会产生内存泄露(类似进程的僵尸进程) 返回值:成功返回0, 失败返回错误号 调用该函数的线程将挂起等待,直到id 为thread 的线程终止。thread 线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下: 1.如果thread 线程通过return 返回,value_ptr所指向的单元存放的是thread线程函数的返回值。 2.如果thread 线程被别的线程调pthread_cancel异常终掉,value_ptr所指向的单元存放的是常数PTHREAD_CANCELED 。 3.如果thread 线程是调用pthread_exit 终止的,value_ptr所指向的单元存放的是传给pthread_exit 的参数。 如果对thread 线程的终止状态不感兴趣,可以传NULL给value_ptr参数。调用 pthread_join 函数的线程将被挂起,然后等待 thread 所表示的线程的结束。 retval 是指向线程 thread 返回值的指针。
需要注意的是 thread 所表示的线程必须是 joinable 的,即处于非 detached(游离)状态;并且只可以有唯一的一个线程对 thread 调用 pthread_join() 。
如果 thread 处于 detached 状态,那么对 thread 的 pthread_join() 调用将返回错误。
如果你压根儿不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而来让操作系统在该线程结束时来回收它所占的资源。
pthread_join使一个线程等待另一个线程结束。代码中如果没有pthread_join主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。所有线程都有一个线程号,也就是Thread ID。其类型为pthread_t。通过调用pthread_self()函数可以获得自身的线程号。
pthread_join 使用示例
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h> void thread(void)
{ int i; for(i = 0; i < 4; i++) printf("This is a pthread.\n"), sleep(1);
}
int main(void)
{ pthread_t id; int i, ret; ret = pthread_create(&id, NULL, (void*)thread, NULL); if(ret != 0) { printf("Create pthread error!\n"); exit(EXIT_FAILURE); } for(i = 0; i < 3; i++) printf("This is the main process.\n"), sleep(1); pthread_join(id, NULL); return 0;
}
示例
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>void* pthread_test(void* arg)
{int i=0;printf("this is first thread! pid:%d,tid: %u\n",(int)getpid(),(unsigned long)pthread_self());sleep(1);return (void*) 1;
}
void* pthread_test1(void* arg)
{int i=0;printf("this is secend thread! pid:%d,tid: %u\n",(int)getpid(),(unsigned long)pthread_self());pthread_exit((void*)2);
}
void* pthread_test2(void* arg)
{int i=0;printf("this is third thread! pid:%d,tid: %u\n",(int)getpid(),(unsigned long)pthread_self());sleep(3);return NULL;
}int main()
{pthread_t ptr1;pthread_t ptr2;pthread_t ptr3;void *tret;int pthread_ret=pthread_create(&ptr1,NULL,pthread_test,NULL);pthread_join(ptr1,&tret);printf("thread return!,thread id:%u,return code:%d\n",(unsigned long)ptr1,(int)tret);int pthread_ret1=pthread_create(&ptr2,NULL,pthread_test1,NULL);pthread_join(ptr2,&tret);printf("thread exit!,thread id:%u,return code:%d\n",(unsigned long)ptr2,(int)tret);int pthread_ret2=pthread_create(&ptr3,NULL,pthread_test2,NULL);sleep(3);pthread_cancel(ptr3);pthread_join(ptr3,&tret);printf("thread be canceled!,thread id:%u,return code:%d\n",(unsigned long)ptr3,(int)tret);if(pthread_ret!=0){printf("create pthread error!\n,info is:%s\n",strerror(pthread_ret));exit(pthread_ret);}printf("this is man thread pid:%d,tid: %u!\n",(int )getpid(),(unsigned long long)pthread_self());sleep(1);return 0;
}
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。 但是线程也可以被置为detach 状态,这样的线程一旦终就立刻回收它占有的所有资源,不保留终止状态。不能对一个已经处于detach 状态的线程调pthread_join,这样的调用将返回EINVAL。 对一个尚未detach 的线程调pthread_join或pthread_detach都可以把该线程置为detach 状态,也 就是说,不能对同一线程调用两次pthread_join,或者如果已经对一个线程调了pthread_detach就不能再调pthread_join了。
4.线程取消
线程取消:http://www.cnblogs.com/lijunamneg/archive/2013/01/25/2877211.html
一般情况下,线程在其主体函数退出的时候会自动终止,但同时也可以因为接收到另一个线程发来的终止(取消)请求而强制终止。取消后返回PTHREAD_CANCELED(它被定义为(void *)-1),取消调用函数pthread_cancel(id);
一个线程可以调用pthread_cancel终止同一进程中的另一个线程。pthread_cancel终止一个线程分同步和异步两种情况
线程取消的语义:
线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定。
线程接收到CANCEL信号的缺省处理(即pthread_create()创建线程的缺省状态)是继续运行至取消点,也就是说设置一个CANCELED状态,线程继续运行,只有运行至Cancelation-point的时候才会退出。
取消点定义:
根据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。但是pthread_cancel的手册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标.
即如下代码段:
pthread_testcancel();
retcode = read(fd, buffer, length);
pthread_testcancel();
注意:基于程序设计方面的考虑,如果线程处于无限循环中,且循环体内没有执行至取消点的必然路径,则线程无法由外部其他线程的取消请求而终止。因此在这样的循环体的必经路径上应该加入pthread_testcancel()调用.
程序设计方面的考虑:
如果线程处于无限循环中,且循环体内没有执行至取消点的必然路径,则线程无法由外部其他线程的取消请求而终止。因此在这样的循环体的必经路径上应该加入pthread_testcancel()调用。即如下代码段:
While(1)
{………pthread_testcancel();
}
与线程取消相关的pthread函数:
intpthread_cancel(pthread_t thread):线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他进程。
发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。注意pthread_cancel并不等待线程终止,它仅仅提出请求。
int pthread_setcancelstate(int state, int*oldstate):
设置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和 PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为 NULL则存入原来的Cancel状态以便恢复。
int pthread_setcanceltype(int type, int*oldtype)
设置本线程取消动作的执行时机,type由两种取值:PTHREAD_CANCEL_DEFFERED和 PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出);oldtype如果不为NULL则存入原来的取消动作类型值。
void pthread_testcancel(void)
检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回。
http://blog.csdn.net/shanzhizi
线程可以安排它退出时需要调用的函数,这与进程可以用atexit函数安排进程退出时需要调用的函数是类似的。线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说它们的执行顺序与它们注册时的顺序相反。
#include <pthread.h>
void pthread_cleanup_push(void(*rtn)(void*),void *arg);
void pthread_cleanup_pop(int execute);
当线程执行以下动作时调用清理函数,调用参数为arg,清理函数的调用顺序用pthread_cleanup_push来安排。
调用pthread_exit时
响应取消请求时
用非0的execute参数调用pthread_cleanup_pop时。
如果线程是通过从它的启动例程中返回而终止的话,那么它的清理处理程序就不会被调用,还要注意清理处理程序是按照与它们安装时相反的顺序被调用的。
示例代码:
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>pthread_t tid;
int i = 0;
void *do_thread(void * arg)
{//在系统调用的时候检测cancel标志while (1) {i++;pthread_testcancel(); //主动产生系统调用,检测cancel标志// printf("work thread\n");
//sleep(1);}fun();return (void *)5;
}int main(void)
{int arg = 10;int *resp;pthread_create(&tid, NULL, do_thread, (void *)&arg);sleep(3);pthread_cancel(tid);while (1) {printf("I am mainthread %d\n", i);sleep(2);}return 0; //exit(main)
}
5.线程分离
线程的分离状态决定一个线程以什么样的方式来终止自己。通常采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。即调pthread_join 后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此。例如,在Web服务器中当主线程为每个新来的连接请求创建一个子线程进行处理的时候,主线程并不希望因为调pthread_join阻塞(因为还要继续处理之后到来的连接请求)。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。在使用 Pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于 detached 状态,否着就需要调用 pthread_join() 函数来对其进行资源回收。
在任何一个时间点上线程是可结合的(joinable)或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。相反一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终时时由系统自动释放。
默认情况一个线程是可结合的,每一个可结合的线程都应该被显性的回收,既调用pthread_join()函数,分离调用函数pthread_detach。分离的这个函数是非阻塞的,可以立即返回。但为什么要分离呢?因为主进程在处理线程时,要处理不止一个,而每一个都需要被等待,然而join是阻塞式的,这样的话就只能处理一个线程,所以要加入分离,这样就可以处理多个线程。调用pthread_detach()后,这些子进程的状态会被设置为分离的,该线程运行结束会自动释放所有资源。
这时可以在子线程中加入
pthread_detach(pthread_self())
或者父线程调
pthread_detach(thread_id) (非阻塞,可立即返回)
这将该子线程的状态设置为分离的(detached),如此一来,该线程运结束后会自动释放所有资源。
int pthread_detach(pthread_t th);
将一个线程设置为 detached 状态可以通过两种方式来实现。
方法一:是调用 pthread_detach() 函数,可以将线程 thread 设置为 detached 状态。
方法二:是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,
最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。
创建 detach 线程:
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_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
总之为了在使用 Pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于 detached 状态,否着就需要调用 pthread_join() 函数来对其进行资源回收。
测试代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>void* thread1()
{pthread_detach(pthread_self());//分离后仍可被等待printf("pid is: %d, tid is: %d\n", getpid(),pthread_self());return (void*)1;
}
int main()
{pthread_t tid;void *ret;int err = pthread_create(&tid, NULL, thread1, NULL);if (err != 0){perror("pthread_create\n");return err;}//如果直接运行等待代码,一般会等待成功,返回1//如果在等待之前加入取消。等待错误,返回-1// pthread_cancel(tid);//线程可以自我取消也可以被取消,线程终止//调用pthread_exit(tid);和取消同样用法。int tmp = pthread_join(tid, &ret);if (tmp == 0){printf("wait success\n");}else{printf("wait failed\n");}printf(" pid is: %d, tid is: %d\n", getpid(),pthread_self());sleep(1);return 0;
}
子线程分离结束 父线程得到返回值
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>void* pthread_test(void* arg)
{pthread_detach(pthread_self());printf("%s\n",arg);return NULL;
}int main()
{pthread_t ptr1;
// void *tret;int pthread_ret=pthread_create(&ptr1,NULL,pthread_test,"chiled thread is run!");if(pthread_ret!=0){printf("create pthread error!\n,info is:%s\n",strerror(pthread_ret));exit(pthread_ret);}sleep(1);//等待一秒让系统执行子线程if(0==pthread_join(ptr1,NULL)){printf("detach success!\n");}elseprintf("detach failed!\n");//printf("this is man thread pid!!:%d,tid: %u!\n",(int )getpid(),(unsigned long long)pthread_self());return 0;
}
6. 线程私有数据
进程内的
所有线程
共享进程的数据空间,因此全局变量为所有线程所共有。但有时线程也需要保存自己的私有数据,这时可以创建线程私有数据(Thread-specific Date)TSD来解决。在线程内部,私有数据可以被各个函数访问,但对其他线程是屏蔽的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
线程私有数据采用了一键多值的技术,即一个键对应多个数值,访问数据时好像是对同一个变量进行访问,但其实是在访问不同的数据。
创建私有数据的函数有4个:pthread_key_create(创建), pthread_setspecific(设置), pthread_getspecific(获取), pthread_key_delete(删除)。
#include <pthread.h>
int pthread_key_creadte(pthread_key_t *key,void (*destr_fuction) (void *));
int pthread_setspecific(pthread_key_t key,const void * pointer));
void * pthread_getspecific(pthread_key_t key);
int pthread_key_delete(ptherad_key_t key);
7. 线程同步
线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。
基本pthread同步模型使用互斥量来保护共享数据、使用条件变量来通信,还可以使用其他的同步机制,如信号量、管道和消息队列。
互斥量允许线程在访问共享数据时锁定它,以避免其他线程的干扰。
条件变量允许线程等待共享数据到达某个期望的状态(例如队列非空或者资源可用)。
linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和异步信号。
互斥锁(mutex)
通过锁机制实现线程间的同步。同一时刻只允许一个线程执行一个关键部分的代码。
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex *mutex);
int pthread_mutex_destroy(pthread_mutex *mutex);
int pthread_mutex_unlock(pthread_mutex *mutex);
在对临界资源进行操作之前需要pthread_mutex_lock先加锁,操作完之后pthread_mutex_unlock再解锁。而且在这之前需要声明一个pthread_mutex_t类型的变量,用作前面两个函数的参数
互斥锁使用流程
- 先初始化锁init()或静态赋值pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIER
attr_t有:
PTHREAD_MUTEX_TIMED_NP:其余线程等待队列
PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁,允许线程多次加锁,不同线程,解锁后重新竞争
PTHREAD_MUTEX_ERRORCHECK_NP:检错,与一同,线程请求已用锁,返回EDEADLK;
PTHREAD_MUTEX_ADAPTIVE_NP:适应锁,解锁后重新竞争 - 加锁:lock、trylock。其中 lock 阻塞等待锁,trylock 是 lock 的非阻塞版本,立即返回EBUSY。即 trylock 不会阻塞当前线程,而是立即返回一个值来描述互斥锁的状况。然后继续执行下面的流程。
- 解锁:unlock,需满足是加锁状态,且由加锁线程解锁。
- 清除锁:destroy(此时锁必需unlock,否则返回EBUSY。//Linux下互斥锁不占用资源内存
互斥锁使用示例代码(mutex.c)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; //声明并定义锁
int a; // 全局变量int * thread_handle(void *arg)
{printf("线程 ID: %d\n", pthread_self());pthread_mutex_lock(&mutex); // 加锁a=10; // 操作全局变量printf("a changed to %d.\n",a); // 打印全局变量pthread_mutex_unlock(&mutex); // 释放锁return NULL;
}int main()
{pthread_t tid;printf("主线程 ID: %d \n",pthread_self());a=3;printf("主函数中 a = %d\n", a);if(!pthread_create(&tid, NULL, (void *)thread_handle, NULL)){printf("线程创建成功!\n");}else{printf("线程创建失败");return -1;}pthread_join(tid, NULL);pthread_mutex_destroy(&mutex);return 0;
}
另一示例代码:
#include <stdio.h>
#include <pthread.h>pthread_mutex_t Device_mutex ;
int count=0;void thread_func1()
{while(1){pthread_mutex_lock(&Device_mutex);printf("thread1: %d\n",count);pthread_mutex_unlock(&Device_mutex);count++;sleep(1);}
}void thread_func2()
{while(1){pthread_mutex_lock(&Device_mutex);printf("thread2: %d\n",count);pthread_mutex_unlock(&Device_mutex);count++;sleep(1);}
}int main()
{pthread_t thread1, thread2;pthread_mutex_init(&Device_mutex,NULL);if(pthread_create(&thread1,NULL,(void*)thread_func1,NULL) == -1){printf("create IP81 Thread error !\n");exit(1);}sleep(1);if(pthread_create(&thread2,NULL,(void *)thread_func2,NULL) == -1){printf("create IP81_2 Thread error!\n");exit(1);}sleep(1);pthread_join(thread1,NULL);pthread_join(thread2,NULL);pthread_mutex_destroy(&Device_mutex);return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int counter = 0;
pthread_mutex_t mutex;void *docounter(void *arg)
{int var, i;for (i = 0; i < 10000; i++) {pthread_mutex_lock(&mutex);var = counter;printf("%d\n", var+1);counter = var+1;pthread_mutex_unlock(&mutex);}return (void *)0;
}
int main(void)
{int i;pthread_t tid[2];pthread_mutex_init(&mutex, NULL);for (i = 0; i < 2; i++) {pthread_create(&tid[i], NULL, docounter, NULL);}pthread_join(tid[0], NULL);pthread_join(tid[1], NULL);pthread_mutex_destroy(&mutex);return 0;
}
读写锁
读写锁和互斥量(互斥锁)很类似,是另一种线程同步机制,但不属于POSIX标准,可以用来同步同一进程中的各个线程。当然如果一个读写锁存放在多个进程共享的某个内存区中,那么还可以用来进行进程间的同步,和互斥量不同的是:互斥量会把试图进入已保护的临界区的线程都阻塞;然而读写锁会视当前进入临界区的线程和请求进入临界区的线程的属性来判断是否允许线程进入。相对互斥量只有加锁和不加锁两种状态,读写锁有三种状态:读模式下的加锁,写模式下的加锁,不加锁。
读写锁 允许多个线程同时读,只能有一个线程同时写
读写锁的使用规则:
- 只要没有写模式下的加锁,任意线程都可以进行读模式下的加锁;
- 只有读写锁处于不加锁状态时,才能进行写模式下的加锁;
读写锁也称为 共享-独占(shared-exclusive)锁,当读写锁以读模式加锁时,它是以共享模式锁住,当以写模式加锁时,它是以独占模式锁住。读写锁非常适合读数据的频率远大于写数据的频率从的应用中。这样可以在任何时刻运行多个读线程并发的执行,给程序带来了更高的并发度。
读写锁的初始化和销毁
#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,失败-错误编号
上面两个函数分别由于读写锁的初始化和销毁。和互斥量,条件变量一样。
如果读写锁是静态分配的,可以通过常量进行初始化,如下:pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
也可以通过pthread_rwlock_init()进行初始化。对于动态分配的读写锁由于不能直接赋值进行初始化,只能通过这种方式进行初始化。pthread_rwlock_init()第二个参数是读写锁的属性,如果采用默认属性,可以传入空指针NULL。当不在需要使用时及释放(自动或者手动)读写锁占用的内存之前,需要调用pthread_rwlock_destroy()进行销毁读写锁占用的资源。
读写锁的属性设置
/* 初始化读写锁属性对象 */
int pthread_rwlockattr_init (pthread_rwlockattr_t *__attr);/* 销毁读写锁属性对象 */
int pthread_rwlockattr_destroy (pthread_rwlockattr_t *__attr);/* 获取读写锁属性对象在进程间共享与否的标识*/
int pthread_rwlockattr_getpshared (__const pthread_rwlockattr_t * __restrict __attr,int *__restrict __pshared);/* 设置读写锁属性对象,标识在进程间共享与否 */
int pthread_rwlockattr_setpshared (pthread_rwlockattr_t *__attr, int __pshared);返回值:成功返回0,否则返回错误代码
这个属性设置和互斥量的基本一样,具体可以参考互斥量的设置互斥量的属性设置
读写锁的使用
/* 读模式下加锁 */
int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock);/* 非阻塞的读模式下加锁 */
int pthread_rwlock_tryrdlock (pthread_rwlock_t *__rwlock);# ifdef __USE_XOPEN2K
/* 限时等待的读模式加锁 */
int pthread_rwlock_timedrdlock (pthread_rwlock_t *__restrict __rwlock,__const struct timespec *__restrict __abstime);
# endif/* 写模式下加锁 */
int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock);/* 非阻塞的写模式下加锁 */
int pthread_rwlock_trywrlock (pthread_rwlock_t *__rwlock);# ifdef __USE_XOPEN2K
/* 限时等待的写模式加锁 */
int pthread_rwlock_timedwrlock (pthread_rwlock_t *__restrict __rwlock,__const struct timespec *__restrict __abstime);
# endif/* 解锁 */
int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock);返回值:成功返回0,否则返回错误代码
加锁,这里分为读加锁和写加锁。
读加锁:
int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock)
用于以读模式即共享模式获取读写锁,如果读写锁已经被某个线程以读模式占用,那么调用线程就被阻塞。
在实现读写锁的时候可以对共享模式下锁的数量进行限制(目前不知如何限制)。pthread_rwlock_tryrdlock()和pthread_rwlock_rdlock()的唯一区别就是,在无法获取读写锁的时候,调用线程不会阻塞,会立即返回,并返回错误代码EBUSY。pthread_rwlock_timedrdlock()是限时等待读模式加锁,时间参数struct timespec * __restrict __abstime也是绝对时间,
和条件变量的pthread_cond_timedwait()使用基本一致,具体可以参考pthread_cond_timedwait() 3条件变量的使用写加锁:
int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock)
用于写模式即独占模式获取读写锁,如果读写锁已经被其他线程占用,不论是以共享模式还是独占模式占用,调用线程都会进入阻塞状态。pthread_rwlock_trywrlock()在无法获取读写锁的时候,调用线程不会进入睡眠,会立即返回,并返回错误代码EBUSY。pthread_rwlock_timedwrlock()是限时等待写模式加锁,也和条件变量的pthread_cond_timedwait()使用基本一致,具体可以参考pthread_cond_timedwait()3条件变量的使用。
解锁用同一个函数:
int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock)
示例代码:
#include <iostream>
#include <cstdlib>#include <unistd.h>
#include <pthread.h>using namespace std;struct{pthread_rwlock_t rwlock;int product;
}sharedData = {PTHREAD_RWLOCK_INITIALIZER, 0};void * produce(void *ptr)
{for (int i = 0; i < 5; ++i){pthread_rwlock_wrlock(&sharedData.rwlock);sharedData.product = i;pthread_rwlock_unlock(&sharedData.rwlock);sleep(1);}
}void * consume1(void *ptr)
{for (int i = 0; i < 5;){pthread_rwlock_rdlock(&sharedData.rwlock);cout<<"consume1:"<<sharedData.product<<endl;pthread_rwlock_unlock(&sharedData.rwlock);++i;sleep(1);}
}void * consume2(void *ptr)
{for (int i = 0; i < 5;){pthread_rwlock_rdlock(&sharedData.rwlock);cout<<"consume2:"<<sharedData.product<<endl;pthread_rwlock_unlock(&sharedData.rwlock);++i;sleep(1);}
}int main()
{pthread_t tid1, tid2, tid3;pthread_create(&tid1, NULL, produce, NULL);pthread_create(&tid2, NULL, consume1, NULL);pthread_create(&tid3, NULL, consume2, NULL);void *retVal;pthread_join(tid1, &retVal);pthread_join(tid2, &retVal);pthread_join(tid3, &retVal);return 0;
}
另一示例代码:
/*使用读写锁实现四个线程读写一段程序的实例。共创建了四个新的线程,其中两个线程用来读取数据,另外两个线程用来写入数据。在任意时刻,如果有一个线程在写数据,将阻塞所有其他线程的任何操作。
*/
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <bits/pthreadtypes.h>static pthread_rwlock_t rwlock;//读写锁对象#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int time_to_exit;void *thread_function_read_o(void *arg); //读线程1
void *thread_function_read_t(void *arg); //读线程2
void *thread_function_write_o(void *arg); //写线程1
void *thread_function_write_t(void *arg); //写线程2int main(int argc,char *argv[])
{int res;pthread_t a_thread, b_thread, c_thread, d_thread;void *thread_result;res=pthread_rwlock_init(&rwlock,NULL);//初始化读写锁if (res != 0){perror("rwlock initialization failed");exit(EXIT_FAILURE);}res = pthread_create(&a_thread, NULL, thread_function_read_o, NULL);//create new thread创建线程if (res != 0){perror("Thread creation failed");exit(EXIT_FAILURE);}res = pthread_create(&b_thread, NULL, thread_function_read_t, NULL);//create new threadif (res != 0){perror("Thread creation failed");exit(EXIT_FAILURE);}res = pthread_create(&c_thread, NULL, thread_function_write_o, NULL);//create new threadif (res != 0){perror("Thread creation failed");exit(EXIT_FAILURE);}res = pthread_create(&d_thread, NULL, thread_function_write_t, NULL);//create new threadif (res != 0){perror("Thread creation failed");exit(EXIT_FAILURE);}res = pthread_join(a_thread, &thread_result);//等待a_thread线程结束 if (res != 0){perror("Thread join failed");exit(EXIT_FAILURE);}res = pthread_join(b_thread, &thread_result); if (res != 0){perror("Thread join failed");exit(EXIT_FAILURE);}res = pthread_join(c_thread, &thread_result); if (res != 0){perror("Thread join failed");exit(EXIT_FAILURE);}res = pthread_join(d_thread, &thread_result); if (res != 0){perror("Thread join failed");exit(EXIT_FAILURE);}pthread_rwlock_destroy(&rwlock);//销毁读写锁 exit(EXIT_SUCCESS);
}void *thread_function_read_o(void *arg)
{printf("thread read one try to get lock\n"); pthread_rwlock_rdlock(&rwlock);//获取读取锁while(strncmp("end", work_area, 3) != 0){printf("this is thread read one.");printf("the characters is %s",work_area); pthread_rwlock_unlock(&rwlock); sleep(2);pthread_rwlock_rdlock(&rwlock); while (work_area[0] == '\0' ) {pthread_rwlock_unlock(&rwlock); sleep(2);pthread_rwlock_rdlock(&rwlock);}} pthread_rwlock_unlock(&rwlock); time_to_exit=1;pthread_exit(0);
}void *thread_function_read_t(void *arg)
{printf("thread read one try to get lock\n");pthread_rwlock_rdlock(&rwlock);while(strncmp("end", work_area, 3) != 0){printf("this is thread read two.");printf("the characters is %s",work_area); pthread_rwlock_unlock(&rwlock); sleep(5);pthread_rwlock_rdlock(&rwlock); while (work_area[0] == '\0' ) { pthread_rwlock_unlock(&rwlock); sleep(5);pthread_rwlock_rdlock(&rwlock); }}pthread_rwlock_unlock(&rwlock); time_to_exit=1;pthread_exit(0);
}void *thread_function_write_o(void *arg)
{printf("this is write thread one try to get lock\n");while(!time_to_exit){pthread_rwlock_wrlock(&rwlock);printf("this is write thread one.\nInput some text. Enter 'end' to finish\n");fgets(work_area, WORK_SIZE, stdin);pthread_rwlock_unlock(&rwlock);sleep(15);}pthread_rwlock_unlock(&rwlock);pthread_exit(0);
}void *thread_function_write_t(void *arg)
{sleep(10);while(!time_to_exit){pthread_rwlock_wrlock(&rwlock);//获取写入锁printf("this is write thread two.\nInput some text. Enter 'end' to finish\n");fgets(work_area, WORK_SIZE, stdin);//写入pthread_rwlock_unlock(&rwlock);//解锁sleep(20);}pthread_rwlock_unlock(&rwlock);//解锁pthread_exit(0);
}
3个线程不定时写同一全局资源,5个线程不定时读同一全局资源
#include <stdio.h>
#include <pthread.h>int counter;
pthread_rwlock_t rwlock;//3个线程不定时写同一全局资源,5个线程不定时读同一全局资源
void *th_write(void *arg)
{int t;while (1) {pthread_rwlock_wrlock(&rwlock);t = counter;usleep(100);printf("write %x : counter=%d ++counter=%d\n", (int)pthread_self(), t, ++counter);pthread_rwlock_unlock(&rwlock);usleep(10000);}
}
void *th_read(void *arg)
{while (1) {pthread_rwlock_rdlock(&rwlock);printf("**********************************read %x : %d\n", (int)pthread_self(), counter);pthread_rwlock_unlock(&rwlock);usleep(100);}
}
int main(void)
{int i;pthread_t tid[8];pthread_rwlock_init(&rwlock, NULL);for (i = 0; i < 3; i++)pthread_create(&tid[i], NULL, th_write, NULL);for (i = 0; i < 5; i++)pthread_create(&tid[i+3], NULL, th_read, NULL);for (i = 0; i < 8; i++)pthread_join(tid[i], NULL);pthread_rwlock_destroy(&rwlock);return 0;
}
条件变量(cond)
一定要在mutex的锁定区域内使用。
利用线程间共享的全局变量进行同步的一种机制。使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。条件变量始终与互斥锁一起使用。对条件的测试是在互斥锁(互斥)的保护下进行的。如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而使一个或多个等待的线程执行以下操作:唤醒、再次获取互斥锁、重新评估条件。
在以下情况下,条件变量可用于在进程之间同步线程:线程是在可以写入的内存中分配的、内存由协作进程共享。
条件变量的相关函数如下:
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒等待该条件的某个线程
int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有线程的阻塞
pthread_cond_wait用于等待某个特定的条件为真,pthread_cond_signal用于通知阻塞的线程某个特定的条件为真了。在调用者两个函数之前需要声明一个pthread_cond_t类型的变量,用于这两个函数的参数。
为什么条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的呢?因为“某个特性条件”通常是在多个线程之间共享的某个变量。互斥锁允许这个变量可以在不同的线程中设置和检测。
通常,pthread_cond_wait只是唤醒等待某个条件变量的一个线程。如果需要唤醒所有等待某个条件变量的线程,需要调用:
int pthread_cond_broadcast (pthread_cond_t * cptr);
默认情况下面,阻塞的线程会一直等待,知道某个条件变量为真。如果想设置最大的阻塞时间可以调用:
int pthread_cond_timedwait (pthread_cond_t * cptr, pthread_mutex_t *mptr, const struct timespec *abstime);
如果时间到了,条件变量还没有为真,仍然返回,返回值为ETIME。
条件变量使用流程
- 初始化.init()或者pthread_cond_t cond=PTHREAD_COND_INITIALIER;属性置为NULL
- 等待条件成立.pthread_wait,pthread_timewait.wait()释放锁,并阻塞等待条件变量为真。
timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait) - 激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)
- 清除条件变量:destroy;无线程等待,否则返回EBUSY
对于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 struct timespec *abstime);
一定要在mutex的锁定区域内使用。
如果要正确的使用pthread_mutex_lock与pthread_mutex_unlock,请参考
pthread_cleanup_push和pthread_cleanup_pop宏,它能够在线程被cancel的时候正确的释放mutex!
另外,posix1标准说,pthread_cond_signal与pthread_cond_broadcast无需考虑调用线程是否是mutex的拥有者,也就是所,可以在lock与unlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用吧。
pthread_cleanup_push()/pthread_cleanup_pop()的详解:http://blog.csdn.net/caianye/article/details/5912172
/* cond.c */#include <stdio.h>
#include <pthread.h>pthread_mutex_t mutex;
pthread_cond_t cond;void *thread_handle_1(void *arg)
{pthread_cleanup_push(pthread_mutex_unlock,&mutex);//提供函数回调保护while(1){printf("thread_handle_1 is running\n");pthread_mutex_lock(&mutex);pthread_cond_wait(&cond,&mutex);printf("thread_handle_1 applied the condition\n");pthread_mutex_unlock(&mutex);sleep(4);}pthread_cleanup_pop(0);
}void *thread_handle_2(void *arg)
{while(1){printf("thread_handle_2 is running\n");pthread_mutex_lock(&mutex);pthread_cond_wait(&cond,&mutex);printf("thread_handle_2 applied the condition\n");pthread_mutex_unlock(&mutex);sleep(1);}
}
int main()
{pthread_t thid1,thid2;printf("condition variable study!\n");pthread_mutex_init(&mutex,NULL);pthread_cond_init(&cond,NULL);pthread_create(&thid1,NULL,(void*)thread_handle_1,NULL);pthread_create(&thid2,NULL,(void*)thread_handle_2,NULL);do{pthread_cond_signal(&cond);}while(1);sleep(20);pthread_exit(0);return 0;
}
gcc cond.c -lpthread -o cond
为什么有了互斥锁还要条件变量(二者的不同)
首先的明白 pthread_cond_wait 的执行过程原理:当代码执行到 pthread_cond_wait 时,pthread_cond_wait 会释放mutex,然后等待条件变为真返回。当返回时会再次锁。这是一个原子操作。也就是说,当执行到 pthread_cond_wait 时,锁被释放,等待条件变量变为 真,变真之后,再次锁住mutex,
一。互斥量和条件变量简介
互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥锁加锁的线程将会阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥锁加锁,其他线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用。
条件变量(cond)是在多线程程序中用来实现”等待–》唤醒”逻辑常用的方法。条件变量利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使“条件成立”。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。线程在改变条件状态前必须首先锁住互斥量,函数pthread_cond_wait把自己放到等待条件的线程列表上,然后对互斥锁解锁(这两个操作是原子操作)。在函数返回时,互斥量再次被锁住。
二。为什么存在条件变量
首先,举个例子:在应用程序中有连个线程thread1,thread2,thread3和thread4,有一个int类型的全局变量iCount。iCount初始化为0,thread1和thread2的功能是对iCount的加1,thread3的功能是对iCount的值减1,而thread4的功能是当iCount的值大于等于100时,打印提示信息并重置iCount=0。
如果使用互斥量,线程代码大概应是下面的样子:
thread1/2:
while (1)
{pthread_mutex_lock(&mutex);iCount++;pthread_mutex_unlock(&mutex);
}thread4:
while(1)
{pthead_mutex_lock(&mutex);if (100 <= iCount){printf("iCount >= 100\r\n");iCount = 0;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);}
}
在上面代码中由于thread4并不知道什么时候iCount会大于等于100,所以就会一直在循环判断,但是每次判断都要加锁、解锁(即使本次并没有修改iCount)。这就带来了问题一,CPU浪费严重。所以在代码中添加了sleep(),这样让每次判断都休眠一定时间。但这由带来的第二个问题,如果sleep()的时间比较长,导致thread4处理不够及时,等iCount到了很大的值时才重置。对于上面的两个问题,可以使用条件变量来解决。
首先看一下使用条件变量后,线程代码大概的样子:
thread1/2:
while(1)
{pthread_mutex_lock(&mutex);iCount++;pthread_mutex_unlock(&mutex);if (iCount >= 100){pthread_cond_signal(&cond);}
} thread4:
while (1)
{pthread_mutex_lock(&mutex);while(iCount < 100){pthread_cond_wait(&cond, &mutex);}printf("iCount >= 100\r\n");iCount = 0;pthread_mutex_unlock(&mutex);
}
从上面的代码可以看出thread4中,当iCount < 100时,会调用pthread_cond_wait。而pthread_cond_wait在上面应经讲到它会释放mutex,然后等待条件变为真返回。当返回时会再次锁住mutex。因为pthread_cond_wait会等待,从而不用一直的轮询,减少CPU的浪费。在thread1和thread2中的函数pthread_cond_signal会唤醒等待cond的线程(即thread4),这样当iCount一到大于等于100就会去唤醒thread4。从而不致出现iCount很大了,thread4才去处理。
需要注意的一点是:在thread4中使用的while (iCount < 100),而不是if (iCount < 100)。这是因为在pthread_cond_singal()和pthread_cond_wait()返回之间有时间差,假如在时间差内,thread3又将iCount减到了100以下了,那么thread4就需要在等待条件为真了。
总结,注意两点:
- 在thread_cond_wait()之前,必须先lock相关联的mutex, 因为假如目标条件未满足,pthread_cond_wait()实际上会 unlock 该 mutex,然后 block,在目标条件满足后再重新 lock 该 mutex, 然后返回。
- 为什么是while(iCount <100),而不是if(iCount <100) ? 这是因为在pthread_cond_signal()和pthread_cond_wait()返回之间有时间差,假设在这个时间差内,还有另外一个线程又把 iCount 减少到100以下了,那么 thread4 在pthread_cond_wait()返回之后,显然应该再检查一遍 iCount 的大小,这就是用 while 的用意。
示例代码:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; /* 初始化互斥锁 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; /* 初始化条件变量 */ void *thread1(void *);
void *thread2(void *); int i=1; int main(void)
{ pthread_t t_a; pthread_t t_b; pthread_create(&t_a,NULL,thread1,(void *)NULL);/*创建进程t_a*/ pthread_create(&t_b,NULL,thread2,(void *)NULL); /*创建进程t_b*/ pthread_join(t_a, NULL);/*等待进程t_a结束*/ pthread_join(t_b, NULL);/*等待进程t_b结束*/ pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); exit(0);
} void *thread1(void *junk)
{ for(i=1;i<=6;i++){ pthread_mutex_lock(&mutex); /* 锁住互斥量 */if(i%3==0){ printf("thread1:signal 1 %d/n", __LINE__); pthread_cond_signal(&cond); /* 条件改变,发送信号,通知t_b进程 */printf("thread1:signal 2 %d/n", __LINE__); sleep(1); } pthread_mutex_unlock(&mutex); /* 解锁互斥量 */sleep(1); }
} void *thread2(void *junk)
{ while(i<6){ pthread_mutex_lock(&mutex); if(i%3!=0){ printf("thread2: wait 1 %d/n", __LINE__); pthread_cond_wait(&cond,&mutex); /* 解锁mutex,并等待cond改变 */ printf("thread2: wait 2 %d/n", __LINE__); } pthread_mutex_unlock(&mutex); sleep(1); }
}
使用条件变量实现“生产者消费者问题”:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<time.h>
#include<pthread.h>#define BUFFER_SIZE 16struct prodcons
{int buffer[BUFFER_SIZE];pthread_mutex_t lock; //mutex ensuring exclusive access to bufferint readpos,writepos; //position for reading and writingpthread_cond_t notempty; //signal when buffer is not emptypthread_cond_t notfull; //signal when buffer is not full
};//initialize a buffer
void init(struct prodcons* b)
{pthread_mutex_init(&b->lock,NULL);pthread_cond_init(&b->notempty,NULL);pthread_cond_init(&b->notfull,NULL);b->readpos=0;b->writepos=0;
}//store an integer in the buffer
void put(struct prodcons* b, int data)
{pthread_mutex_lock(&b->lock);//wait until buffer is not fullwhile((b->writepos+1)%BUFFER_SIZE==b->readpos){printf("wait for not full\n");pthread_cond_wait(&b->notfull,&b->lock);} b->buffer[b->writepos]=data;b->writepos++;pthread_cond_signal(&b->notempty); //signal buffer is not emptypthread_mutex_unlock(&b->lock);
}//read and remove an integer from the buffer
int get(struct prodcons* b)
{int data;pthread_mutex_lock(&b->lock);//wait until buffer is not emptywhile(b->writepos==b->readpos){printf("wait for not empty\n");pthread_cond_wait(&b->notempty,&b->lock);} data=b->buffer[b->readpos];b->readpos++;if(b->readpos>=BUFFER_SIZE) b->readpos=0;pthread_cond_signal(&b->notfull); //signal buffer is not fullpthread_mutex_unlock(&b->lock);return data;
}#define OVER -1struct prodcons buffer;void * producer(void * data)
{int n;for(n=0;n<1000;++n){printf("put-->%d\n",n);put(&buffer,n);}put(&buffer,OVER);printf("producer stopped\n");return NULL;
}void * consumer(void * data)
{int n;while(1){int d=get(&buffer);if(d==OVER) break;printf("%d-->get\n",d);}printf("consumer stopped\n");return NULL;
}int main()
{pthread_t tha,thb;void * retval;init(&buffer);pthread_creare(&tha,NULL,producer,0);pthread_creare(&thb,NULL,consumer,0);pthread_join(tha,&retval);pthread_join(thb,&retval);return 0;
}
生产者,消费者 2:
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
struct msg {struct msg *next;int num;
};
struct msg *head;
/**1.静态初始化,全局变量和静态局部变量可以采用静态初始化方法*2.动态初始化,局部变量或堆变量需要采用动态初始化** */
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void *consumer(void *p)
{struct msg *mp;for (;;) {pthread_mutex_lock(&lock);while (head == NULL)pthread_cond_wait(&has_product, &lock);//1.等待条件变量唤醒 2.释放已持有的互斥锁 3,唤醒时重新去拿互斥锁mp = head; //模拟消费产品head = mp->next;pthread_mutex_unlock(&lock);printf("Consume %d\n", mp->num); free(mp);sleep(rand() % 5);}
}
void *producer(void *p)
{struct msg *mp;for (;;) {mp = malloc(sizeof(struct msg));mp->num = rand() % 1000 + 1; //模拟生产产品printf("Produce %d\n", mp->num);pthread_mutex_lock(&lock);mp->next = head;head = mp;pthread_mutex_unlock(&lock);pthread_cond_signal(&has_product);sleep(rand() % 5);}
}
int main(int argc, char *argv[])
{pthread_t pid, cid;srand(time(NULL));pthread_create(&pid, NULL, producer, NULL);pthread_create(&cid, NULL, consumer, NULL);pthread_join(pid, NULL);pthread_join(cid, NULL);return 0;
}
信号量
如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。
信号量函数的名字都以"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);这个函数的作用是再我们用完信号量后都它进行清理。归还自己占有的一切资源。
用信号量的方法实现生产者消费者
这里使用4个信号量,其中两个信号量occupied和empty分别用于解决生产者和消费者线程之间的同步问题,pmut和cmut是用于这两个线程之间的互斥问题。其中empty初始化为N(有界缓区的空间元数),occupied初始化为0,pmut和cmut初始化为1。
typedef struct
{char buf[BSIZE];sem_t occupied;sem_t empty;int nextin;int nextout;sem_t pmut;sem_t cmut;
}buffer_t;buffer_t buffer;void init(buffer_t buffer)
{sem_init(&buffer.occupied, 0, 0);sem_init(&buffer.empty,0, BSIZE);sem_init(&buffer.pmut, 0, 1);sem_init(&buffer.cmut, 0, 1);buffer.nextin = buffer.nextout = 0;
}void producer(buffer_t *b, char item)
{sem_wait(&b->empty);sem_wait(&b->pmut);b->buf[b->nextin] = item;b->nextin++;b->nextin %= BSIZE;sem_post(&b->pmut);sem_post(&b->occupied);
}char consumer(buffer_t *b)
{char item;sem_wait(&b->occupied);sem_wait(&b->cmut);item = b->buf[b->nextout];b->nextout++;b->nextout %= BSIZE;sem_post(&b->cmut);sem_post(&b->empty);return(item);
}
信号量 生产者和消费者:
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#define NUM 5int queue[NUM];
sem_t blank_number, product_number;void *producer(void *arg)
{int p = 0;while (1) {sem_wait(&blank_number); //pthread_mutex_lock()queue[p] = rand() % 1000 + 1;printf("Produce %d\n", queue[p]);sem_post(&product_number);p = (p+1)%NUM;sleep(rand()%5);}
}
void *consumer(void *arg)
{int c = 0;while (1) {sem_wait(&product_number);printf("Consume %d\n", queue[c]);queue[c] = 0;sem_post(&blank_number);c = (c+1)%NUM;sleep(rand()%5);}
}
int main(int argc, char *argv[])
{pthread_t pid, cid;sem_init(&blank_number, 0, NUM);sem_init(&product_number, 0, 0);pthread_create(&pid, NULL, producer, NULL);pthread_create(&cid, NULL, consumer, NULL);pthread_join(pid, NULL);pthread_join(cid, NULL);sem_destroy(&blank_number);sem_destroy(&product_number);return 0;
}
3. 线程原语(函数)
进程原语 | 线程原语 | 描述 |
fork | pthread_create | 创建新的控制流 |
exit | pthread_exit | 从现有的控制流中退出 |
waitpid | pthread_join | 从控制流中得到退出状态 |
atexit | pthread_cancel_push | 注册在退出控制流时调用的函数 |
getpid | pthread_self | 获取控制流的ID |
abort | pthread_cancel | 请深圳市控制流的非正常退出 |
linux 线程函数大全:http://blog.csdn.net/haidonglin/article/details/4379735
创建一个缺省的线程。缺省的线程的属性: 非绑定、 未分离、一个缺省大小的堆栈、 具有和父线程一样的优先级
用 phread_attr_init() 创建一个缺省的属性对象,
用属性对象创建一个线程 pthread_create(3T)
4. 线程属性
线程的属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。
/* man pthread_attr_init */
typedef struct
{int detachstate; //是否与其他线程脱离同步int schedpolicy; //新线程的调度策略struct sched_param schedparam; //运行优先级等int inheritsched; //是否继承调用者线程的值int scope; //线程竞争CPU的范围(优先级的范围)size_t guardsize; //警戒堆栈的大小int stackaddr_set; //堆栈地址集void * stackaddr; //堆栈地址size_t stacksize; //堆栈大小
} pthread_attr_t;
关于线程的绑定,牵涉到另外一个概念:轻量级进程(LWP:Light Weight Process)。轻量级进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数
第一个是指向属性结构的指针。
第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。
下面的代码即创建了一个绑定的线程。
#include <pthread.h>pthread_attr_t attr;
pthread_t tid;/*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);pthread_create(&tid, &attr, (void *) my_function, NULL);
另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数pthread_attr_getschedparam和函数pthread_attr_setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。
示例代码:
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>#define STACK_SIZE 1024*1024
void *dowork(void *arg)
{while (1)sleep(10);
}
int main(void)
{int i = 1, res;char *p;pthread_t tid;pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //属性里设置创建出来的线程为分离态while (1) {p = malloc(STACK_SIZE);pthread_attr_setstack(&attr, p, STACK_SIZE);res = pthread_create(&tid, &attr, dowork, NULL);if (res != 0) {printf("%s\n", strerror(res));break;}printf("%d\n", i);i++;}pthread_attr_destroy(&attr);return 0;
}
5. NPTL (POSIX Thread Library)
http://people.redhat.com/drepper/nptl-design.pdf
http://www.gelato.unsw.edu.au/papers/ianw/2003/AUUG/paper.pdf
介绍NPTL (经典--诠释了线程的概念,以及linux调度单位):http://blog.csdn.net/zhangxinrun/article/details/5958742
LinuxThreads 和 NPTL:http://blog.csdn.net/hzrandd/article/details/50577653
LinuxThreads 项目最初将多线程的概念引入了 Linux®,但是 LinuxThreads 并不遵守 POSIX 线程标准。尽管更新的 Native POSIX Thread Library(NPTL)库填补了一些空白,但是这仍然存在一些问题。本文为那些需要将自己的应用程序从 LinuxThreads 移植到 NPTL 上或者只是希望理解有何区别的开发人员介绍这两种 Linux 线程模型之间的区别。
当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone()
系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads 项目使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个线程模型也不符合 POSIX 的要求。
要改进 LinuxThreads,非常明显我们需要内核的支持,并且需要重写线程库。有两个相互竞争的项目开始来满足这些要求。一个包括 IBM 的开发人员的团队开展了 NGPT(Next-Generation POSIX Threads)项目。同时,Red Hat 的一些开发人员开展了 NPTL 项目。NGPT 在 2003 年中期被放弃了,把这个领域完全留给了 NPTL。
尽管从 LinuxThreads 到 NPTL 看起来似乎是一个必然的过程,但是如果您正在为一个历史悠久的 Linux 发行版维护一些应用程序,并且计划很快就要进行升级,那么如何迁移到 NPTL 上就会变成整个移植过程中重要的一个部分。另外,我们可能会希望了解二者之间的区别,这样就可以对自己的应用程序进行设计,使其能够更好地利用这两种技术。
本文详细介绍了这些线程模型分别是在哪些发行版上实现的。
LinuxThreads 设计细节
线程 将应用程序划分成一个或多个同时运行的任务。线程与传统的多任务进程 之间的区别在于:线程共享的是单个进程的状态信息,并会直接共享内存和其他资源。同一个进程中线程之间的上下文切换通常要比进程之间的上下文切换速度更快。因此,多线程程序的优点就是它可以比多进程应用程序的执行速度更快。另外,使用线程我们可以实现并行处理。这些相对于基于进程的方法所具有的优点推动了 LinuxThreads 的实现。
LinuxThreads 最初的设计相信相关进程之间的上下文切换速度很快,因此每个内核线程足以处理很多相关的用户级线程。这就导致了一对一 线程模型的革命。
让我们来回顾一下 LinuxThreads 设计细节的一些基本理念:
LinuxThreads 非常出名的一个特性就是管理线程(manager thread)。管理线程可以满足以下要求:
系统必须能够响应终止信号并杀死整个进程。
以堆栈形式使用的内存回收必须在线程完成之后进行。因此,线程无法自行完成这个过程。
终止线程必须进行等待,这样它们才不会进入僵尸状态。
线程本地数据的回收需要对所有线程进行遍历;这必须由管理线程来进行。
如果主线程需要调用 pthread_exit(),那么这个线程就无法结束。主线程要进入睡眠状态,而管理线程的工作就是在所有线程都被杀死之后来唤醒这个主线程。
为了维护线程本地数据和内存,LinuxThreads 使用了进程地址空间的高位内存(就在堆栈地址之下)。
原语的同步是使用信号 来实现的。例如,线程会一直阻塞,直到被信号唤醒为止。
在克隆系统的最初设计之下,LinuxThreads 将每个线程都是作为一个具有惟一进程 ID 的进程实现的。
终止信号可以杀死所有的线程。LinuxThreads 接收到终止信号之后,管理线程就会使用相同的信号杀死所有其他线程(进程)。
根据 LinuxThreads 的设计,如果一个异步信号被发送了,那么管理线程就会将这个信号发送给一个线程。如果这个线程现在阻塞了这个信号,那么这个信号也就会被挂起。这是因为管理线程无法将这个信号发送给进程;相反,每个线程都是作为一个进程在执行。
线程之间的调度是由内核调度器来处理的。
LinuxThreads 及其局限性
LinuxThreads 的设计通常都可以很好地工作;但是在压力很大的应用程序中,它的性能、可伸缩性和可用性都会存在问题。下面让我们来看一下 LinuxThreads 设计的一些局限性:
它使用管理线程来创建线程,并对每个进程所拥有的所有线程进行协调。这增加了创建和销毁线程所需要的开销。
由于它是围绕一个管理线程来设计的,因此会导致很多的上下文切换的开销,这可能会妨碍系统的可伸缩性和性能。
由于管理线程只能在一个 CPU 上运行,因此所执行的同步操作在 SMP 或 NUMA 系统上可能会产生可伸缩性的问题。
由于线程的管理方式,以及每个线程都使用了一个不同的进程 ID,因此 LinuxThreads 与其他与 POSIX 相关的线程库并不兼容。
信号用来实现同步原语,这会影响操作的响应时间。另外,将信号发送到主进程的概念也并不存在。因此,这并不遵守 POSIX 中处理信号的方法。
LinuxThreads 中对信号的处理是按照每线程的原则建立的,而不是按照每进程的原则建立的,这是因为每个线程都有一个独立的进程 ID。由于信号被发送给了一个专用的线程,因此信号是串行化的 —— 也就是说,信号是透过这个线程再传递给其他线程的。这与 POSIX 标准对线程进行并行处理的要求形成了鲜明的对比。例如,在 LinuxThreads 中,通过 kill() 所发送的信号被传递到一些单独的线程,而不是集中整体进行处理。这意味着如果有线程阻塞了这个信号,那么 LinuxThreads 就只能对这个线程进行排队,并在线程开放这个信号时在执行处理,而不是像其他没有阻塞信号的线程中一样立即处理这个信号。
由于 LinuxThreads 中的每个线程都是一个进程,因此用户和组 ID 的信息可能对单个进程中的所有线程来说都不是通用的。例如,一个多线程的 setuid()/setgid() 进程对于不同的线程来说可能都是不同的。
有一些情况下,所创建的多线程核心转储中并没有包含所有的线程信息。同样,这种行为也是每个线程都是一个进程这个事实所导致的结果。如果任何线程发生了问题,我们在系统的核心文件中只能看到这个线程的信息。不过,这种行为主要适用于早期版本的 LinuxThreads 实现。
由于每个线程都是一个单独的进程,因此 /proc 目录中会充满众多的进程项,而这实际上应该是线程。
由于每个线程都是一个进程,因此对每个应用程序只能创建有限数目的线程。例如,在 IA32 系统上,可用进程总数 —— 也就是可以创建的线程总数 —— 是 4,090。
由于计算线程本地数据的方法是基于堆栈地址的位置的,因此对于这些数据的访问速度都很慢。另外一个缺点是用户无法可信地指定堆栈的大小,因为用户可能会意外地将堆栈地址映射到本来要为其他目的所使用的区域上了。按需增长(grow on demand) 的概念(也称为浮动堆栈的概念)是在 2.4.10 版本的 Linux 内核中实现的。在此之前,LinuxThreads 使用的是固定堆栈。
关于 NPTL
NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。与 LinuxThreads 一样,NPTL 也实现了一对一的模型。
Ulrich Drepper 和 Ingo Molnar 是 Red Hat 参与 NPTL 设计的两名员工。他们的总体设计目标如下:
这个新线程库应该兼容 POSIX 标准。
这个线程实现应该在具有很多处理器的系统上也能很好地工作。
为一小段任务创建新线程应该具有很低的启动成本。
NPTL 线程库应该与 LinuxThreads 是二进制兼容的。注意,为此我们可以使用 LD_ASSUME_KERNEL,这会在本文稍后进行讨论。
这个新线程库应该可以利用 NUMA 支持的优点。
NPTL 的优点
与 LinuxThreads 相比,NPTL 具有很多优点:
NPTL 没有使用管理线程。管理线程的一些需求,例如向作为进程一部分的所有线程发送终止信号,是并不需要的;因为内核本身就可以实现这些功能。内核还会处理每个线程堆栈所使用的内存的回收工作。它甚至还通过在清除父线程之前进行等待,从而实现对所有线程结束的管理,这样可以避免僵尸进程的问题。
由于 NPTL 没有使用管理线程,因此其线程模型在 NUMA 和 SMP 系统上具有更好的可伸缩性和同步机制。
使用 NPTL 线程库与新内核实现,就可以避免使用信号来对线程进行同步了。为了这个目的,NPTL 引入了一种名为 futex 的新机制。futex 在共享内存区域上进行工作,因此可以在进程之间进行共享,这样就可以提供进程间 POSIX 同步机制。我们也可以在进程之间共享一个 futex。这种行为使得进程间同步成为可能。实际上,NPTL 包含了一个 PTHREAD_PROCESS_SHARED 宏,使得开发人员可以让用户级进程在不同进程的线程之间共享互斥锁。
由于 NPTL 是 POSIX 兼容的,因此它对信号的处理是按照每进程的原则进行的;getpid() 会为所有的线程返回相同的进程 ID。例如,如果发送了 SIGSTOP 信号,那么整个进程都会停止;使用 LinuxThreads,只有接收到这个信号的线程才会停止。这样可以在基于 NPTL 的应用程序上更好地利用调试器,例如 GDB。
由于在 NPTL 中所有线程都具有一个父进程,因此对父进程汇报的资源使用情况(例如 CPU 和内存百分比)都是对整个进程进行统计的,而不是对一个线程进行统计的。
NPTL 线程库所引入的一个实现特性是对 ABI(应用程序二进制接口)的支持。这帮助实现了与 LinuxThreads 的向后兼容性。这个特性是通过使用 LD_ASSUME_KERNEL 实现的,下面就来介绍这个特性。
LD_ASSUME_KERNEL 环境变量
正如上面介绍的一样,ABI 的引入使得可以同时支持 NPTL 和 LinuxThreads 模型。基本上来说,这是通过 ld (一个动态链接器/加载器)来进行处理的,它会决定动态链接到哪个运行时线程库上。
举例来说,下面是 WebSphere® Application Server 对这个变量所使用的一些通用设置;您可以根据自己的需要进行适当的设置:
LD_ASSUME_KERNEL=2.4.19:这会覆盖 NPTL 的实现。这种实现通常都表示使用标准的 LinuxThreads 模型,并启用浮动堆栈的特性。
LD_ASSUME_KERNEL=2.2.5:这会覆盖 NPTL 的实现。这种实现通常都表示使用 LinuxThreads 模型,同时使用固定堆栈大小。
我们可以使用下面的命令来设置这个变量:
export LD_ASSUME_KERNEL=2.4.19
注意,对于任何 LD_ASSUME_KERNEL 设置的支持都取决于目前所支持的线程库的 ABI 版本。
例如,如果线程库并不支持 2.2.5 版本的 ABI,那么用户就不能将 LD_ASSUME_KERNEL 设置为 2.2.5。通常,NPTL 需要 2.4.20,而 LinuxThreads 则需要 2.4.1。
如果您正运行的是一个启用了 NPTL 的 Linux 发行版,但是应用程序却是基于 LinuxThreads 模型来设计的,那么所有这些设置通常都可以使用。
GNU_LIBPTHREAD_VERSION 宏
大部分现代 Linux 发行版都预装了 LinuxThreads 和 NPTL,因此它们提供了一种机制来在二者之间进行切换。
要查看您的系统上正在使用的是哪个线程库,请运行下面的命令:$ getconf GNU_LIBPTHREAD_VERSION这会产生类似于下面的输出结果:NPTL 0.34或者:linuxthreads-0.10
结束语
LinuxThreads 的限制已经在 NPTL 以及 LinuxThreads 后期的一些版本中得到了克服。例如,最新的 LinuxThreads 实现使用了线程注册来定位线程本地数据;例如在 Intel® 处理器上,它就使用了 %fs 和 %gs 段寄存器来定位访问线程本地数据所使用的虚拟地址。尽管这个结果展示了 LinuxThreads 所采纳的一些修改的改进结果,但是它在更高负载和压力测试中,依然存在很多问题,因为它过分地依赖于一个管理线程,使用它来进行信号处理等操作。
您应该记住,在使用 LinuxThreads 构建库时,需要使用 -D_REENTRANT 编译时标志。这使得库线程是安全的。
最后,也许是最重要的事情,请记住 LinuxThreads 项目的创建者已经不再积极更新它了,他们认为 NPTL 会取代 LinuxThreads。
LinuxThreads 的缺点并不意味着 NPTL 就没有错误。作为一个面向 SMP 的设计,NPTL 也有一些缺点。我曾经看到过在最近的 Red Hat 内核上出现过这样的问题:一个简单线程在单处理器的机器上运行良好,但在 SMP 机器上却挂起了。我相信在 Linux 上还有更多工作要做才能使它具有更好的可伸缩性,从而满足高端应用程序的需求。
7. 示例
示例1:
简单实现多线程需要用到 pthread_create函数和pthread_create函数
#include <stdio.h>
#include <pthread.h>
#include <stddef.h>
void thread(void){ int i; for(i=0;i<60 ; i++){ printf("这是子线程!\n"); sleep(1); }
} int main(void){ pthread_t id; int i,ret; ret = pthread_create(&id,NULL,(void *)thread,NULL); if(ret!=0){ printf("创建新显存错误!\n"); exit(1); } for(i=0;i<50;i++){ printf("这是主线程.\n"); sleep(1); } pthread_join(id,NULL); printf("两个线程都已经运行结束.\n"); return 0;
}
Linux下多线程需要用到pthread.h头文件,所以在代码的开始处引入了头文件。在例子中使用到了NULL,所以我们也引入了stddef.h头文件。接下来我们从main函数开始分析代码。
pthread_t是一个多线程标识符,在pthreadtypes.h中申明:
typedef unsigned long int pthread_t;
pthread_t 用来记录线程ID的。
pthread_create函数在pthread.h中声明,如果创建线程成功则返回0,如果返回线程失败则返回非零。
pthread_create函数有四个参数,第一个参数为指向线程标识符的指针,就是我们前边申明的pthread_t id。第二个参数用来设置线程属性。第三个参数是线程运行函数的起始地址,例子中的多线程函数为thread函数,所以在这里就传入thread函数的地址。最后一个参数是运行函数的参数,例子中thread无参数,所以此处传入NULL。
pthread_join函数同样在pthread.h中声明,是用来等待一个线程的结束,一般情况线程结束之后需要统一回收资源。pthread_join的第一个参数是需要等待结束的现场ID,其类型为pthread_t,第二个参数是用来存储线程结束后的返回值。
连接时需要使用库libpthread.a,所以在编译必须在选项中加入 -lpthread 选项,例如
gcc main.c -lpthread -o main.o.
示例2
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#include <stddef.h> const int MY_HIGTH_PRIORITY=10;
const int MY_LOW_PRIORITY=3;
void pthread_function_1(void);
void pthread_function_2(void);
int pthread_function_3(void); int main(void){ pthread_t pf_1,pf_2,pf_3; pthread_attr_t pfattr_1; pthread_attr_t pfattr_2; pthread_attr_t * pfattr_3; int ret = 0; int i = 0; int result; //sched_param 线程优先级 该strut中只有一个属性 struct sched_param my_sched_param1,my_sched_param2,my_sched_param3; //初始化pfattr_1 pthread_attr_init(&pfattr_1); //设置_scope属性,该属性有两个值 //PTHREAD_SCOPE_SYSTEM表示跟系统的所有进程竞争CPU PTHREAD_SCOPE_PROCESS表示跟该进程中的所有的线程竞争CPU pthread_attr_setscope(&pfattr_1,PTHREAD_SCOPE_SYSTEM); //设置detachstate属性 该属性是表示该线程是否与进程中的其他线程脱离同步 该属性有两个值 //PTHREAD_CREATE_JOINABLE不与其他线程脱机同步 PTHREAD_CREATE_DETACHED与其他线程脱机同步 pthread_attr_setdetachstate(&pfattr_1,PTHREAD_CREATE_DETACHED); my_sched_param1.sched_priority=MY_HIGTH_PRIORITY; pthread_attr_setschedparam(&pfattr_1,&my_sched_param1); /************同样的方式设置线程2 线程3****************/ pthread_attr_init(&pfattr_2); pthread_attr_setscope(&pfattr_2,PTHREAD_SCOPE_SYSTEM); pthread_attr_setdetachstate(&pfattr_2,PTHREAD_CREATE_DETACHED); my_sched_param2.sched_priority=MY_LOW_PRIORITY; pthread_attr_setschedparam(&pfattr_2,&my_sched_param2); pfattr_3 =(pthread_attr_t *) malloc(sizeof(pthread_attr_t)); pthread_attr_init(pfattr_3); pthread_attr_setscope(pfattr_3,PTHREAD_SCOPE_SYSTEM); pthread_attr_setdetachstate(pfattr_3,PTHREAD_CREATE_JOINABLE); my_sched_param3.sched_priority=MY_LOW_PRIORITY; pthread_attr_setschedparam(pfattr_3,&my_sched_param3); ret = pthread_create(&pf_1,&pfattr_1,(void *)pthread_function_1,NULL); if(ret!=0){ printf("线程1创建失败!\n"); } ret = pthread_create(&pf_2,&pfattr_2,(void *)pthread_function_2,NULL); if(ret!=0){ printf("线程2创建失败!\n"); } ret = pthread_create(&pf_3,pfattr_3,(void *)pthread_function_3,NULL); if(ret!=0){ printf("线程3创建失败!\n"); } for(i=0;i<500;i++){ printf("这是MAIN线程:NUM=%d\n",i); } pthread_join(pf_3,(void *)&result); printf("主线程和线程3运行结束。。。\n"); printf("线程3返回的结果为:RESULT:%d\n",result); //释放内存空间 free(pfattr_3);
} void pthread_function_1(void){ int i = 0; printf("我是线程1,我开始执行了。。。。。\n"); for(i=0;i<800;i++){ printf("我是线程1,我执行到NUM=%d\n",i); } printf("我是线程1,我执行结束了。。。。\n");
} void pthread_function_2(void){ int i = 0; printf("我是线程2,我开始执行了。。。。。\n"); for(i=0;i<800;i++){ printf("我是线程2,我执行到NUM=%d\n",i); } printf("我是线程2,我执行结束了。。。。\n");
} int pthread_function_3(void){ int i = 0; int result = 0; printf("我是线程3,我开始执行了。。。。。\n"); for(i=0;i<600;i++){ printf("我是线程3,我执行到NUM=%d\n",i); result = result + i; } printf("我是线程3,我执行结束了。。。。\n"); return result;
}
这个例子中创建了三个子程序,主要用这三个子程序来看线程属性参数的作用。线程属性的声明类型为 pthread_attr_t,同样在pthreadtypes.h中定义。
typedef union
{ char __size[__SIZEOF_PTHREAD_ATTR_T]; long int __align;
} pthread_attr_t;
设置线程熟悉第一步需要初始化,调用pthread_attr_init 函数,需要传入线程属性变量的地址。
一般情况下会用到以下三种属性。
第一种:scope属性,该该属性有两个值。PTHREAD_SCOPE_SYSTEM表示跟系统的所有进程竞争。CPU PTHREAD_SCOPE_PROCESS表示跟该进程中的所有的线程竞争CPU
第二种:detachstate属性,该属性是表示该线程是否与进程中的其他线程脱离同步。该属性有两个值。PTHREAD_CREATE_JOINABLE不与其他线程脱机同步,该属性为默认属性,线程为该熟悉的时候可以用pthread_join方法同步,结束之后可以在其他现场中回收资源,例子中就是在main线程中回收线程3的资源。PTHREAD_CREATE_DETACHED与其他线程脱机同步,该设置是不可逆的,不能再设置成PTHREAD_CREATE_JOINABLE属性,也不能用pthread_join方法 同步。
第三种:设置线程的优先级,优先级需要传入struct sched_param类型的参数,在sched.h中定义。
struct sched_param
{ int __sched_priority;
};
在struct sched_param结构体中只有_sched_priority一个属性,在sched.h中有#define sched_priority __sched_priority定义,所以我们在例子中使用的是
my_sched_param1.sched_priority=MY_HIGTH_PRIORITY;
在该例子中用pthread_join函数接收线程3返回的值。
示例3
linux下的C\C++多进程多线程编程实例详解
1、多进程编程#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h> int main()
{ pid_t child_pid; /* 创建一个子进程 */ child_pid = fork(); if(child_pid == 0) { printf("child pid\n"); exit(0); } else{ printf("father pid\n"); sleep(60); } return 0;
} 2、多线程编程#include <stdio.h>
#include <pthread.h> struct char_print_params
{ char character; int count;
}; void *char_print(void *parameters)
{ struct char_print_params *p = (struct char_print_params *)parameters; int i; for(i = 0; i < p->count; i++) { fputc(p->character,stderr); } return NULL;
} int main()
{ pthread_t thread1_id; pthread_t thread2_id; struct char_print_params thread1_args; struct char_print_params thread2_args; thread1_args.character = 'x'; thread1_args.count = 3000; pthread_create(&thread1_id, NULL, &char_print, &thread1_args); thread2_args.character = 'o'; thread2_args.count = 2000; pthread_create(&thread2_id, NULL, &char_print, &thread2_args); pthread_join(thread1_id, NULL); pthread_join(thread2_id, NULL); return 0;
} 3、线程同步与互斥1)、互斥
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); /*也可以用下面的方式初始化*/
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&mutex);
/* 互斥 */thread_flag = value; pthread_mutex_unlock(&mutex); 2)、条件变量int thread_flag = 0;
pthread_mutex_t mutex;
pthread_cond_t thread_flag_cv;\ void init_flag()
{ pthread_mutex_init(&mutex, NULL); pthread_cond_init(&thread_flag_cv, NULL); thread_flag = 0;
} void *thread_function(void *thread_flag)
{ while(1) { pthread_mutex_lock(&mutex); while(thread_flag != 0 ) { pthread_cond_wait(&thread_flag_cv, &mutex); } pthread_mutex_unlock(&mutex); do_work(); } return NULL;
} void set_thread_flag(int flag_value)
{ pthread_mutex_lock(&mutex); thread_flag = flag_value; pthread_cond_signal(&thread_flag_cv); pthread_mutex_unlock(&mutex);
}
向线程传递参数
//向线程函数传递整形参数 。注:字符串,结构参数,一样道理
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *create(void *arg)
{int *num;num=(int *)arg;printf("create parameter is %d \n",*num);return (void *)0;
}
int main(int argc,char *argv[])
{pthread_t tidp;int error;int test=4;int* attr = &test;error=pthread_create(&tidp,NULL,create,(void*)attr);if(error){printf("pthread_create is created is not created...\n");return -1;}sleep(1);printf("pthread_create is created...\n");return 0;
}
示例4:(多线程拷贝,实现类似 cp 命令功能)
/*
编程实现多线程拷贝文件, 要求能随着线程数量的增加提高拷贝效率。
需完成简单的进度条提示。
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <pthread.h>
#define T_NUM 5
#define ITEMS 50
void err_sys(void *str)
{
perror(str);
exit(1);
}
void err_usr(char *str)
{
fputs(str, stderr);
exit(1);
}
typedef struct
{
int off, size, t_no;
}arg_t;
char *s, *d;
int *done;
int n = T_NUM;
//arg{off, size, t_no;}
void *tfn(void *arg)
{
arg_t *arg_p; int i;
char *p, *q;
arg_p = (arg_t *)arg;
p = s + arg_p->off, q = d + arg_p->off;
for (i = 0; i < arg_p->size; i++)
{
//使用逗号表达式来完成自增操作
*q++ = *p++, done[arg_p->t_no]++;
usleep(5000);
}
return NULL;
}
//拷贝进度条显示函数
void *display(void *arg)
{
int size, interval, draw, sum, i, j;
size = (int *)arg;
interval = size / (ITEMS - 1);
draw = 0;
while (draw < ITEMS)
{ //#define ITEMS 50
for (i = 0, sum = 0; i < n; i++)
sum += done[i];
j = sum / interval + 1;
for (; j > draw; draw++)
{
putchar('=');
fflush(stdout);
}
}
putchar('\n');
return NULL;
}
int main(int argc, char *argv[])
{
int src, dst, i, len, off;
struct stat statbuf;
pthread_t *tid;
arg_t *arr;
if (argc != 3 && argc != 4) //用户没有指定原/目标文件名,及创建线程数
err_usr("usage : cp src dst [thread_no]\n");
//指定创建线程数量
if (argc == 4) n = atoi(argv[3]);
src = open(argv[1], O_RDONLY); //打开源文件
if(src == -1) err_sys("fail to open");
//打开目标文件
dst = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, 0644);
if(dst == -1) err_sys("fail to open");
//使用文件描述符获取文件属性, statbuf结构体指针传出
if(fstat(src, &statbuf) == -1) err_sys("fail to stat");
lseek(dst, statbuf.st_size - 1, SEEK_SET);
write(dst, "a", 1);//IO操作拓展文件大小,也可以使用truncate
//给源文件建立映射区
s = (char *)mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, src, 0);
if(s == MAP_FAILED) err_sys("fail to mmap");
//给目标文件建立共享映射区
d = (char *)mmap(NULL, statbuf.st_size, PROT_WRITE , MAP_SHARED, dst, 0);
if(d == MAP_FAILED) err_sys("fail to mmap");
close(src); close(dst);
//pthread_t tid[n+1];
//根据线程数量,给线程开辟内存空间
tid = (pthread_t *)malloc(sizeof(pthread_t) * (n + 1));
if(tid == NULL) err_sys("fail to malloc");
//int done[n] 每个线程完成任务字节数
done = (int *)calloc(sizeof(int), n);
if(done == NULL) err_sys("fail to malloc");
//arr[n] 每个线程的任务
arr = (arg_t *)malloc(sizeof(arg_t) * n);
if(arr == NULL) err_sys("fail to malloc");
//构建线程任务数组,分配任务
//根据线程数量确定每个线程拷贝字节数的长度.初始偏移位置至为0
len = statbuf.st_size / n, off = 0;
for(i = 0; i < n; i++, off += len)
arr[i].off = off, arr[i].size = len, arr[i].t_no = i;
arr[n - 1].size += (statbuf.st_size % n);
//创建执行拷贝任务线程
for(i = 0; i < n; i++)
pthread_create(&tid[i], NULL, tfn, (void *)&arr[i]);
//创建进度线程
pthread_create(&tid[n], NULL, display, (void *)statbuf.st_size);
for(i = 0; i < n + 1; i++)
pthread_join(tid[i], NULL);
munmap(s, statbuf.st_size);
munmap(d, statbuf.st_size);
free(tid); free(done); free(arr);
return 0;
}
总述
主要内容:
第一部分 多线程
第二部分 互斥锁
第三部分 条件变量
第四部分 读写锁
第五部分 自旋锁
第六部分 线程壁垒
第七部分 记录锁
第一部分 多线程
Linux 线程的创建:
int pthread_create ( pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg);
tid:返回的线程id
attr:创建线程的属性可以为NULL,也可以在运行时再改变
func:线程运行的callback函数
arg:传给callback函数的参数
主要属性简要说明:
PTHREAD_CREATE_DETACH :表示同步脱离,且在退出时自行释放所占用的资源, 此时线程不能用pthread_join()来同步,这个属性可以在线程运行时调用pthread_detach()来设置,而一旦设置为 PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE(默 认属性)状态。
int pthread_detach (pthread_t tid);
说明:线程默认终止的时候资源不释放需要主进程调用pthread_join函数才能释放(类似于僵死进程和waitpid函数), pthread_detach 使得线程终止即释放资源,主进程不需要调用pthread_join函数
线程退出情形:
1) 正常return退出
2) 被其它线程调用pthread_cancel取消
3) 调用pthread_exit 退出,其中参数为函数退出时返回的数据
int pthread_join (pthread_t thread, void **rval_ptr);
阻塞直到指定线程终止, 并返回线程的返回值。
注意pthread_join函数操作的线程必须不是DETACHED线程。
rval_ptr返回值设置:
1) 如果线程正常return,则设置为return的值
2) 如果线程被取消则设置为PTHREAD_CANCELED
3) 如果线程调用pthread_exit退出则为pthread_exit 的参数
退出函数设置:
当线程退出时,可以设置一组退出函数类似于进程的atexit
void pthread_cleanup_push (void (*rtn)(void *), void *arg);
void pthread_cleanup_pop (int execute);
退出函数执行条件:
1) 线程调用pthread_exit 退出
2) 线程被其它线程取消
3) 调用pthread_cleanup_pop 并且参数非0
注意:函数正常return 不调用退出函数
线程的取消:
int pthread_cancel (pthread_t tid);发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止 .
int pthread_setcancelstate (int state, int *oldstate)
设置本线程对cancel信号的反应.
state有两种值:
1)PTHREAD_CANCEL_ENABLE(缺省)收到信号后设为CANCLED状态2)PTHREAD_CANCEL_DISABLE,忽略CANCEL信号继续运行
old_state如果不为NULL则存入原来的cancel状态以便恢复。
int pthread_setcanceltype (int type, int *oldtype)
设置本线程取消动作的执行时机,仅当cancel状态为Enable时有效
type由两种取值:
1) PTHREAD_CANCEL_DEFFERED表示收到信号后继续运行至下一个取消点再退出
2) PTHREAD_CANCEL_ASYCHRONOUS表示立即执行取消动作(退出);
oldtype如果不为NULL则存入原来的取消动作类型值。
void pthread_testcancel (void)
检查本线程是否处于canceld状态,如果是,则执行取消动作,否则直接返回。
此函数用来设置取消点
线程私有数据(Thread-specific Data,或TSD)
int pthread_key_create (pthread_key_t *keyptr, void (*destructor) (void *value));
该函数从TSD池中分配一项,将其值赋给keyptr供以后访问使用。如果destr_function不为空,在线程退出(pthread_exit())时将以key所关联的数据为参数调用destr_function(),以释放分配的缓冲区。
int pthread_key_delete (pthread_key_t key)这个函数并不检查当前是否有线程正使用该TSD,也不会调用清理函数,而只是将TSD释放以供下一次调用pthread_key_create()使用。
TSD的读写:
int pthread_setspecific (pthread_key_t key, const void *pointer)
写入,将pointer的值(不是所指的内容)与key相关联
void * pthread_getspecific (pthread_key_t key)
读出函数,将与key相关联的数据读出来。
说明:不同线程对同一个key的访问互不冲突。
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once (pthread_once_t *initflag, void (*initfn)(void));
多个线程调用,保证仅执行一次的函数,其中initflag不可以是局部变量。
获得线程本身id
pthread_t pthread_self (void);
线程id 比较int pthread_equal (pthread_t tid1, pthread_t tid2); 相等返回非0
线程IO操作:
如果多个线程并发访问同一个文件的时候可以使用pread或pwrite它们保证了原子操作
其它:
pthread_kill_other_threads_np() 强行杀死所有线程,它没有通过pthread_cancel()来终止线程,而是直接向管理线程发“进程退出”信号,使所有其他线程都结束运行,而不经过 cancel动作,当然也不会执行退出回调函数 ,一般用在exec执行前来结束所有正在运行的线程。
线程属性操作(略)
第二部分 互斥锁
Mutex基本操作函数:
初始化方法:
1) 如:static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER 使用默认属性
2) 调用pthread_mutex_init 方法,可以同时设定属性
销毁方法:
int pthread_mutex_destroy (pthread_mutex_t *mutex);
销毁一个互斥锁即释放它所占用的资源,并且要求锁当前是非锁定状态。由于在Linux中,互斥锁并不占用任何资源,它除了检查锁状态以外(锁定状态则返回EBUSY)没有其它动作。
互斥锁基本操作:
int pthread_mutex_lock (pthread_mutex_t *mutex);
int pthread_mutex_trylock (pthread_mutex_t *mutex);
int pthread_mutex_unlock (pthread_mutex_t *mutex);
Mutex基本属性操作:
int pthread_mutexattr_init (pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy (pthread_mutexattr_t *attr);
int pthread_mutexattr_getpshared (const pthread_mutexattr_t * restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared (pthread_mutexattr_t *attr, int pshared);
说明:
1) Mutex默认只能用于多线程间互斥操作即属性为PTHREAD_PROCESS_PRIVATE ,如果用于多进程操作必须修改其属性为PTHREAD_PROCESS_SHARED
2) pthread_mutexattr_init 和 pthread_mutexattr_destroy 函数用来初始化或析构pthread_mutexattr_t 结构 但由另外两个函数来设置PTHREAD_PROCESS_SHARED 属性
3) 注意有些平台不支持多进程间互斥锁.
其它属性简要介绍:
1) PTHREAD_MUTEX_TIMED_NP: 普通锁(缺省值)。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
2) PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争
3) PTHREAD_MUTEX_ERRORCHECK_NP: 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁
4) PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,仅等待解锁后重新竞争。
使用互斥锁注意事项:
1) Linux 中实现的POSIX线程锁都不是取消点,因此,线程被取消后不会因为收到取消信号而离开加锁等待状态。
2) 如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,或者设置了异步取消类型,则必须在退出回调函数中解锁。
3) 这个锁机制同时不是异步信号安全的,所以不应该在信号处理函数中使用互斥锁,否则容易造成死锁。
第三部分 条件变量
条件变量主要的目的是等待直到条件成立。类似于某些系统的Event。
它主要包括两个动作:
1) 一个线程等待“条件变量的条件成立”而挂起;
2) 另一个线程使“条件成立”。
注意:为了防止竞争条件变量的使用总是和一个互斥锁结合在一起
条件变量基本操作函数:初始化方法:
1) 如:static pthread_cond_t cond=PTHREAD_COND_INITIALIZER默认属性
2) 调用pthread_cond_init 方法,可以同时设定属性
销毁方法:
int pthread_cond_destroy (pthread_cond_t *cond);
只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。
等待操作:
int pthread_cond_wait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
int pthread_cond_timedwait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);
产生信号操作:
int pthread_cond_signal (pthread_cond_t *cond);激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个
int pthread_cond_broadcast (pthread_cond_t *cond);激活所有等待线程
属性基本操作(类似于互斥锁)
int pthread_condattr_init (pthread_condattr_t *attr);
int pthread_condattr_destroy (pthread_condattr_t *attr);
int pthread_condattr_getpshared (const pthread_condattr_t * restrict attr, int *restrict pshared);
int pthread_condattr_setpshared (pthread_condattr_t *attr, int pshared);
使用条件变量注意事项:
1) pthread_cond_wait()和pthread_cond_timedwait()都被实现为取消点, 在该处等待的线程将立即重新运行 2) 被取消后它首先获的互斥锁,然后执行取消动作。即互斥锁是保持锁定状态的,因而需要定义退出回调函数来为其解锁。
使用举例:
{
pthread_mutex_lock (&mutex));
pthread_cond_wait (&cond,& mutex);
pthread_mutex_unlock (&mutex);
}
第四部分 读写锁
读写锁基本特点:
1)如果没有线程持有写锁, 那么获得读锁就会成功
2)如果没有线程持有读锁和写锁,那么获得写锁才会成功
它常被应用于频繁读而很少修改得情况。
读写锁初始化或销毁:
static pthread_rwlock_t rwLock = PTHREAD_RWLOCK_INITIALIZER
int pthread_rwlock_init (pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy (pthread_rwlock_t *rwlock);
获得锁或释放锁操作
int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock);
属性设置
int pthread_rwlockattr_init (pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy (pthread_rwlockattr_t *attr);
int pthread_rwlockattr_getpshared (const pthread_rwlockattr_t * restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared (pthread_rwlockattr_t *attr, int pshared);
第五部分 自旋锁
自旋锁基本特点:
1) 在用户空间实现, 不需要做内核切换
2) 忙-等待,也就是说在获得不了锁的情况下仍然占用CPU资源
3) 要求关键区域执行时间必须非常得短小,并且不允许睡眠, 如果执行时间超过了其它线程等待的时间片(即时间片用完,发生了线程切换), 则用自旋锁就失去了意义,并空耗了CPU时间, 这种情况不防称为失败的等待
4) 如果失败的等待非常多会严重影响系统的性能。此时就应该选用其它锁
基本操作函数:
1) 初始化方法: __pshared大于0表示用在多进程间
int pthread_spin_init (pthread_spinlock_t *__lock, int __pshared)
2)销毁方法:
int pthread_spin_destory (pthread_spinlock_t * __lock)
3) 锁操作方法:
int pthread_spin_lock (pthread_spinlock_t * __lock)
int pthread_spin_trylock (pthread_spinlock_t * __lock)
int pthread_spin_unlock (pthread_spinlock_t * __lock)
第六部分 线程壁垒
壁垒(barrier)基本说明:
主要用来等待某些数量的线程在壁垒处集合。在设定的数目达到之后,解锁这些线程,让它们继续运行。
而pthread_join() 方法是等待线程结束
壁垒(barrier)基本操作函数:
初始化: count表示必须调用pthread_barrier_wait线程的个数
int pthread_barrier_init (pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count);
销毁函数:
int pthread_barrier_destroy (pthread_barrier_t *barrier)
同步点函数:
int pthread_barrier_wait (pthread_barrier_t *barrier);
第七部分 记录锁
记录锁主要用于不同进程间锁定文件,它可以锁定整个文件或部分字节数据
注意:记录锁是面向进程的,因此不能用于线程间。
主要操作函数如下:
int fcntl (int filedes, int cmd, ... /* struct flock *flockptr */ );
其中:
struct flock
{
short l_type; /* F_RDLCK(共享读), F_WRLCK(排斥写), F_UNLCK(解锁) */
off_t l_start; /* 相对于l_whence的偏移(字节单位) */
short l_whence; /* SEEK_SET, SEEK_CUR, or SEEK_END */
off_t l_len; /* 锁定长度,0表示锁定到文件结束 */
pid_t l_pid; /* 用于 F_GETLK */
};
cmd:
F_GETLK: 获得锁状态
F_SETLK: 非阻塞获得锁失败返回EAGAIN
F_SETLKW: 阻塞获得锁, 可被信号唤醒。
记录锁用法说明:
1) 当进程终止或关闭描述符的时候,锁自动释放。
a. fd1 = open (pathname, ...); b. fd1 = open (pathname, ...);
read_lock(fd1, ...); read_lock(fd1, ...);
fd2 = dup(fd1); fd2 = open (pathname, ...)
close(fd2); close(fd2);
2) 当fork子进程的时候,锁不被继承。
3) 当执行exec的时候锁不释放。设置了close-on-exec的除外
4) 有Advisory和Mandatory锁
系统中默认是Advisory锁, 当一个进程获得锁进行读写的时候,并不能阻止其它进行不去获得锁直接进行读写, 为了保证操作的正确性, 所有操作文件的进程必须按照规则, 首先获得锁 然后才能进行读写。
Mandatory 锁保证了当一个进程获得锁进行读写得时候, 其它进行直接读写文件会失败。