[TLPI] C30 Thread: Introduction

混江龙づ霸主 提交于 2019-11-28 22:30:24

Threads: Introduction


Overview

同一个进程内的所有线程相互独立运行,执行着相同的程序,所有的线程共享相同的内存,包括初始化过的数据、未初始化的数据以及堆区(一个传统的UNIX进程是一特殊多线程进程,该进程只包括一个线程)。

在Figure 29-1中有一些简化。在实际中,每个线程的栈区可能会混入共享库以及共享内存区,这取决于线程创建的顺序、共享库被加载的顺序、共享内存区被使用的顺序。此外,每个线程的栈区分布也和Linux发行版有关。

同一个进程内的线程可以并发执行。在一个多处理器系统上,多个线程可以并行执行。如果某个线程因为IO而阻塞,其他的线程依然能够执行。

相对于进程,线程提供了很多优势。试着想想传统的UNIX编程里,通过创建多个进程来实现并发。其中一个例子,是网络服务器的设计,当接收到来自客户端的连接请求后,父进程就通过fork()来创建一个独立的子进程来和各个客户端联系。这种设计下,同时处理多个客户端的连接变得可能。尽管这种设计在很多情况下是有用的,但是依然有如下几个方面的限制:

  • 在进程之间共享信息很困难。由于父进程和子进程不会共享内存(除了只读段),我们必须使用某种进程间通信方式来彼此交换信息。
  • 通过fork()创建进程的开销是很大的。即使通过copy-on-write技术,用来复制各种进程属性,比如页表以及文件描述符,的开销依然是很费时的。

线程正是为了解决这些问题的:

  • 在线程之间共享信息快速且方便。只需要将拷贝数据放入共享(全局或者堆)变量。然而,为了避免多个线程尝试更新相同的数据,我们需要引入同步技术。
  • 线程的创建要比进程创建快速地多。典型来说,要快十倍。(在Linux上,线程是通过clone()系统调用创建的)。线程创建之所以快,在于通过fork()创建进程时需要的很多属性在线程之间是共享的。具体来说,对内存页的copy-on-write复制不再需要,并且不需要再复制页表。

除了全局变量之外,线程依然有其他共享属性。包括有:

  • process ID and parent process ID
  • process group ID and session ID;
  • controlling terminal;
  • process credentials (user and group IDs);
  • open file descriptors;
  • record locks created using fcntl();
  • signal dispositions;
  • file system–related information: umask, current working directory, and root directory;
  • interval timers (setitimer()) and POSIX timers (timer_create());
  • System V semaphore undo (semadj) values (Section 47.8);
  • resource limits;
  • CPU time consumed (as returned by times());
  • resources consumed (as returned by getrusage()); and
  • nice value (set by setpriority() and nice()).

线程之间的区别在于:

  • thread ID (Section 29.5);
  • signal mask;
  • thread-specific data (Section 31.3);
  • alternate signal stack (sigaltstack());
  • the errno variable;
  • floating-point environment (see fenv(3));
  • realtime scheduling policy and priority (Sections 35.2 and 35.3);
  • CPU affinity (Linux-specific, described in Section 35.4);
  • capabilities (Linux-specific, described in Chapter 39); and
  • stack (local variables and function call linkage information).

Background Details of the Pthreads API

Thread Creation

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start)(void *), void *arg);
Returns 0 on success, or a positive error number on error

新创建的线程从strat指向的函数开始执行,并赋给其参数arg。创建新线程的线程则继续执行该语句后面的代码。

arg的类型是void *,意味着我们可以用一个指向任意类型的指针,来给start传参。通常来说,arg指向一个全局变量或者位于堆区的变量,当然也可以是NULL。如果需要给start传递多个参数,那么可以将arg指向一个结构体,which contains the arguments as separate fields.

start的返回值类似于类型void *

参数thread指向一个pthread_t类型的缓冲区,在pthread_create()返回之前,会将标识新线程的唯一的标识符复制进该缓冲区。

参数attr是一个指向pthread_arrt_t对象的指针,该对象规定了新线程的多种属性。如果attrNULL,那么新线程会具有各种默认属性。

当新线程创建完成之后,哪个线程先获得CPU是无法预测的。如果需要强行要求执行顺序,那么需要使用一些同步手段。

Thread Termination

线程结束:

  • start参数指向的函数进行了return,为线程明确了一个返回值
  • 线程主动调用pthread_exit()
  • 使用pthread_cancel()取消线程
  • 任意一个线程调用exit()或者主线程执行一个return语句,导致进程内的所有线程都被终止。

pthread_exit()函数将会终止calling thread。并且明确一个返回值,其他线程可以通过调用pthread_join()函数来获取该返回值。

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

实际上,调用pthread_exit()效果和在线程的start函数中执行一条return语句一样。

如果main thread调用了pthread_exit(),那么main thread结束之后,其他线程还会继续执行。

#include <assert.h>
#include <pthread.h>
#include <unistd.h>
#include <iostream>

static void *threadFun(void *) {
  sleep(5);
  std::cout << "threadFun Finished\n";
}

int main() {
  pthread_t t;
  assert(pthread_create(&t, NULL, threadFun, NULL) == 0);

  std::cout << "Main Exited\n";
  pthread_exit(NULL);
  //return 0;
}

Thread IDs

include <pthread.h>
pthread_t pthread_self(void);
//Returns the thread ID of the calling thread

pthread_equal()检查两个线程的ID是否一样。

include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
//Returns nonzero value if t1 and t2 are equal, otherwise 0

Joining with a Terminated Thread

include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//Returns 0 on success, or a positive error number on error

pthread_join函数等待thread参数标识的线程结束(如果thread线程已经结束,那么pthread_join()将会立刻返回)。如果retval参数是一个non-NULL指针,就说明它接收到了一个已终结线程返回值的复制,也即,已终结线程通过pthread_exit()或者调用return返回的返回值。

Calling pthread_join() for a thread ID that has been previously joined can lead to unpredictable behavior; for example, it might instead join with a thread created later that happened to reuse the same thread ID.

如果某个线程不是分离的(not detached),那么我们必须使用pthread_join()将它join。否则在其运行完成后,将会变成僵尸线程。除了浪费资源外,如果僵尸线程过多,我们将无法创建新线程。

pthread_join()的作用类似于waitpid()。然而有一些显著的区别:

  • Thread are peers.Any thread in a process can use pthread_join() to join with any other thread in the process.就是说,线程之间相互join与相互的创建关系无关。进程之间只能是父进程对子进程调用waitpid().
  • There is no way of saying “join with any thread” (for processes, we can do this using the call waitpid(–1, &status, options)); nor is there a way to do a nonblocking join (analogous to the waitpid() WNOHANG flag).
#include <pthread.h>
#include <tlpi_hdr.h>
#include <unistd.h>

static void *threadFunc(void *arg) {
  char *s = (char *)arg;
  printf("%s", s);
  return (void *)strlen(s);
}

int main(int argc, char *argv[]) {
  pthread_t t1;
  void *res;
  int s;

  s = pthread_create(&t1, NULL, threadFunc, (void *)"Hello World\n");
  if (s != 0) {
    errExitEN(s, "pthread_create");
  }

  printf("Message from main()\n");
  s = pthread_join(t1, &res);
  if (s != 0) errExitEN(s, "pthread_join");

  printf("Thread returned %ld\n", (long)res);

  exit(EXIT_SUCCESS);
}
$ ./simple_thread
Message from main()
Hello world
Thread returned 12

Detaching a Thread

默认情况下,线程是joinable状态,意思是当它结束之后,其他线程可以通过pthread_join()获取它的返回值。有时候我们不关系线程的返回状态,而是希望系统自动清除已经终结的线程。在这种情况下, 我们可以通过使用pthread_detach()将目标线程标记为detached

#include <pthread.h>
int pthread_detach(pthread_t thread);
//Returns 0 on success, or a positive error number on error

例子:

pthread_detach(pthread_self());

Thread Attributes

前面提到在创建线程的时候,pthread_attr_t类型的参数可以控制所创建线程的一些属性。这里我们不详细解释每个属性的细节,也不详细研究各种各样的可以用来管理pthread_attr_t对象的pthread函数原型。

//detached_thread
pthread_t thr;
pthread_attr_t attr;
int s;
s = pthread_attr_init(&attr); /* Assigns default values */
if (s != 0)
errExitEN(s, "pthread_attr_init");
s = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (s != 0)
errExitEN(s, "pthread_attr_setdetachstate");
s = pthread_create(&thr, &attr, threadFunc, (void *) 1);
if (s != 0)
errExitEN(s, "pthread_create");
s = pthread_attr_destroy(&attr); /* No longer needed */
if (s != 0)
errExitEN(s, "pthread_attr_destroy");

这段代码首先初始化了一个具有默认值的pthread_attr_t对象,然后设置该对象中用来表示detached的属性,然后用这个对象来创建一个新的detached线程。一旦线程创建完毕,该对象就被销毁。

Threads versus Processes

使用线程的优势:

  • 共享数据更加容易
  • 线程创建很容易

劣势:

  • 需要保证线程安全
  • 一个线程中的bug(比如修改了内存中错误的位置)会导致进程内的一组线程全部出错。
  • 线程之间相互竞争使用 host process 的有限的虚拟地址空间。事实上,每个线程的栈区,以及各个线程专属的数据会消耗进程的虚拟地址空间的一部分,这部分数据对于其他线程不可见。尽管可用的地址空间很大,但是这依然会是限制使用大量线程的重要限制因素。相反,独立的进程之间则各自都拥有巨大的可用的虚拟地址空间。(受到主存和交换空间大小的限制)

下面这些因素是其他一些可能会影响到底是使用线程还是进程的因素:

  • 处理多线程应用中的各种信号需要很细心的设计。
  • 多线程应用中,所有的线程都需要运行相同的代码(尽管可能在不同的函数中)。
  • 除了数据之外,线程之间还会共享一些其他信息(比如文件描述符、信号、当前工作目录、以及用户和用户组ID)。这个特性对于不同目的的应用来说优劣不同。

Exercises

  1. What possible outcomes might there be if a thread executes the following code:
pthread_join(pthread_self(), NULL);

Write a program to see what actually happens on Linux. If we have a variable, tid, containing a thread ID, how can a thread prevent itself from making a call, pthread_join(tid, NULL), that is equivalent to the above statement?

如果一个线程调用pthread_join(pthread_self(),NULL),那么pthread_join将会返回一个非0值,表示pthread_join出错。
为了防止这种情况,可以对pthread_join()进行一层封装,在执行pthread_join()之前使用pthread_equal()进行提前检查,防止这种情况的发生。

//joinself.cpp
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <iostream>
#include <string>

static void* thread_func(void* arg) {
  char* s = (char*)arg;
  return (void*)strlen(s);
}

static int robustJoin(pthread_t t, void** thread_return) {
  std::cout << "Robust Join Begin.\n";
  if (pthread_equal(pthread_self(), t) == 0) {
    int res = pthread_join(t, thread_return);
    std::cout << "Robust Join Finish.\n";
    return res;
  } else {
    std::cout << "Can't join thead with itself\n";
    return NULL;
  }
}

int main() {
  robustJoin(pthread_self(), NULL);
  std::cout << "pthread_join(pthread_self(), NULL) finished\n";

  std::string s = "Hello World";
  char ts[200];
  memcpy(ts, s.c_str(), strlen(s.c_str()));
  ts[strlen(s.c_str())] = '\0';

  pthread_t t1;
  pthread_create(&t1, NULL, thread_func, (void*)ts);

  void* res;
  robustJoin(t1, &res);
  std::cout << "Return Value of Thread " << t1 << " is " << (long)res
            << std::endl;
}
$ g++ -o join_self join_self.cpp -lpthread
$ ./join_self

Robust Join Begin.
Can't join thead with itself
pthread_join(pthread_self(), NULL) finished
Robust Join Begin.
Robust Join Finish.
Return Value of Thread 139757819791104 is 11

上述代码中,直接使用cout来打印 thread ID 的做法并不被提倡。因为虽然在Linux的线程实现中,不同进程之间的线程ID不同,然而,在其他实现中不一定是这样,而且SUSv3明确指出,应用程序使用线程ID来标识另一个进程中的线程的做法是不具有平台移植性的。Thread ID最好被理解成为一个结构体,使用pthread_equal()来判断两个线程ID是否一致。

  1. Aside from the absence of error checking and various variable and structure declarations, what is the problem with the following program?
static void *
threadFunc(void *arg)
{
struct someStruct *pbuf = (struct someStruct *) arg;
/* Do some work with structure pointed to by 'pbuf' */
}
int
main(int argc, char *argv[])
{
struct someStruct buf;
pthread_create(&thr, NULL, threadFunc, (void *) &buf);
pthread_exit(NULL);
}

pthread_create之前缺少pthread_t thr;
主线程是通过pthread_exit()结束的,而新创建的线程thr的状态不是detached,这会导致主线程终结之后,thr线程继续运行,如果threadFunc中没有返回值,则会变为僵尸线程。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!