Linux多线程编程


线程(thread)是允许应用程序并发执行多个任务的一种机制。如图所示,一个进程可以包含多个线程。同一程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段(initialized data)、未初始化数据段(uninitialized data),以及堆内存段(heap segment)。( 传统意义上的 UNIX 进程只是多线程程序的一个特例,该进程只包含一个线程。

在这里插入图片描述

1.基本线程API

Linux上他们都定义在pthread.h头文件中

1.1Pthreads API 的概念简介

线程和 errno
在传统 UNIX API 中,errno 是一全局整型变量。然而,这无法满足多线程程序的需要。如果线程调用的函数通过全局 errno 返回错误时,会与其他发起函数调用并检查 errno 的线程混淆在一起。换言之,这将引发竞争条件(race condition)。因此,在多线程程序中,每个线程都有属于自己的 errno。

Pthreads 函数返回值
从系统调用和库函数中返回状态,传统的做法是:返回 0 表示成功,返回-1 表示失败,并设置 errno 以标识错误原因。Pthreads API 则反其道而行之。所有 Pthreads 函数均以返回 0 表示成功,返回一正值表示失败。这一失败时的返回值,与传统 UNIX 系统调用置于 errno 中的值含义相同。

编译 Pthreads 程序
在 Linux 平台上,在编译调用了 Pthreads API 的程序时,需要设置 cc -pthread 的编译选项。使用该选项的效果如下。

  • 定义_REENTRANT 预处理宏。这会公开对少数可重入(reentrant)函数的声明。
  • 程序会与库 libpthread 进行链接(等价于-lpthread)。

1.2 API介绍

1.2.1 创建线程

函数 pthread_create()负责创建一条新线程。

   #include <pthread.h>

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

新线程通过调用带有参数 arg 的函数 start(即 start(arg))而开始执行。调用 pthread_create()的线程会继续执行该调用之后的语句。(这一行为与 glibc 库对系统调用 clone()的包装函数行为相同。)
将参数 arg 声明为 void*类型,意味着可以将指向任意对象的指针传递给 start()函数。一般情况下,arg 指向一个全局或堆变量,也可将其置为 NULL。如果需要向 start()传递多个参数,可以将 arg 指向一个结构,该结构的各个字段则对应于待传递的参数。通过审慎的类型强制转换,arg 甚至可以传递 int 类型的值。
参数 attr 是指向 pthread_attr_t 对象的指针,该对象指定了新线程的各种属性。如果将 attr 设置为 NULL,那么创建新线程时将使用各种默认属性。

1.2.2 终止线程

可以如下方式终止线程的运行。

  • 线程 start 函数执行 return 语句并返回指定值。
  • 线程调用 pthread_exit()(详见后述)。
  • 调用 pthread_cancel()取消线程。
  • 任意线程调用了 exit(),或者主线程执行了 return 语句(在 main()函数中),都会导致进程中的所有线程立即终止。
    pthread_exit()函数将终止调用线程,且其返回值可由另一线程通过调用 pthread_join()来获取。
   #include <pthread.h>

   void pthread_exit(void *retval);

pthread_exit函数通过retval参数向线程的回收者传递其退出信息。它执行完之后不会返回到调用者,而且永远不会失败。

函数 pthread_join()等待由 thread 标识的线程终止。(如果线程已经终止,pthread_join()会立即返回)。这种操作被称为连接(joining)。

   #include <pthread.h>

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

thread是目标线程的标识符,retval为目标线程返回的退出信息。若 retval 为一非空指针,将会保存线程终止时返回值的拷贝,该返回值亦即线程调用return 或 pthred_exit()时所指定的值。可能的错误码如表:

错误码描述
EDEADLKA deadlock was detected (e.g., two threads tried to join with each other); or thread specifies the calling thread.
EINVALthread is not a joinable thread or another thread is already waiting to join with this thread.
ESRCHNo thread with the ID thread could be found.

有时候我们希望异常终止一个线程,即取消线程,它是通过如下函数实现的:

   #include <pthread.h>

   int pthread_cancel(pthread_t thread);

thread是目标线程的标识符。成功时返回0,失败返回错误码。接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,分为如下两个函数完成:

   #include <pthread.h>

   int pthread_setcancelstate(int state, int *oldstate);
   int pthread_setcanceltype(int type, int *oldtype);
   				返回值:成功时返回0,出错返回错误码

state用于设置线程的取消状态(是否允许取消),有两个可选值:

  • PTHREAD_CANCEL_ENABLE
    The thread is cancelable. This is the default cancelability state in all new
    threads, including the initial thread. The thread’s cancelability type determines
    when a cancelable thread will respond to a cancellation request.

  • PTHREAD_CANCEL_DISABLE
    The thread is not cancelable. If a cancellation request is received, it is blocked
    until cancelability is enabled.

type取消类型(如何取消),有两个可选值:

  • PTHREAD_CANCEL_DEFERRED
    A cancellation request is deferred until the thread next calls a function that is a
    cancellation point . This is the default cancelability type in all new threads, including the initial thread.

  • PTHREAD_CANCEL_ASYNCHRONOUS
    The thread can be canceled at any time. (Typically, it will be canceled immedi‐
    ately upon receiving a cancellation request, but the system doesn’t guarantee
    this.)

oldstate原取消状态,oldtype原取消类型

1.3 线程属性

   #include <pthread.h>

   int pthread_attr_init(pthread_attr_t *attr);
   int pthread_attr_destroy(pthread_attr_t *attr);
   int pthread_attr_setstack(pthread_attr_t *attr,
                             void *stackaddr, size_t stacksize);
   int pthread_attr_getstack(const pthread_attr_t *attr,
                             void **stackaddr, size_t *stacksize);
   int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
   int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
 
   int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
   int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);

   int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
   int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t *guardsize);

   int pthread_attr_setschedparam(pthread_attr_t *attr,
                                  const struct sched_param *param);
   int pthread_attr_getschedparam(const pthread_attr_t *attr,
                                  struct sched_param *param);

   int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
   int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);

   int pthread_attr_setinheritsched(pthread_attr_t *attr,
                                    int inheritsched);
   int pthread_attr_getinheritsched(const pthread_attr_t *attr,
                                    int *inheritsched);

   int pthread_attr_setscope(pthread_attr_t *attr, int scope);
   int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);

在这里插入图片描述
scope,线程间竞争CPU的范围,即线程优先级的有效范围。POSIX标准规定了该属性的PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS两个可选值,前者表示目标线程与系统中所有线程一起竞争CPU的使用,后者表示目标线程仅与其他隶属于统一进程的线程竞争CPU的使用。

2. 互斥锁

线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正由其他线程修改的变量。术语临界区(critical section)是指访问某一共享资源的代码片段,并且这段代码的执行应为原子(atomic)操作,亦即,同时访问同一共享资源的其他线程不应中断该片段的执行。

2.1 保护对共享变量的访问

为避免线程更新共享变量时所出现问题,必须使用互斥量(mutex 是 mutual exclusion 的缩写)来确保同时仅有一个线程可以访问某项共享资源。 更为全面的说法是,可以使用互斥量来保证对任意共享资源的原子访问,而保护共享变量是其最常见的用法。 互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。 一旦线程锁定互斥量,随即成为该互斥量的所有者。只有所有者才能给互斥量解锁。这一属性改善了使用互斥量的代码结构,也顾及到对互斥量实现的优化。因为所有权的关系,
有时会使用术语获取(acquire)和释放(release)来替代加锁和解锁。
一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问同一资源时将采用如下协议。

  • 针对共享资源锁定互斥量。
  • 访问共享资源。
  • 对互斥量解锁。
    如果多个线程试图执行这一代码块(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示:

在这里插入图片描述

2.2 互斥锁基础API

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

在这里插入图片描述

2.3 互斥锁属性

   #include <pthread.h>

   int pthread_mutexattr_init(pthread_mutexattr_t *attr);
   int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

   int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr,
                                    int *pshared);
   int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
                                    int pshared);

   int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,
       int *restrict type);
   int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

在这里插入图片描述

2.4 死锁

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

int a = 0;
int b = 0;
pthread_mutex_t mutex_a;

pthread_mutex_t mutex_b;

/*子线程先申请b锁再申请a锁*/
void * another( void *arg )
{
	pthread_mutex_lock( &mutex_b );
	printf( "in child thread, got mutex b, waiting for mutex a\n" );
	sleep( 5 );  //延时,让子线程得到b锁
	++b;
	pthread_mutex_lock( &mutex_a );
	b += a++;
	pthread_mutex_unlock( &mutex_a );
	pthread_mutex_unlock( &mutex_a );
	pthread_exit( NULL );
}

/* 主线程先申请a锁再申请b锁*/
int main()
{
	pthread_t id;

	pthread_mutex_init( &mutex_a, NULL );
	pthread_mutex_init( &mutex_b, NULL );
	pthread_create( &id, NULL, another, NULL );

	pthread_mutex_lock( &mutex_a );
	printf( "in parent thread, got mutex a, waiting for mutex b\n" );
	sleep(5);  // 延时,让主线程得到a锁
	++a;
	pthread_mutex_lock( &mutex_b );
	a+= b++;
	pthread_mutex_unlock( &mutex_b );
	pthread_mutex_unlock( &mutex_a );

	pthread_join( id, NULL );
	pthread_mutex_destroy( &mutex_a );
	pthread_mutex_destroy( &mutex_b );

	return 0;
}

2.5 条件变量

条件变量用在线程之间同步共享数据的值,条件变量提供了一种线程间的通信机制:当某个共享数据达到某个值的时候,唤醒这个共享数据的线程。

   #include <pthread.h>

   int pthread_cond_destroy(pthread_cond_t *cond);
   int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
   pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
   int pthread_cond_broadcast(pthread_cond_t *cond);
   int pthread_cond_signal(pthread_cond_t *cond);
   int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
       返回值:成功,返回0,失败返回错误码

这些函数的第一个参数cond指向要操作的目标条件变量,条件变量是pthread_cond_t结构体
pthread_cond_init用于初始化条件变量,attr参数指定条件变量的属性。如果为NULL,则表示默认属性。
还可以使用:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
来初始化一个条件变量。宏PTHREAD_COND_INITIALIZER把条件变量各个字段都初始化为0。

pthread_cond_destroy用于销毁条件变量,是放弃占用的内核资源。销毁一个正在被等待的条件变量将失败并返回EBUSY

pthread_cond_broadcast 以广播的方式唤醒所有等待目标条件变量的线程。pthread_cond_signal函数用于唤醒一个等待目标条件变量的线程。至于哪个线程将被唤醒,则取决于线程的优先级和调度策略。有实我们想唤醒一个指定的线程,可以间接的实现该需求:定义一个能唯一表示目标线程的全局变量,在唤醒等待条件变量的线程前设置该变量的目标线程。然后采取广播方式去唤醒所有等待条件变量的线程。这些线程被唤醒后都检查该变量以判断被唤醒的是否是自己,如果是就开始执行后续代码,否则返回继续等待

pthread_cond_wait用于等待目标条件变量。mutex参数是用于保护条件变量的互斥锁,以保证pthread_cond_wait操作的原子性。在调用pthread_cond_wait之前必须保证互斥锁mutex已加锁,否则将导致不可预期的结果。pthread_cond_wait执行时,首先把调用线程放入条件变量的等待队列中,然后将互斥锁mutex解锁(可见,从pthread_cond_wait开始执行到其调用线程被放入条件变量的等待队列之间的这段时间内,pthread_cond_broadcast和pthread_cond_signal不会修改条件变量),当pthread_cond_wait成功返回时,互斥锁将再次被锁上。

3 多线程环境

3.1 可重入函数

如果一个函数能被多个线程同时调用且不发生竞态条件,则我们称它为线程安全的,或者说它是可重入函数。Linux库函数只有一小部分是不可重入的。这些库函数之所以不可重入,主要原因是其内部使用了静态变量。不过Linux对很多不可重入库函数提供了对应可重入版本,这些可重入版本的函数名是在原函数名尾部加上_r。如,函数localtime对应可重入函数localtime_r。多线程程序中调用的库函数,一定要使用其可重入版本,否则肯能导致意想不到的结果。

3.2 线程和进程

如果一个多线程程序的某个线程调用了fork()函数,子进程只拥有一个执行线程,它是调用fork那个线程的完整复制。并且子进程将自动继承父进程中互斥锁(条件变量与之类似)的状态。也就是说,父进程中已经被加锁的互斥锁在子进程依旧是被锁住的。这会导致一个问题:子进程可能不清楚从父进程继承来的互斥锁的具体状态(枷锁状态还是解锁状态)。这个互斥锁可能被加锁了,但并不是由调用fork函数的哪个线程锁住的,而是由其他线程锁住的。如果是这样,子进程再次对该互斥锁执行加锁状态会导致死锁。

代码:

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

pthread_mutex_t mutex;

/* 子线程运行的函数,它首先获得互斥锁mutex,然后暂停5s,再释放互斥锁*/
void * another( void *arg )
{
	printf( "in child thread ,lock the mutex\n" );
	pthread_mutex_lock( &mutex );
	sleep( 5 );
	pthread_mutex_unlock( &mutex );
}


/*void prepare()
{
	pthread_mutex_lock( &mutex );
}

void infork()
{
	pthread_mutex_unlock( &mutex );
}
*/
int main()
{
	pthread_mutex_init( &mutex, NULL );
	pthread_t id;
	pthread_create( &id, NULL, another, NULL );
	/*父进程中主线程暂停1s,确保执行fork操作之前,子线程已经开始运行并获得了互斥变量mutex*/
	sleep(1);

//	pthread_atfork( prepare, infork, infork );

	int pid = fork();

	if( pid < 0 )
	{
		pthread_join( id, NULL );
		pthread_mutex_destroy( &mutex );
		return 1;
	}
	else if( pid == 0 )
	{
		printf( "I'm in the child, want to get the lock" );
		/* 子进程从父进程继承了互斥锁mutex状态,该互斥锁处于锁住状态,这是由于父进程中子线程执行pthread_mutex_lock引起的,因此,下面的这句枷锁操作会一直阻塞。(尽管逻辑上说它是不应该阻塞的)*/
		pthread_mutex_lock( &mutex );
		printf( "I can not run to here, oop ...\n" );
		pthread_mutex_unlock( &mutex );
		exit( 0 );
	}
	else{
		wait( NULL );
	}
	pthread_join( id, NULL );
	pthread_mutex_destroy( &mutex );
	return 0;
}

运行结果:

[fancy@localhost ch14]$ ./multi_thread_fork 
in child thread ,lock the mutex



^C

程序一直阻塞在某处,强制退出。
pthread提供了一个专门函数以确保fork调用后父进程和子进程都有一个清楚的锁状态。该函数定义如下:

#include <pthread.h>

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

该函数建立三个句柄帮助清理互斥锁的状态。prepare句柄将在fork调用创建出子进程之前被执行。它可以用来锁住所有父进程中的互斥锁。parent句柄在fork调用创建出子进程之后,fork返回之前在父进程中被执行。它的作用是释放所有在prepare句柄中被锁住的互斥锁。child句柄是fork返回之前,在子进程中被执行。和parent句柄一样,child句柄作用是释放所有在prepare句柄中被锁住的互斥锁。
该函数成功返回0,失败返回错误代码。
改进的代码(去掉代码注释部分)执行结果:

[fancy@localhost ch14]$ ./multi_thread_fork2 
in child thread ,lock the mutex
I'm in the child, want to get the lockI can not run to here, oop ...

3.3 线程和信号

每个线程都可以独立地设置信号掩码。进程信号掩码函数sigprocmask。但在多线程环境下我们应使用pthread版本的sigprocmask函数来设置线程信号掩码:

#include <signal.h>
#include <pthread.h>

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

The pthread_sigmask() function is just like sigprocmask(2), with the difference that its use in multithreaded programs is explicitly specified by POSIX.1.

The behavior of the call is dependent on the value of how, as follows.

  • SIG_BLOCK
    The set of blocked signals is the union of the current set and the set argument.

  • SIG_UNBLOCK
    The signals in set are removed from the current set of blocked signals. It is permissible to attempt to unblock a signal which is not blocked.

  • SIG_SETMASK
    The set of blocked signals is set to the argument set.

If oldset is non-NULL, the previous value of the signal mask is stored in oldset.

If set is NULL, then the signal mask is unchanged (i.e., how is ignored), but the current value of the signal mask is nevertheless returned in oldset (if it is not NULL).

returns 0 on success and -1 on error. In the event of an error, errno is set to indicate the cause.

由于进程中所有线程共享该进程的信号,所以线程库根据掩码把信号发送给哪个具体的线程。因此,如果我们在每个子线程中都单独设置信号掩码,据容易导致逻辑所悟。此外,所有线程共享信号处理函数(这意味着,当我们在一个线程中设置了某个信号的信号处理函数后,它将覆盖其他线程为同一个信号设置的信号处理函数),这说明我们应该定义一个专门的线程来处理所有的信号,这可以通过两步实现:

  1. 在主线程创建出其他子线程之前就调用pthread_sigmask来设置好信号掩码,所有的新创建子线程都将自动继承这个信号掩码。这样做之后,实际上所有线程都不会相应被屏蔽的信号了。
  2. 在某个线程中调用如下函数等待信号并处理之:
#include <signal.h>

int sigwait(const sigset_t *set, int *sig);
          返回值:成功,返回0;失败,返回错误码。

set指定需要等待的信号的集合。
参数sig指向的整数用于存储该函数返回的信号值。
一旦sigwait正确返回,我们就可以对接收到的信号做处理了。显然,如果使用了sigwait,就不应该再为信号设置信号处理函数了。因为当程序接收到信号时,二者中只能有一个起作用。

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

   /* Simple error handling functions */

   #define handle_error_en(en, msg) \
           do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

   static void *
   sig_thread(void *arg)
   {
       sigset_t *set = arg;
       int s, sig;

       for (;;) {
           s = sigwait(set, &sig);
           if (s != 0)
               handle_error_en(s, "sigwait");
           printf("Signal handling thread got signal %d\n", sig);
       }
   }

   int
   main(int argc, char *argv[])
   {
       pthread_t thread;
       sigset_t set;
       int s;

       /* Block SIGQUIT and SIGUSR1; other threads created by main()
          will inherit a copy of the signal mask. */

       sigemptyset(&set);
       sigaddset(&set, SIGQUIT);
       sigaddset(&set, SIGUSR1);
       s = pthread_sigmask(SIG_BLOCK, &set, NULL);
       if (s != 0)
           handle_error_en(s, "pthread_sigmask");

       s = pthread_create(&thread, NULL, &sig_thread, (void *) &set);
       if (s != 0)
           handle_error_en(s, "pthread_create");

       /* Main thread carries on to create other threads and/or do
          other work */

       pause();            /* Dummy pause so we can test program */
   }

另外,pthread提供了以下方法,使我们可以明确将一个信号发送给指定的线程:

#include <signal.h>

int pthread_kill(pthread_t thread, int sig);
			返回值:成功,返回0;失败,返回错误码。

参数:

thread 指定目标线程
sig 指定待发送的信号,如果sig为0,则pthread_kill不发送信号,但它会执行错误检查,据此可以检测目标线程是否存在

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值