《Linux应用多线程(一) — 线程的概念及实现》

1.线程的定义

  线程:线程是进程中的一个独立的代码块。说白了,其实它就是个函数,只不过再也不用像以前的函数调用来调用它。而是通过pthread_create函数来创建它,也就是告诉内核,这个函数是个线程,今后交给你来调度了。

  如果从以上的定义来看,那么很明显,线程是拥有自己的栈空间(局部变量),但是共享全局变量、文件描述符等。

  注意:这边区别一下和fork系统调用创建出来的进程进行一下比较:fork创建出的是一个新的进程,因此他拥有自己的变量和PID,而不需要和父进程进行共享。

 

  Linux系统中线程的实现是基于pthread线程库的,pthread在不同的linux kernel版本中是不同的,大体上以linux 2.6为分界,linux 2.6以前的linux版本的线程库采用的是linux Threads线程库,linux 2.6开始的linux版本采用NTPL线程库。

  针对linux Threads线程库以及NTPL线程库就不过多的解释了,主要目前用的基本都是NTPL线程库就行了。

 

2.多线程编程的优缺点

对于大多数合作性任务,多线程比多个独立的进程更优越呢?

  这是因为,线程共享相同的内存空间。不同的线程可以存取内存中的同一个变量。所以,程序中的所有线程都可以读或写声明过的全局变量。如果曾用 fork() 编写过重要代码,就会认识到这个工具的重要性。为什么呢?虽然 fork() 允许创建多个进程,但它还会带来以下通信问题: 如何让多个进程相互通信,这里每个进程都有各自独立的内存空间。

  对这个问题没有一个简单的答案。虽然有许多不同种类的本地 IPC (进程间通信),但它们都遇到两个重要障碍:

  • 强加了某种形式的额外内核开销,从而降低性能。
  • 对于大多数情形,IPC 不是对于代码的“自然”扩展。通常极大地增加了程序的复杂性。

  由于所有的线程都驻留在同一内存空间,POSIX 线程无需进行开销大而复杂的长距离调用。只要利用简单的同步机制,程序中所有的线程都可以读取和修改已有的数据结构。而无需将数据经由文件描述符转储或挤入紧窄的共享内存空间。仅此一个原因,就足以让您考虑应该采用单进程/多线程模式而非多进程/单线程模式。

优点:

    1、无需跨进程边界

    2、程序逻辑和控制方式简单

    3、所有线程可以直接共享内存和变量等

    4、线程方式消耗的总资源比进程方式少

缺点:

    1、每个线程与主程序共用地址空间,受限于2GB地址空间

    2、线程之间的同步和加锁控制比较麻烦

    3、一个线程的崩溃可能影响到整个程序的稳定性

    4、到达一定的线程数程度后,即使再增加CPU也无法提高性能

    5、线程能够提高的总性能有限,而且线程数量较大时,线程本身的调度开销不小

 

3.线程函数

3.1 pthread_create

#include <pthread.h>

int pthread_create(pthread_t *thread, pthread_attr_t *attr, void*(*start_routine)(void*), void *arg);
作用:创建一个新的线程。
  • 第一个参数:只想pthread_t类型数据的指针,线程被创建时,这个指针所指向的变量将会被写入一个标识符,用于后面对线程的引用。
  • 第二个参数:用于设置线程的属性,如果不需要设置线程的属性,直接设置参数为NULL。
  • 第三个参数:执行的代码函数。
  • 第四个参数:传递给该函数的参数。
  • 返回值:成功返回0,失败返回错误代码

  线程创建时,并不能保证哪个线程会先运行。

实例:

#include <pthread.h>

pthread_t ntid;

void printfs(const char *s)
{
  pid_t pid;
  pthread_t tid;

  pid = getpid();
  tid = pthread_self();  //获取当前线程的ID
  printf("%s pid %lu tid %lu (0x%1x) \n", s,(unsigned long) pid, (unsigned long)tid, (unsigned long)tid);  
}

void *thr_fn(void *arg)
{
  printfs("new thread:");
  return((void *) 0);
}

int main(void)
{
  int err;
  err = pthread_create(&ntid, NULL, thr_fn, NULL);
  if(err != 0)
       err_exit(err, "can't create thread");
  printfs("main thread:"); 
  sleep(1); 
  exit(0);    
}

  pthread_create()函数中第一个参数所获取出的ID,其实是进程地址空间内的一个地址。

以上例程有两个需要注意的点:

  • 需要特别注意主线程和新线程的竞争(线程创建时,并不能保证哪个线程会先运行),所以目前这里采用的主线程休眠一秒,如果主线程不休眠,并且是主线程先运行,那么久会直接exit,新线程就没有运行的机会。
  • 新线程是通过pthread_self来获取自己的线程ID。而不是通过ntid。因为主线程通过pthread_create创建新线程,会把新线程的ID存在放第一个参数也就是ntid上。但是新建的线程并不能安全的使用它。理由是:如果新线程在主线程调用pthread_create返回之前就运行了。那么新线程看到的是未初始化的ntid内容,这个内容并不是正确的线程ID。

导致以上两个特别需要注意的原因都是线程创建时,并不能保证哪个线程会先运行。

prctl(PR_SET_NAME, "xxxx");
给线程起名字

  命名以后,既可以从 procfs 中获取到线程的名字,也可以从 ps 命令中得到线程的名字,这样就可以更好地辨识不同的线程。

  

3.2 pthread_exit

  如果进程中的任意线程调用了exit、_Exit或者_exit,那么整个进程就会终止(注意,不要在某个线程调用这几个退出函数,因此一旦某个线程调用,那么进程就会退出,而一个进程中通过有多个线程,这样会导致其他线程也一起退出)。

  单个线程在不终止整个进程的情况下可以通过三种方式退出:

  • 线程可以简单地从启动例程中返回(函数运行结束退出),返回值是线程的退出码(return)。
  • 线程可以被同一进程中的其他线程取消pthread_cancle()。
  • 线程调用pthread_exit。
#include <pthread.h>

void pthread_exit(void *retval)

  作用:终止调用它的线程并返回一个指向某个对象的指针(即void *retval)。

       value_ptr 是一个指针,存放线程的 “ 临终遗言 ” 。线程组内的其他线程可以通过调用 pthread_join 函数接收这个地址,从而获取到退出线程的临终遗言。如果线程退出时没有什么遗言,则可以直接传递NULL 指针。

  注意:绝不能用它返回一个指向局部变量的指针,因为线程调用该函数后,局部变量就将不存在,就会引起严重的程序问题。

3.3 pthread_join

#include <pthread.h>

int thread_join(pthread_t th, void **thread_return);
作用:等价于进程中用来收集子进程信息的wait函数。

  第一个参数:指定将要等待的线程。

  第二个参数:这个参数是一个指针,它指向另一个指针,后者指向线程的返回值。这边主要用于同步,即上面的pthread_exit结束完当前的线程会返回他参数里面的数据给pthread_join,然后在主线程中可以调用这个数据。

  当一个线程通过调用pthread_exit退出或者简单从启动例程中返回时,进程中的其他线程可以通过使用pthread_join函数获得该线程的退出状态。

  pthread_create和pthread_exit函数的无类型指针参数可以传递的值不止一个,这个指针可以传递包含复杂信息的结构的地址。但是注意,这个结构所使用的内存在调用者完成调用以后必须仍然有效。例如:在调用线程的栈上分配了该结构,那么其他的线程在使用这个结果的时候内存内容可能已经改变了。又比如:线程在自己的栈上分配了一个结构,然后把指向这个结构的指针传给pthread_exit,那么调用pthread_join的线程试图使用该结构时,这个栈有可能已经被撤销。

  由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放。pthread_join可以用于将当前线程挂起,等待线程的结束。这个函数是一个线程阻塞的函数,调用它的函数将一直被等待的线程结束为止,当函数返回时,被等待线程的资源就被收回。

3.4  pthread_cancel

#include <pthread.h>

int pthread_cancel(pthread_t tid);
返回值: 成功:0;  失败:返回错误编号

  线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。

  在默认情况下,pthread_cancel函数会使得由tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCELED的pthread_exit函数。但是,线程可以选择忽略取消或者控制如何被取消。需要特别注意的是:pthread_cancel并不等待线程终止,它仅仅提出请求。

 3.5 pthread_detach

#include <pthread.h>
int pthread_detach(pthread_t thread);

  可以是线程组内其他线程对目标线程进行分离,也可以是线程自己执行 pthread_detach 函数,将自身设置成已分离的状态,如下:

pthread_detach(pthread_self())

  作用:线程退出时,系统自动将线程相关的资源释放掉,无须等待连接。

  也可以在创建线程的时候,通过设置属性来分离。

#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate);
int pthread_attr_getdetachstate(pthread_attr_t *attr,int *detachstate);

 3.6 设置线程属性

  线程创建的第二个参数是 pthread_attr_t 类型的指针, pthread_attr_init 函数会将线程的属性重置成默认值。

pthread_attr_t attr;
pthread_attr_init(&attr);

   比较常用的是设置线程栈大小。默认情况下,线程栈的大小为 8MB

ulimit -s
8192

  一个线程需要分配 8MB 左右的栈空间,就决定了不可能无限地创建线程,在进程地址空间受限的 32 位系统里尤为如此。在 32 位系统下, 3GB的用户地址空间决定了能创建线程的个数不会太多。如果确实需要很多的线程,可以调用接口来调整线程栈的大小:(单位为KB,1MB = 1024KB)

#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr,size_t *stacksize);

 



 

 

 

 

  

 

posted @ 2020-06-23 10:21  一个不知道干嘛的小萌新  阅读(418)  评论(0编辑  收藏  举报