线程概念与线程创建

什么是线程? 为什么要有多线程?
       一家公司需要生产某种产品,然后为生产这种产品提供了各种原材料和几层楼的资源。而这件产品是有很多个零件组成的,各个零件需要的材料可能是不同的,即,有些零件之间的制造是不相互影响的。现在要生产一种产品,由A、B两种零件组成。公司分配了1、2、3这三层楼(2楼是用于生产的该产品的各种器械)用于生产该产品。假设加工零件A是将材料都准备好了放到2楼的机器里边,然后等上很长一段时间。在生产的时候可能产品负责人可能就是分派一部分人在1楼完成加工零件A的前期准备,然后再分配一部分人在3楼加工零件B,最后再将所有的零件组合起来。负责人肯定不会让大家一块先加工零件A,然后大家都在那闲等着机器操作,等着把零件A加工完成才开始零件B的加工,因为这样效率太低了。
       同样的,如果将执行一个进程比作生产一种产品,公司就好比操作系统,产品的原材料和提供的生产产地就是分配给进程的资源。为了让进程更合理的进行,将进程的分成多个执行流(加工多个零件),各个执行流相当于就是不同的函数。而这样的执行流其实就是线程,一个执行流就是一个线程。 这些执行流同属于一个进程(同是一个产品的一部分),且共享地址空间(都在那分配的那几层楼里)和共享资源(都要使用2楼的器械)。 实际的进程程比这个稍微要复杂一点,线程之间共享的东西远不止这些。 同一进程下的线程之间共享了哪些资源呢?
    1)、共享同一地址空间,因此Text Segment(代码段) 和 Data Segment(数据段)都是共享的,如果定义一个函数,在各线程都可以调用,如果定义一个全局变量,在各线程都可以访问到。
    2)、文件描述符表
    3)、每种信号的处理方式
    4)、当前工作目录
    5)、用户id 和 组id
但是,就像每个零件的原材料可能不同,每个线程都有自己独特的东西, 每个线程都有自己的线程id、一组寄存器(用于保存上下文信息的)、栈、errno、信号屏蔽字、调度优先级。
       总结一下,其实 线程是进程的一部分,描述指令流执行状态。它是进程中的指令执行流的最小单位,是CPU调度的基本单位。

线程标识:
       和每个进程都有一个进程ID一样,每个线程也有自己的ID。进程ID是一个非负整数,用pid_t来表示,线程ID用pthread_t 数据类型表示。由于在不同的平台,用于表示pthread_t的结构是不同的。而每个平台都有着自己的线程库,并在库里边实现了一些公有的接口。当我们在对线程ID进行一些操作时,为了程序的可移植性,我们需要利用这些公有的接口来实现。
    获取自身的线程ID
    #include <pthread.h>
    pthread_t  pthread_self ( void )

    比较两个线程ID是否相等
    #include <pthread.h>
    int pthread_equal( pthread_t tid1,pthread_t tid2 )
    相等返回非0值,不相等返回0。

线程创建:
当我们创建出一个进程时,该进程只有一个执行流,但是在进程的执行过程中,我们可以通过 pthread_create 函数来创建其他执行流,也就是创建其他线程,跟使用fork()创建一个新进程不一样,创建出来的新线程与原来的线程是没有父子关系的,他们之间的关系是平等的,被调用的顺序也是不确定的。

int pthread_create(pthread_t* thread, const pthread_attr*  attr,  void*  (*start_routine)(void*), void* arg)
参数: thread:返回线程ID
          attr:设置线程的属性,attr为NULL表示使用默认属性
          start_routine:是一个函数指针,创建一个线程是要为其安排执行的任务(函数)
          arg:传给线程启动函数的参数(即执行第三个参数指向的函数时所需要的参数)
          attr:设置线程的属性,attr为NULL表⽰示使⽤用默认属性
返回值:成功返回0;失败返回错误码

说明:
传统的一些函数是成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(大部分其他POSIX函数会这样做)而是通过返回值将错误码返回
pthreads同样也提供了线程内的全局变量errno,以支持其他使用errno的代码。

对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销要小。


#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
void* routine(void* arg)
{
    while(1)
    {
        printf("I'm new thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
        sleep(1);
    }
    return (void*) 1;
}
int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,routine,NULL);
    if(ret != 0)
    {
        fprintf(stderr,"pthread_create err:%s\n",strerror(ret));
        exit(EXIT_FAILURE);
    }
    while(1)
    {
        sleep(1);
        printf("I'm main thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
    }
    return 0;
}


该进程内的每一个线程的pid都是一样的,由此也可以证明这些线程同属于一个进程。
在此解释一下 LWP与线程ID的区别
这块比较绕,可能不太好理解。要有耐心~
       线程分为用户级线程和内核级线程,而我们创建的线程都是用户级线程。在Linux中,是没有真正意义上的线程的,线程都是通过pthread库模拟进程而实现出来的,而在CPU看来,就没有进程线程之分,都是一个个的PCB, 所以线程又被称为轻量级进程。 既然被认为是PCB,那么就有进程ID,而实际上LWP就是线程在被CPU认为是进程并为其分配的pid,这个pid在整个内核中都是唯一的而我们所说的线程id只是相对于用户级来说的,线程id只是在用户级唯一。事实上,线程id的实质就是在进程内部为其分配的地址空间的地址。
       多线程的进程,又被称为线程组,线程组内的每⼀一个线程在内核之中都存在⼀一个进程描述符 (task_struct)与之对应。进程描述符结构体中的pid,表⾯面上看对应的是进程ID,其实不然,它对应的是线程ID(也就是上边所说的LWP);进程描述符中的tgid,含义是Thread Group ID(即进程组ID),该值对应的是用户层面的进程ID。
         线程组 内的第一个线程,在⽤用户态被称为主线程(main thread),在内核中被称为group leader,内核在创建 第一个线程时,会将线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身, 既主线程的进程描述符。所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线 程。

线程的终止:
在进程中的任一线程调用了exit,_Exit、_exit或者主执行流(第一个执行流)return,都会    导致整个进程终止。与此类似,如果进程组中的某一线程收到一个默认处理动作是终止进程的信号,整个进程也会终止。
   
首先来验证一下在一个新线程中调用exit会不会使整个进程都退出。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void* routine(void* arg)
{
    int count = 3;
    while(count--)
    {
        printf("I'm new thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
        sleep(1);
    }
    exit(0);
}
int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,routine,NULL);
    if(ret != 0)
    {
        fprintf(stderr,"pthread_create err:%s\n",strerror(ret));
        exit(EXIT_FAILURE);
    }
    while(1)
    {
        sleep(1);
        printf("I'm main thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
    }
    return 0;
}

在此段程序中,新线程循环3次后调用exit退出,而主线程(第一个线程)一直在死循环。

当新线程循环完3次之后,整个进程都退出了。其他的就不一一验证了。
如果需要只终止某个线程而不终止整个进程,可以有三种⽅方法:
          1. 从线程函数return。这种⽅方法对主线程不适用,从main函数return相当于调用exit。
          2. 线程可以调用pthread_ exit终止自己。
          3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit函数  ----->线程终止
void pthread_exit(void *value_ptr);
参数: value_ptr是一个无类型的指针,不能向一个局部变量。进程中的其他线程可以通过pthread_jion函数来访问到该指针。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的(malloc分配空间是在堆上分配的),不能在线 程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel函数 -----> 取消一个执行中的线程
原型: int pthread_cancel(pthread_t thread);
参数: thread --- 线程ID
返回值:成功返回0;失败返回错误码。
pthrea_cancell函数会使得进程thread表现的如同调用PTRHEAD_CANCELED的pthread_exit函数,但是进程可以选择忽略取消方式或者控制取消方式。一个线程调用pthread_cancel函数去取消另一个线程,调用线程只是相当于只是提出请求,并不会等待线程被取消。

看一看使用pthread_exit函数退出会不会导致整个进程的退出。
将上边的调用exit函数改成调用pthread_exit函数


重新执行程序,发现,新线程循环3次之后就退出了,而主线程依然在执行,说明,调用pthread_exit函数可以使线程终止而进程不终止


验证一下使用pthread_cancel来终止一个正在运行的线程。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void* routine(void* arg)
{
    while(1)
    {
        printf("I'm new thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,routine,NULL);
    if(ret != 0)
    {
        fprintf(stderr,"pthread_create err:%s\n",strerror(ret));
        exit(EXIT_FAILURE);
    }
    sleep(3);
    pthread_cancel(tid);
    while(1)
    {
        sleep(1);
        printf("I'm main thread,pid is:%u tid is:%u\n",getpid(),(unsigned int)pthread_self());
    }
    return 0;
}

这段程序主线程先创建一个新线程,然后隔了3秒便使用pthread_cancel函数将其终止了,然后主线程在死循环。


新线程被终止,主线程依然在执行,说明进程还没有结束。

线程等待与分离

在多进程中父进程可以调用wait函数来获取子进程的退出信息,在多线程中,一个线程退出了也需要其他进程来等待,为什么线程也需要等待呢?

已经退出的线程,其空间还没有退出,仍然在进程的地址空间内。当再创建新的线程也不会复用刚刚才退出线程的空间。这就造成进程内部资源的浪费,相当于僵尸进程中的资源泄漏。同一进程内部的其他线程可以是通过pthread_jion函数获取线程的退出信息。

int pthread_jion(pthread thread,void** value_ptr);
       参数:thread为需要进行等待的线程id
                value_ptr是用于获取线程的退出信息的。
       返回值:等待成功返回0,失败返回错误码。
说明:调用pthread_jion函数的线程会一直阻塞,直到指定的线程调用pthread_exit退出或者从启动例程中返回或者被取消。如果是从启动例程中返回,那么value_ptr将包含返回码。如果是被取消了,那么,value_ptr指定的内存单元就会被置为PTHREAD_CANCELED(#define  PTHREAD_CANCELED  (void*  -1))。如果只是想要在指定线程结束后再执行,并不想知道线程的具体退出信息,调用的时候可以将第二个参数value_ptr设置为NULL。

线程的分离
       默认情况下,新创建的线程是可结合的(jionable),需要对其进行pthread_jion操作,否则无法释放资源,从而造成系统泄露。如果我们不关心线程的返回值,jion就变成了一种负担,为了减少这种负担,当我们认为线程的返回值不重要的时候,可以提前告诉系统,当线程退出时,自动释放资源。这就是线程的分离。
线程的分离是通过函数thread_detach实现的。
int pthread_detach(pthread_t  thread);
       参数:thread是要进行分离的线程的id。
       返回值:成功返回0,失败返回错误码。
线程分离可以是被动的(被同一个进程里边的其他线程分离),也可以是主动的(自己将自己分离)。pthread_detach ( pthread_self() );
jionable和分离是相互矛盾的,一个线程不能既是jionable的又是分离的。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_t ptid;

void* thread_run(void* arg)
{
	pthread_detach(pthread_self());
	printf("%s\n",(char*)arg);
	return (void*)0;
}

int main()
{
	int ret;
	ret = pthread_create(&ptid,NULL,thread_run,"new thread is running");
	if(ret != 0)
	{
		printf("can't create a thread\n");
		exit(0);
	}

	sleep(1);
	ret = pthread_join(ptid,NULL);
	if(ret == 0)
		printf("thread wait success\n");
	else
		printf("thread wait failure\n");
	return 0;
	}

上边这个例子在主线程里边创建一个新线程并使用pthread_join函数等待新线程,然后,让新线程自我分离。如果等待不成功,说明等待与分离是相互矛盾的。


结果表明等待失败,分离后的线程是不能被等待的,如果等待,一定会等待失败。

猜你喜欢

转载自blog.csdn.net/guaiguaihenguai/article/details/79904112