UNIX再学习 -- 线程

来源:互联网 发布:贝贝看图软件 编辑:程序博客网 时间:2024/05/22 11:56

终于要讲到线程部分,线程和进程让人够头痛的内容。

一、线程概念

老样子,我们还是按我们讲进程时的方式说起,参看:UNIX再学习 -- 进程环境
首先需要了解下,什么是线程。
Linux 下的线程,可能我们比较陌生,但是我们一直在玩 Windows 系统。应用程序文件、任务管理器,这些东西应该是很溜的。比如:


查看详细信息,最上一列,右击选择列;找到线程打对勾。如下图,就可看到线程数了。





通过上图我们可以看到 一个进程中有多个线程。但是到底什么是线程呢?
参看:进程与线程的一个简单解释  
大神举得例子很有意思。
进程好似车间,线程好似车间工人;任务是有很多工人协同完成;车间内的空间设施工人都是共享的;有些房间比如厕所一次只能容纳一人,进入需加锁(互斥锁)。

进程,是资源分配单位。线程,是CPU调度基本单位。
线程就是程序的执行路线,即进程内部的控制序列,或者说是进程的子任务。
一个进程可以同时拥有多个线程,即同时被系统调度的多条执行路线,但至少要有一个主线程。


一个进程的所有线程都共享进程的代码区、数据区、堆区(注意没有栈区)、环境变量和命令行参数、文件描述符、信号处理函数、当前目录、用户 ID 和组 ID 等。
一个进程的每个线程都拥有独立的 ID、寄存器值、栈内存、调度策略和优先级、信号掩码、errno变量以及线程私有数据等。
也可以说线程是包含在进程中的一种实体。它有自己的运行线索,可完成特定任务。可与其他线程共享进程中的共享变量及部分环境。可通过相互之间协同来完成进程所要完成的任务。
之前有转载一篇文章,可当扩展来看,参看:进程与线程及其区别

二、POSIX 线程

早期 UNIX 厂商各自提供私有的线程库版本,无论是接口还是实现,差异都非常大,代码移植非常困难。
我们将要讨论的线程接口来自 POSIX.1-2001。线程接口也称为“pthread”或“POSIX 线程”
POSIX 线程的功能测试宏是 _POSIX_THREADS。应用程序可以把这个宏用于 #ifdef 测试,从而在编译时确定是否支持线程,也可以把 _SC_THREADS 常数用于调用 sysconf 函数,进而在运行时确定是否支持线程。遵循 SUSv4 的系统定义符号 _POSIX_THREADS 的值为 200809L
查看 /usr/include/i386-linux-gnu/bits/posix_opt.h 可以看到 _POSIX_THREADS 的定义
 69 /* Tell we have POSIX threads.  */ 70 #define _POSIX_THREADS  200809L
使用 pthread 需要包含一个头文件:pthread.h
同时连一个共享库:libpthread.so  即 gcc 编译时加选项 -lpthread
#include <pthread.h>gcc ... -lpthread

三、线程标识

就像每个进程有一个进程 ID 一样,每个线程也有一个线程 ID。进程 ID 在整个系统中是唯一的,但线程 ID 不同,线程 ID 只有在它所属的进程上下文中才有意义。
线程 ID 是用 pthread_t 数据类型表示的.
查看 /usr/include/i386-linux-gnu/bits/pthreadtypes.h 可以看到 pthread_t 类型为无符号长整型
typedef unsigned long int pthread_t;
实现的时候可以用一个结构来代表 pthread_t 数据类型,所以可移植的操作系统实现不能把它作为整数处理。
因此必须使用一个函数来对两个线程 ID 进行比较。
参看:Pthreads and Semaphores

1、函数 pthread_equal

#include <pthread.h>int pthread_equal(pthread_t t1, pthread_t t2);返回值:若相等,返回非 0 数值;否则,返回 0

(1)函数功能

比较线程 ID

(2)示例说明

//示例一#include <stdio.h>  #include <stdlib.h>  #include <pthread.h>    int main(){      pthread_t thread_id;        thread_id=pthread_self(); // 返回调用线程的线程ID      printf("Thread ID: %lu.\n",thread_id);        if (pthread_equal(thread_id,pthread_self()))       {          printf("Equal!\n");       } else {          printf("Not equal!\n");      }      return 0;  }  编译:# gcc test.c -lpthread输出结果:Thread ID: 3075704512.Equal!
//示例二#include <stdio.h>#include <pthread.h>#include <stdlib.h>#include <string.h>#include <unistd.h>void* routine1 (void* arg){    pthread_t thread = pthread_self ();    printf ("子线程1 ID:%lu\n", thread);        return (void*)thread;}void* routine2 (void* arg){    pthread_t thread = pthread_self ();    printf ("子线程2 ID:%lu\n", thread);        if (pthread_equal (thread, (pthread_t)arg))        printf ("两个线程ID相同\n");    else        printf ("两个线程ID不同\n");       return (void*)thread;}int main(){    pthread_t thread1;    int error = pthread_create (&thread1, NULL, routine1, NULL);    if (error)    {        perror ("pthread_create");        exit (EXIT_FAILURE);    }    pthread_join (thread1, NULL);    pthread_t thread2;    error = pthread_create (&thread2, NULL, routine2, (void*)&thread1);    if (error)    {        perror ("pthread_create");        exit (EXIT_FAILURE);    }    //sleep (1);pthread_join (thread2, NULL);    return 0;}输出结果:子线程1 ID:3076029248子线程2 ID:3076029248两个线程ID不同

(3)示例解析

pthread_equal 函数比较两个线程 ID,这个没什么可讲的。 
示例二是我讲完以后又添加的,创建了两个子线程。它们的线程 ID 不同,可以理解。
我要说的是 sleep 和 pthread_join 有什么区别呢? 还有为什么在线程中使用 usleep 不合适? 
留个疑问,后续我们讲多线程会一起讲到的!!

2、函数 pthread_self

#include <pthread.h>pthread_t pthread_self(void);返回值:调用线程的线程 ID

(1)函数功能

获取线程自身的 ID

(2)示例说明

当线程需要识别以线程 ID 作为标识的数据结构时,pthread_self 函数可以与 pthread_equal 函数一起使用,如上例
#include <pthread.h>#include <stdio.h> void* thread_func(void *arg){    printf("thread id=%lu\n", pthread_self());    return arg;} int main(void){    pid_t pid;    pthread_t tid;    pid = getpid();    printf("process id=%lu\n", pid);    pthread_create(&tid, NULL, thread_func, NULL);    pthread_join(tid,NULL);    return 0;}输出结果:process id=2933thread id=3076057920

(3)示例解析

创建线程,查看线程自身的线程 ID

四、线程创建

1、函数 pthread_create

#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,                          void *(*start_routine) (void *), void *arg);返回值:若成功,返回 0;否则,返回错误编号

(1)函数功能

创建新线程

(2)参数解析

thread:输出线程 ID。pthread_t 即 unsigned long int。
attr:线程属性,NULL 表示缺省属性。pthread_attr_t 可能是整型也可能是结构体,因实现而异。
start_routine:线程过程函数指针。参数和返回值的类型都是 void*。启动线程过程其实就是调用一个函数,只不过是在一个独立的线程中调用,函数一旦返回,线程即告结束。
arg:传递给线程过程函数的参数。线程过程函数的调用者是系统内核,因此需要预先将参数存储到系统内核中。

(3)函数解析

main 函数可以被视为主线程的线程过程函数。main函数一旦返回,主线程即告结束。主线程一旦结束,进程即告结束。进程一旦结束,其所有的子线程统统结束。
应设法保证在线程过程函数执行期间,传递给它的参数 arg 所指向的目标持久有效。

注意,pthread 函数在调用失败时通常会返回错误码,它们并不像其他的 POSIX 函数一样设置 errno。每个线程都提供 errno 的副本,这只是为了与使用 errno 的现有函数兼容。子啊线程中,从函数中返回错误码更为清晰整洁,不需要依赖那些随着函数执行不断变化的全局状态,这样可以把错误的范围限制在引起出错的函数中。

(4)示例说明 

#include <stdio.h>#include <stdlib.h>#include <pthread.h>void *task (void* arg){printf ("进入子线程\n");sleep (5);printf ("子线程进程ID是:%lu\n", getpid ());printf ("22子线程ID是:%lu\n", pthread_self ());printf ("退出子线程\n");}int main (void){printf ("主线程启动\n");pthread_t thread;int error = pthread_create (&thread, NULL, task, NULL);if (error)perror ("pthread_create"), exit (1);sleep (10);printf ("退出主线程\n");printf("11子线程ID是:%lu\n",thread);printf ("主线程进程ID是:%lu\n", getpid ());printf("主线程ID是:%lu\n",pthread_self());return 0;}编译:# gcc test.c -lpthread输出结果:主线程启动进入子线程子线程进程ID是:311722子线程ID是:3076180800退出子线程退出主线程11子线程ID是:3076180800主线程进程ID是:3117主线程ID是:3076183744

(5)示例解析

主线程需要等待子线程结束后再结束,如果不等待不如把上例的 sleep (10); 注释掉,则不执行子线程。
子线程和主线程是同一个进程,thread 为输出子线程 ID,pthread_self函数可以得到线程自身的线程ID
再有 pthread 函数在调用失败时通常会返回错误码。

五、线程的等待

1、函数 pthread_join

#include <pthread.h>int pthread_join(pthread_t thread, void **retval);返回值:成功返回 0,失败返回错误号

(1)函数功能

主要用于等待一个线程的结束,并且回去退出码

(2)参数解析

第一个参数:线程的 ID
第二个参数:二级指针,用于获取线程的退出码

(3)函数解析

该函数根据参数 thread 指定的线程进程等待,将目标线程终止时的退出状态信息拷贝到 *retval 这个参数指定的位置上。
pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是 joinable 的。

(4)示例说明

//示例一#include <stdio.h>#include <pthread.h>void *task (void *p){//ps 指向只读常量区 ps本身在栈区char *ps = "hello";return ps;}int main (void){//启动一个线程pthread_t tid;pthread_create (&tid, NULL, task, NULL);//主线程等待子线程的处理,并且获取返回值char *pc = NULL;//pc 指针指向了 上面字符串的首地址pthread_join (tid, (void**)&pc);printf ("pc = %s\n", pc);return 0;}输出结果:pc = hello
//示例二//使用pthread_join函数获取线程的返回值#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <pthread.h>void* task(void* p){int i = 0;//静态局部变量,生命周期变长static int sum = 0;for(i = 1; i <= 100; i++){sum += i;}return (void*)∑}int main(void){//1.启动一个线程pthread_t tid;pthread_create(&tid,NULL,task,NULL);//2.主线程进行等待,获取返回值int* pi = NULL;pthread_join(tid,(void**)&pi);printf("子线程返回的数据是:%d\n",*pi);return 0;}输出结果:子线程返回的数据是:5050

(5)示例解析

上例中主要需要注意的是 pthread_join 获取返回值。
从线程过程函数中返回值的方法:
线程过程函数将所需返回的内容放在一块内存中,返回该内存的地址,同时保证这块内存,在函数返回即线程结束以后依然有效。
若 retval 参数非 NULL,则 pthread_join 函数将线程过程函数所返回的指针,拷贝到该参数所指向的内存中。
若线程过程函数所返回的指针指向动态分配的内存,则还需保证在用过该内存之后释放它。

2、函数 pthread_detach

#include <pthread.h>int pthread_detach(pthread_t thread);返回值:成功返回 0;失败返回一个错误码

(1)函数功能

主要用于将参数指定的线程标记为分离状态,对于分离状态的线程来说:当该线程终止后,会自动将资源释放给系统,不需要其他线程的加入/等待,也就是说分离的线程无法被其他线程使用 pthread_join 进行等待
建议:对于新启动的线程来说,要么使用 pthread_detach 设置为分离状态,要么使用 pthread_join 设置为可加状态。

(2)示例说明

#include <stdio.h>#include <pthread.h>void *task (void *p){int i = 0;for (i = 0; i <= 10; i++)printf ("子线程中:i = %d\n", i);}int main (void){//启动一个子线程,打印1~10之间的数pthread_t tid;pthread_create (&tid, NULL, task, NULL);//设置子线程为分离的状态pthread_detach (tid);//主线程进行等待,然后打印1~10之间的数pthread_join (tid, NULL);int i = 0;for (i = 1; i <=10; i++)printf ("主线程中:i = %d\n", i);return 0;}输出结果:主线程中:i = 1主线程中:i = 2主线程中:i = 3主线程中:i = 4主线程中:i = 5主线程中:i = 6主线程中:i = 7主线程中:i = 8主线程中:i = 9主线程中:i = 10

(3)示例解析

我们上面有讲到,如果使用 pthread_join 等待,则等待子线程完成后打印主线程1~10之间的数。
但是使用了 pthread_detach 分离,则直接打印主线程1~10之间的数,且 pthread_join 等待也会失效。

六、线程终止

如果进程中的任意线程调用了 exit、_Exit 或者 _exit,那么整个进程就会终止。与此相比类似,如果默认的动作是终止进程,那么发送到线程的信号就会终止整个进程。
单个线程可以通过 3 种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。
(1)线程可以简单地从启动例程中返回,返回值是线程的退出码。
(2)线程可以被同一进程中的其他线程取消。
(3)线程调用 pthread_exit。

1、我们讲一下第三种调用 pthread_exit。

#include <pthread.h>void pthread_exit(void *retval);

(1)函数功能

主要用于终止正在运行的线程通过参数 retval 来带出线程的退出状态信息
在同一个进程中的其他线程可以通过调用 pthread_join 函数来获取退出状态信息

(2)函数解析

在线程过程函数或者被线程过程函数直接或间接调用的函数中,调用 pthread_exit 函数,其效果都与在线程过程函数中执行 return 语句效果一样 -- 终止调用线程。
注意,在任何线程中调用 exit 函数,被终止的都是进程。当然随着进程的终止,隶属于该进程的包括调用线程在内的所有线程也都一并终止。

(3)示例说明

//示例一#include <stdio.h>#include <pthread.h>#include <stdlib.h>void *task (void *p){int i = 0;   for (i = 1; i < 100; i++){if (i == 10){//return (void*)i;pthread_exit ((void*)i);//exit (100);}printf ("子线程中:i = %d\n", i);}}int main (void){pthread_t tid;pthread_create (&tid, NULL, task, NULL);int res = 0;pthread_join (tid, (void**)&res);printf ("res = %d\n", res);return 0;}编译:# gcc test.c -lpthread输出结果:子线程中:i = 1子线程中:i = 2子线程中:i = 3子线程中:i = 4子线程中:i = 5子线程中:i = 6子线程中:i = 7子线程中:i = 8子线程中:i = 9res = 10
//示例二#include <stdio.h>#include <pthread.h>#include <stdlib.h>#include <string.h>#include <unistd.h>void circle_area (double r){    double* s = malloc (sizeof (double));    *s = 3.14 * r * r;        pthread_exit (s);}void* start_routine (void* arg){    circle_area (*(double*)arg);    printf("此句将不被执行");    return NULL;}int main(){    double r = 10.0;    pthread_t thread;    int error = pthread_create (&thread, NULL, start_routine, (void*)&r);    if (error)    {        perror ("pthread_create");        exit (EXIT_FAILURE);    }        double* s;    if (pthread_join (thread, (void**)&s))    {        perror ("pthread_join");        exit (EXIT_FAILURE);    }        printf ("圆面积:%g\n", *s);    free (s);    return 0;}输出结果:圆面积:314

(4)示例解析

该示例说明了,在任何线程中调用 exit 函数,被终止的都是进程。进程终止可其他线程就就同样终止了。
使用 pthread_join 获取退出状态信息,return 可以返回退出状态信息,pthread_exit 同样也可以返回退出状态信息。

七、线程取消

1、函数 pthread_cancel

#include <pthread.h>int pthread_cancel(pthread_t thread);返回值:成功返回 0;失败返回错误码

(1)函数功能

主要用于对参数指定的线程发送取消的请求

(2)函数解析

该函数只是向线程发出取消请求,并不等于线程终止。缺省情况下,线程在收到取消请求以后,并不会立即终止,而是仍继续运行,直到其达到某个取消点。在取消点处,线程检查其自身是否已被取消,若是则立即终止。
当线程调用一些特定函数时,取消点会出现。

2、函数 pthread_setcancelstate

#include <pthread.h>int pthread_setcancelstate(int state, int *oldstate);返回值:成功返回 0,失败返回错误码

(1)函数功能

主要用于设置新的取消状态,返回之前的取消状态

(2)参数解析

state:取消状态,可取以下值
    PTHREAD_CANCEL_ENABLE  接受取消请求(缺省)
    PTHREAD_CANCEL_DISABLE  忽略取消请求
oldstate:输出原取消状态,可取 NULL

3、函数 pthread_setcanceltype

#include <pthread.h>int pthread_setcanceltype(int type, int *oldtype);返回值:成功返回 0,;失败返回错误码

(1)函数功能

主要用于设置新的取消类型,获取之前的取消类型

(2)参数解析

type:取消类型,可取以下值
    PTHREAD_CANCEL_DEFERRED  延迟取消(缺省)
        被取消线程接收到取消请求之后并不立即终止,而是一直等到执行了特定的函数(取消点)之后再终止。
    PTHREAD_CANCEL_ASYNCHRONOUS  异步取消
        被取消线程可以在任意时刻终止,而不是非得遇到取消点。
oldtype:输出原取消类型,可取 NULL

4、示例说明

//线程取消函数的使用#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <pthread.h>void* task(void* p){//设置允许被取消pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);//设置为立即取消pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);while(1){printf("I am superman!\n");sleep(1);}}void* task2(void* p){printf("开始取消线程...\n");sleep(5);printf("取消线程结束\n");pthread_cancel(*(pthread_t*)p);}int main(void){//1.启动一个新线程,不断进行打印pthread_t tid;pthread_create(&tid,NULL,task,NULL);//2.启动另外一个新线程,负责取消上述线程pthread_t tid2;pthread_create(&tid2,NULL,task2,&tid);//3.主线程等待子线程的结束pthread_join(tid,NULL);pthread_join(tid2,NULL);return 0;}编译:# gcc test.c -lpthread输出结果:开始取消线程...I am superman!I am superman!I am superman!I am superman!I am superman!取消线程结束

5、示例解析

创建了两个线程,线程 2 负责取消线程 1。而设置取消状态为允许被取消,取消类型是立即取消。

八、线程清理处理程序

1、函数 pthread_cleanup_push、pthread_cleanup_pop

#include <pthread.h>void pthread_cleanup_push(void (*routine)(void *), void *arg);void pthread_cleanup_pop(int execute);

(1)函数解析

与进程在退出时可用 atexit 函数安排退出时类似的,线程也可以安排它退出时需要调用的函数。这样的函数就称为 线程清理处理程序。一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说,它们的执行顺序与它们注册时相反。

(2)参数解析

当线程执行以下动作时,清理函数 routine 是由 pthread_cleanup_push 函数调度的,调用时只有一个参数 arg:
调用 pthread_exit 时;
响应取消请求时;
用非零 execute 参数设置 0,清理函数将不被调用。
不管发生上述哪种情况,pthread_cleanup_pop 都将删除 pthread_cleanup_push 调用建立的清理处理程序。

(3)示例说明

#include "apue.h"#include <pthread.h>voidcleanup(void *arg){printf("cleanup: %s\n", (char *)arg);}void *thr_fn1(void *arg){printf("thread 1 start\n");pthread_cleanup_push(cleanup, "thread 1 first handler");pthread_cleanup_push(cleanup, "thread 1 second handler");printf("thread 1 push complete\n");if (arg)return((void *)1);pthread_cleanup_pop(0);pthread_cleanup_pop(0);return((void *)1);}void *thr_fn2(void *arg){printf("thread 2 start\n");pthread_cleanup_push(cleanup, "thread 2 first handler");pthread_cleanup_push(cleanup, "thread 2 second handler");printf("thread 2 push complete\n");if (arg)pthread_exit((void *)2);pthread_cleanup_pop(0);pthread_cleanup_pop(0);pthread_exit((void *)2);}intmain(void){interr;pthread_ttid1, tid2;void*tret;err = pthread_create(&tid1, NULL, thr_fn1, (void *)1);if (err != 0)err_exit(err, "can't create thread 1");err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);if (err != 0)err_exit(err, "can't create thread 2");err = pthread_join(tid1, &tret);if (err != 0)err_exit(err, "can't join with thread 1");printf("thread 1 exit code %ld\n", (long)tret);err = pthread_join(tid2, &tret);if (err != 0)err_exit(err, "can't join with thread 2");printf("thread 2 exit code %ld\n", (long)tret);exit(0);}编译:# gcc test.c -lpthread输出结果:thread 2 startthread 2 push completecleanup: thread 2 second handlercleanup: thread 2 first handlerthread 1 startthread 1 push completethread 1 exit code 1thread 2 exit code 2

(4)示例解析

两个线程都正确的启动和退出了,但是只有第二个线程的清理处理程序被调用了,而区别是一个是 return 返回,一个为调用 pthread_exit。因此,可以看出如果线程是通过从它的启动例程中返回而终止的话,它的清理处理程序就不会被调用。还要注意,清理处理程序时按照与它们安装时相反的顺序被调用的。
0 0
原创粉丝点击