多线程(1)

        之前我们学习了进程,现在我们又引入了一个新的概念线程,那线程又是什么呢?它和进程间的区别又有哪些呢?本篇博客就介绍了线程的相关知识。让我们一起来看看吧!

线程的概念

什么是线程呢?

    在一个程序里的执行路线就叫做线程(thread)。更准确的来说,线程是一个进程内部的控制序列。。一切进程至少都有一个执行线程。

    简言之,线程是代码的一个执行流。

进程和线程的区别

1、进程是资源竞争的基本单位;线程是程序执行的基本单位。

2、线程共享进程的数据,但是也有自己的一部分数据。

3、线程强调的是资源共享,因为与主线程共享PCB;进程强调的是独立性,因为每一个独立的进程各有各自独立的PCB。

4、正因为如此,创建一个线程的开销要比创建一个进程的开销要小。

5、多线程程序,只要一个程序出现异常,其他线程都会受到影响,从而导致程序出错,多进程程序,各程序之间相互独立,不会被彼此影响。

一个进程的多个线程共享

同一个地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个 全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境;

文件描述符

每种信号的处理方式

当前工作目录

用户id和组id

进程和线程的关系图如下图所示:


现在我们来看一下线程有什么优点和缺点

优点:

1、创建一个线程的代价要比创建一个进程小得多

2、与进程之间的切换相比,线程之间的切换瑶瑶操作系统做的工作要少很多

3、线程占用的资源要比进程小得多

4、能中分利用多处理器的可并行数量

5、在等待 慢速I/O操作结束的同时,程序可执行其他的计算任务

6、计算密集型应用,为了能够在多处理器系统上运行,将计算分解到多个线程中实现。

7、I/O密集型应用,为了提高性能,将I/O操作重叠。县城可以同时等待不同的I/O操作。

缺点:

-->性能损失

    一个很少被外部事件阻塞的计算机密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失是指增加了额外的同步和调度开销,而可用的资源不变。

-->健壮性降低

-->缺乏访问控制

    进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

-->编程难度提高

    编写与调试一个多线程程序比单线程程序困难得多。

线程控制

POSIX线程库

1、与线程有关的函数构成了一个完整的系列,大多数函数的名字都是以“pthread_”打头

2、使用这些函数库,要通过入头文件<pthread.h>

3、链接这些线程函数库时要使用编译器命令的“-lpthread”选项

创建一个线程:

功能:创建一个新的线程

原型:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

  参数:

    thread:返回线程ID

    attr:设置线程的属性,attr为NULL表示使用默认属性

    start_routine:是个函数地址,线程启动后要执行的函数
    arg:传给线程启动函数的参数

返回值:成功返回0;失败返回错误码

错误检查:

传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小。

现在我们来练习一下这个函数:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* thread_run(void* arg)
{
    while(1)
    {   
        printf("thread is run...\n");
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,thread_run,NULL);

    if(ret!=0)
    {
        perror("pthread_create");
        return -1;
    }
    while(1)
    {
        printf("I am running\n");
        sleep(1);
    }
    return 0;
}

结果如下:


首先我们来看一下Makefile里边的代码:


我们需要注意到

gcc -o $@ $^ -lpthread这里我们加了 -lpthread ,是因为与线程有关的函数绝大多数名字都是以“pthread_”打头的,要使用这些函数,除了要引入相应的头文件,链接这些线程函数库时要使用编译器命令的-lpthread选项。

进程ID和线程ID

    在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢?
Linux内核引入了线程组的概念。
    多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID现在介绍的线程ID。

struct task_struct {
    ...
    pid_t pid;
    pid_t tgid;
    ...
    struct task_struct *group_leader;
    ...
    struct list_head thread_group;
    ...
};

不同于pthread_t 类型的线程ID,和进程ID一样,线程ID是pid_t类型的变量,而且是用来唯一标识线程的一个整型变量。如何查看一个线程的ID呢?

这里我们就要来学习一条命令:

ps -eLf |head -1 && ps -eLf |grep a.out |grep -v grep 1

ps命令中的-L选项,会显示以下信息:

LWP:线程ID,即gettid()系统调用的返回值。

NLWP:线程组内线程的个数

Linux提供了gettid系统调用返回其线程ID,可是glibc并没有将该系统调用封装起来,在开放接口来共程序员使用,如果确实有需要获得线程ID。可以采用以下方法:

#include <sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);

现在我们对之前的代码进行修改,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/syscall.h>
void* thread_run(void* arg)
{
    while(1)
    {   
        printf("thread is run...,tid = %d\n",syscall(SYS_gettid));
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,thread_run,NULL);
    if(ret!=0)
    {
        perror("pthread_create");
        return -1;
    }
    while(1)
    {
        printf("I am running\n");
        sleep(1);
    }
    return 0;
}

结果如下:


这时我们用之前提到过的命令查看线程ID:


我们可以看出上面的pthread是多线程的,进程ID为5593,进程内有两个线程,线程ID分别为5593、5594。


    从上面可以看出,pthread进程的ID为5593,下面一个线程的ID也是5593,这不是巧合。线程内的一个线程,用户态被称为主线程(main) ,在内核中被称为group leader,内核在一个线程时,会将线程组的ID的值设置成一个线程的线程ID,group_leader指针则指向自⾝,既主线程的进程描述符。所以线程组内存一个线程ID等于进程而该线程即为线程组的主线程。

线程ID及进程地址空间布局

线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:

pthread_t pthread_self(void);

让我们来看看线程如何获取自己的线程ID呢?首先我们队之前的代码进行修改:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* thread_run(void* arg)
{
    while(1)
    {   
        printf("thread is run...,tid = %d\n",pthread_self());
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,thread_run,NULL);
    if(ret!=0)
    {
        perror("pthread_create");
        return -1;
    }
    while(1)
    {
        printf("I am running\n");
        sleep(1);
    }
    return 0;
}

结果如下:


    这里我们解释一下,获得的线程ID数字很奇怪,那是因为pthread_ create函数会产⽣一个线程ID,存放在一个参数指向的地址中。该线程ID和前面所说的线程ID不是一回事。
前面所讲的线程ID属于进程调度的范畴。因为
线程是轻量级进程,是操作系统调度器的最小单位,以一个数值来一示该线程。

    pthread_ create函数生并标记在一个参数指向的地址中的线程ID中,属于NPTL线程库的畴。线程库的后续操作,就是根据该线程ID来操作线程的。可以注意到,pthread_self()函数返回值得类型是pthread_t,对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

线程终止

如果只需要终止某一个线程而不是终止整个进程,我们有三种方法:

1、从线程函数return。这种方法对主线程不适用,从main函数return相当于调用了exit。

2、线程可以调用pthread_exit终止自己。

3、一个线程可以调用pthread_acancel终止同一个进程中的另一个线程。

pthread_exit函数

功能:线程终止

原型

void pthread_exit(void *retval);参数

参数  value_ptr:value_ptr不要指向同一个局部变量

返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。

    这里我们需要注意的是,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程已经退出了。

pthread_cancel函数

功能:取消一个执行中的线程

原型

int pthread_cancel(pthread_t thread);

参数  thread:线程ID

返回值:成功返回0,失败返回错误码。

线程等待与分离

    线程等待

为什么需要线程等待呢?那是因为已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。而且创建的新线程不会复用刚才退出的线程的地址空间。

功能:等待线程结束

原型

int pthread_join(pthread_t thread,void **value_ptr);

参数  thread:线程ID

        value_ptr:它指向一个指针,后者指向线程的返回值

返回值:成功返回0,失败返回错误码。

调用该函数的线程将挂起等待,知道ID为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,我们总结如下:

1、如果线程通过return返回,retval所指向的单元里存放的是线程函数的返回值

2、如果线程被别的线程调用pthread_cancel异常终止,retval所指向的单元里存放的是常数PTHREAD_CANCELED/

3、如果线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给 pthread_exit的参数

4、如果对线程的终止状态不感兴趣,可以直接传NULL

现在我们来测试一下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* run1(void* arg)
{
    printf("thread1 running...\n");
    int *p=(int *)malloc(sizeof(int));
    *p=1;
    return (void*)p;
}

void* run2(void* arg)
{
    printf("thread2 running...\n");
    int *p=(int *)malloc(sizeof(int));
    *p=2;
    pthread_exit(p);
}
void* run3(void* arg)
{
    while(1)
    {
        printf("thread3 running...\n");
        sleep(1);
    }
    return NULL;
}

int main()
{
    pthread_t t1,t2,t3;
    void *ret; 
    //创建线程1,利用return终止
    pthread_create(&t1,NULL,run1,NULL);
    pthread_join(t1,&ret);
    printf("thread1 return,id:%d,return code:%d\n",t1,*(int*)ret);
    free(ret);

    //创建线程2,利用pthread_exit终止
    pthread_create(&t2,NULL,run2,NULL);
    pthread_join(t2,&ret);
    printf("thread2 return,id:%d,return code:%d\n",t2,*(int*)ret);
    free(ret);

    //创建线程3,利用pthread_cancel终止
    pthread_create(&t3,NULL,run3,NULL);
    sleep(3);
    pthread_cancel(t3);
    pthread_join(t3,&ret);
    if(ret== PTHREAD_CANCELED )
        printf("thread3 return,id:%d,return code: PTHREAD_CANCELED\n",t3);
    return 0;
}

结果如下:

    线程分离

    默认情况下,线程的属性是joinable可结合的,一般当线程终止后,其终止状态就会一直保留,直到其它线程调用pthread_join获取它的状态为止。 但是线程也可以被置为detach分离状态,这样的线程一旦终止就立刻回收它占用的所有资源, 而不保留终止状态。

可以是线程组内其它线程对目标线程进行分离,也可以是线程自己分离

int pthread_detach(pthread_t thread);

pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不可能即是joinable又是分离的。我们来测试一下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* thread_run(void* arg)
{
    pthread_detach(pthread_self());
    printf("thread running...\n");
    sleep(1);
    return NULL;
}

int main()
{
    pthread_t t1;
    pthread_create(&t1,NULL,thread_run,NULL);
    sleep(1);

    int ret = pthread_join(t1,NULL);
    if(ret!=0)
    {
        printf("wait failed\n");
    }
    else
    {
        printf("wait sucess\n");
    }
    return 0;
}

结果如下:


猜你喜欢

转载自blog.csdn.net/lu_1079776757/article/details/79868627