linux C 线程相关

本文围绕Linux多线程展开,介绍了线程概念、与进程关系及使用多线程的优势。阐述了POSIX Threads标准接口,详细讲解线程管理,包括ID、创建、终止等操作,还涉及线程安全的互斥量、条件变量等内容,以及线程存储和C++类成员函数创建多线程的注意点。

linux多线程概述

什么是线程

线程(thread)是包含在进程内部的顺序执行流,是进程中的实际运作单位,也是操作系统能够进行调度的最小单位。一个进程中可以并发多条线程,每条线程并行执行不同的任务。

线程与进程的关系

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个主线程;
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源;
  • 线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
  • 进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源;
  • 在创建或撤销进程时,由于系统要为之分配和回收资源,导致系统的开销大于创建或撤销线程时的开销。

为什么要是用多线程

多进程程序结构和多线程程序结构有很大的不同,多线程程序结构相对于多进程程序结构有以下的优势:
(1)方便的通信和数据交换
线程间有方便的通信和数据交换机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。
(2)更高效的利用CPU
使用多线程可以加快应用程序的响应。这对图形界面的程序尤其有意义,假如一个操作耗时很长,那么整个系统都会等它操作,此时程序不会响应键盘、鼠标、菜单等操作,而使用多线程技术,将耗时长的操作置于一个新的线程,就可以避免这种尴尬情况的发生。
同时多线程使多 CPU 系统更加有效。操作系统会保证当线程数不大于 CPU 数目时,不同的线程运行于不同的 CPU 上。

POSIX Threads 概述

从历史上看,众多软件供应商都为自己的产品实现了多线程库的专有版本。这些线程库实现彼此独立并有很大差别,导致程序员难以开发可移植的多线程应用程序,因此必须要确立一个规范的编程接口标准来充分利用多线程所提供的优势,POSIX Threads 就是这样一个规范的多线程标准接口。

POSIX Threads(通常简称为 Pthreads)定义了创建和操纵线程的一套 API 接口,一般用于 Unix-like POSIX 系统中(如 FreeBSD、GNU/Linux、OpenBSD、Mac OS 等系统)。

Pthreads 接口可以根据功能划分四个组:

  • 线程管理
  • 互斥量
  • 条件变量
  • 同步
    编写 Pthreads 多线程的程序时,源码只需要包含 pthread.h 头文件就可以使用 Pthreads库中的所有类型及函数:
#include <pthread.h>
LDFLAGS += -pthread

在编译 Pthread 程序时在编译和链接过程中需要加上-pthread 参数.

线程管理

线程管理包含了线程的创建、终止、等待、分离、设置属性等操作。

线程ID

线程 ID 可以看作为线程的句柄,用来引用一个线程。
Pthreads 线程通过一个 pthread_t 类型的 ID 来引用。线程可以通过调用 pthread_self()函数来获取自己的 ID。pthread_self()函数原型如下:

pthread_t pthread_self(void);

该函数返回调用线程的线程 ID
由于 pthread_t 类型可能是一个结构体,可以使pthread_equal()来比较两个线程 ID 是否相等。pthread_equal()函数原型如下:

int pthread_equal(pthread_t t1, pthread_t t2);

如果 t1 等于 t2,该函数返回一个非 0 值,否则返回 0。

创建与终止

每个线程都有从创建到终止的生命周期。
1.创建线程
在进程中创建一个新线程的函数是 pthread_create(),原型如下:

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

线程被创建后立即运行

返回值说明:

  • 如果 pthread_create()调用成功,函数返回 0,否则返回一个非 0 的错误码,下列出 pthread_create()函数调用时必须检查的错误码。
错误码出错说明
EAGAIN系统没有创建线程所需的资源
EINVALattr 参数无效
EPERM调用程序没有适当的权限来设定调度策略或 attr 指定的参数

参数说明:

  • thread 用来指向新创建线程的 ID;
  • attr 用来表示一个封装了线程各种属性的属性对象,如果 attr 为 NULL,新线程就使用默认的属性;
  • start_routine 是线程开始执行的时候调用的函数的名字,start_routine 函数有一个指向 void 的指针参数,并有 pthread_create 的第四个参数 arg 指定值,同时 start_routine函数返回一个指向 void 的指针,这个返回值被 pthread_join 当做退出状态处理;
  • arg 为参数 start_routine 指定函数的参数。如果需要多个参数需要把那多个参数放到一个数据结构中,然后将数据结构地址传递到arg中。

2.终止线程
进程的终止可以通过直接调用 exit()、执行 main()中的 return、或者通过进程的某个其它线程调用 exit()来实现。在以上任何一种情况发生时,所有的线程都会被终止。如果主线程在创建了其它线程后没有任务需要处理,那么它应该阻塞等待直到所有线程都结束为止,或者应该调用 pthread_exit(NULL)。

调用 exit()函数会使整个进程终止,而调用 pthread_exit()只会使得调用线程终止,同时在创建的线程的顶层执行 return 线程会隐式地调用 pthread_exit()。pthread_exit()函数原型如下:

void pthread_exit(void *retval);

retval 是一个 void 类型的指针,可以将线程的返回值当作 pthread_exit()的参数传入,这个值同样被 pthread_join()当作退出状态处理。如果进程的最后一个线程调用了 pthread_exit(),进程会带着状态返回值 0 退出。
3.范例

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 5
void *PrintHello(void *threadid){ /* 线程函数  */
	long tid;
	tid = (long)threadid;
	printf("Hello World! It's me, thread #%ld!\n", tid); /* 打印线程对应的参数  */
	pthread_exit(NULL);
}
int main (int argc, char *argv[])
{
	pthread_t threads[NUM_THREADS];
	int rc;
	long t;
	for(t=0; t<NUM_THREADS; t++){  /* 循环创建 5 个线程 */
		printf("In main: creating thread %ld\n", t);
		rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t); /* 创建线程  */
		if (rc){
			printf("ERROR; return code from pthread_create() is %d\n", rc);
			exit(-1);
		}
	}
	printf("In main: exit!\n");
	pthread_exit(NULL); /* 主线程退出 */
	return 0;
}

主线程创建了 5 个线程,这 5 个线程和主线程并发执行,主线程创建完线程后调用 pthread_exit()函数退出线程,其它线程分别打印当前线程的序号。当主线程先于其它进程执行pthread_exit()时,进程还不会退出,只有最后一个线程也完成了,进程才会退出。

在这里插入图片描述

连接与分离

线程可以分为 分离线程(DETACHED)和 非分离线程(JOINABLE)两种:

  • 分离线程是退出时会释放它的资源的线程;
  • 非分离线程退出后不会立即释放资源,需要另一个线程为它调用 pthread_join 函数或者进程退出时才会释放资源。只有非分离线程才是可连接的,分离线程退出时不会报告它的退出状态。
    1.线程分离
    pthread_detach()函数可以将非分离线程设置为分离线程,函数原型如下:
int pthread_detach(pthread_t thread);

参数 thread 是要分离的线程的 ID。
线程可以自己来设置分离,也可以由其它线程来设置分离,以下代码线程可设置自身分离:

pthread_detach(pthread_self());

成功返回 0;失败返回一个非 0 的错误码.
pthread_detach 的实现必须检查的错误码。

错误码出错描述
EINVALthread 参数所表示的线程不是可分离的线程
ESRCH没有找到 ID 为 thread 的线程

2.线程连接
如果一个线程是非分离线程,那么其它线程可调pthread_join()函数对非分离线程进行连接。pthread_join()函数原型如下:

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

pthread_join()函数将调用线程挂起,直到参数 thread 指定的目标线程终止运行为止。

参数 retval 的作用是为指向线程的返回值的指针提供一个位置,这个返回值是目标线程

调用 pthread_exit()或者 return 后所返回的值。当目标线程无需返回时可使用 NULL 值,调用线程如果不需对目标线程的返回状态进行检查可直接将 retval 赋值为 NULL。

如果 pthread_join()成功调用,它将返回 0 值,如果不成功,pthread_join()返回一个非 0的错误码,以下为pthread_join()的实现必须检查的错误码。

错误码出错描述
EINVALthread 参数所表示的线程不是可分离的线程
ESRCH没有找到 ID 为 thread 的线程

3.范例:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define NUM_THREADS 4
void *BusyWork(void *t)  /* 线程函数 */
{
	int i;
	long tid;
	double result=0.0;
	tid = (long)t;
	printf("Thread %ld starting...\n",tid);
	for (i=0; i<1000000; i++) {
		result = result + sin(i) * tan(i);  /* 进行数学运算 */
	}
	printf("Thread %ld done. Result = %e\n",tid, result);
	pthread_exit((void*) t);  /* 带计算结果退出 */
}
int main (int argc, char *argv[]){
	pthread_t thread[NUM_THREADS];
	int rc;
	long t;
	void *status;
	for(t=0; t<NUM_THREADS; t++) {
		printf("Main: creating thread %ld\n", t);
		rc = pthread_create(&thread[t], NULL, BusyWork, (void *)t); /* 创建线程 */
		if (rc) {
			printf("ERROR; return code from pthread_create() is %d\n", rc);
			exit(-1);
		}
	}
	for(t=0; t<NUM_THREADS; t++) {
	rc = pthread_join(thread[t], &status);  /*等待线程终止,并获取返回值*/
	if (rc) {
		printf("ERROR; return code from pthread_join() is %d\n", rc);
		exit(-1);
	}
	printf("Main: completed join with thread %ld having a status of %ld\n",t,(long)status);
	}
	printf("Main: program completed. Exiting.\n");
	pthread_exit(NULL);
}

gcc编译指令:
在这里插入图片描述
运行结果:
在这里插入图片描述
可以看出四个线程的计算结果相同,主线程在 4 个线程完成后退出。

线程属性

前面介绍的线程创建 pthread_create()函数,pthread_create()函数的第二个参数为pthread_attr_t 类型,用于设置线程的属性。

线程基本属性包括: 栈大小、 调度策略和 线程状态。

通常先创建一个属性对象,然后在属性对象上设置属性的值,再将属性对象传给pthread_create 函数的第二个参数用来创建含有该属性的线程。

一个属性对象可以多次传给 pthread_create()函数,以创建多个含有相同属性的线程。

1.属性对象
(1)初始化属性对象
pthread_attr_init()函数用于将属性对象使用默认值进行初始化,函数原型如下:

int pthread_attr_init(pthread_attr_t *attr);

函数只有一个参数,是一个指向 pthread_attr_t 的属性对象的指针。成功返回 0,否则返回一个非 0 的错误码。

(2)销毁属性对象

int pthread_attr_destroy(pthread_attr_t *attr);

函数只有一个参数,是一个指向 pthread_attr_t 的属性对象的指针。成功返回 0,否则返回一个非 0 的错误码。

2.线程状态

线程可以有两种状态,分别是:

  • PTHREAD_CREATE_JOINABLE——非分离线程;
  • PTHREAD_CREATE_DETACHED——分离线程。

(1)获取线程状态

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

参数 attr 是一个指向已初始化的属性对象的指针,detachstate 是所获取状态值的指针。
成功返回 0,否则返回一个非 0 的错误码。

(2)设置线程状态

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

参数 attr 是一个指向已初始化的属性对象的指针, detachstate 是要设置的值。成功返回0,否则返回一个非 0 的错误码。

3.线程栈

每个线程都有一个独立的调用栈,线程的栈大小在线程创建的时候就已经固定下来,Linux 系统线程的默认栈大小为 8MB,只有主线程的栈大小会在运行过程中自动增长。用户可以通过属性对象来设置和获取栈大小。

(1)获取线程栈

int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

参数 attr 是一个指向已初始化的属性对象的指针,stacksize 是保存所获取栈大小的指针。
成功返回 0,否则返回一个非 0 的错误码。

(2)设置线程栈

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

参数 attr 是一个指向已初始化的属性对象的指针,stacksize 是需要设置的栈大小。成功返回 0,否则返回一个非 0 的错误码。

4.范例

#include <pthread.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#define handle_error_en(en, msg) \  /* 出错处理宏供返回错误码的函数使用 */
do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

#define handle_error(msg) \ /* 出错处理宏 */

do { perror(msg); exit(EXIT_FAILURE); } while (0)

struct thread_info {
	pthread_t thread_id;
	int thread_num;
	char *argv_string;
};

static void *thread_start(void *arg){  /* 线程运行函数 */
	struct thread_info *tinfo = arg;
	char *uargv, *p;
	printf("Thread %d: top of stack near %p; argv_string=%s\n",  /* 通过 p 的地址来计算栈的起始地址 */
	tinfo->thread_num, &p, tinfo->argv_string);
	uargv = strdup(tinfo->argv_string);//将argv_string字符串拷贝至uargv
	if (uargv == NULL)
	handle_error("strdup");
	for (p = uargv; *p != '\0'; p++)
	*p = toupper(*p);  /* 小写字符转换大写字符 */
	return uargv;  /* 将转换结果返回  */
}
int main(int argc, char *argv[]){
	int s, tnum, opt, num_threads;
	struct thread_info *tinfo;
	pthread_attr_t attr;
	int stack_size;
	void *res;
	stack_size = -1;
	//getopt()用来分析命令行参数
	while ((opt = getopt(argc, argv, "s:")) != -1) { /* 处理参数-s 所指定的栈大小 */
		switch (opt) {
			case 's':
			stack_size = strtoul(optarg, NULL, 0);//将字符串转换成无符号长整型数
			break;
			default:
			fprintf(stderr, "Usage: %s [-s stack-size] arg...\n",argv[0]);
			exit(EXIT_FAILURE);
		}
	}
	num_threads = argc - optind;
	s = pthread_attr_init(&attr); /* 初始化属性对象 */
	if (s != 0)
	handle_error_en(s, "pthread_attr_init");
	if (stack_size > 0) {
		s = pthread_attr_setstacksize(&attr, stack_size);  /* 设置属性对象的栈大小 */
		if (s != 0)
		handle_error_en(s, "pthread_attr_setstacksize");
	}
	tinfo = calloc(num_threads, sizeof(struct thread_info));
	if (tinfo == NULL)
	handle_error("calloc");
	for (tnum = 0; tnum < num_threads; tnum++) {
		tinfo[tnum].thread_num = tnum + 1;
		tinfo[tnum].argv_string = argv[optind + tnum];
		s = pthread_create(&tinfo[tnum].thread_id, &attr,  /* 根据属性创建线程 */
		&thread_start, &tinfo[tnum]);
		if (s != 0)
		handle_error_en(s, "pthread_create");
	}
	s = pthread_attr_destroy(&attr);  /* 销毁属性对象 */
	if (s != 0)
	handle_error_en(s, "pthread_attr_destroy");
	for (tnum = 0; tnum < num_threads; tnum++) {
		s = pthread_join(tinfo[tnum].thread_id, &res); /* 等待线程终止,并获取返回值 */
		if (s != 0)
		handle_error_en(s, "pthread_join");
		printf("Joined with thread %d; returned value was %s\n",
		tinfo[tnum].thread_num, (char *) res);
		free(res);
	}
	free(tinfo);
	exit(EXIT_SUCCESS);
}

举例说明了线程创建及线程属性的使用方法,主线程根据参数给出的线程栈大小来设置线程属性对象,然后使用剩余参数分别创建线程来实现小写转大写的功能并打印出栈地址。
在这里插入图片描述
此程序使用-s 参数来指定每个新创建线程的栈大小,每个线程运行起来后都先取出栈变量的地址,通过打印变量地址来大概估计栈的起始地址。然后每个线程将线程参数给出的字符串转换为大写并返回给主线程,主线程使用pthread_join()等待并获取线程的结果。

线程安全

多线程编程环境中,多个线程同时调用某些函数可能会产生错一些误结果,这些函数称为 非线程安全函数。如果库函数能够在多个线程中同时执行并且不会互相干扰,那么这个库函数就是 线程安全(thread-safe )函数。

POSIX.1-2008 规定,除了表 13.4 列出的特定函数外,所有标准库的函数都应该是线程安全函数。有些库函数虽然不是线程安全函数,但系统有后缀为_r 的线程安全版本,如strtok_r。
在这里插入图片描述

什么是互斥量

互斥量(Mutex),又称为互斥锁,是一种用来保护临界区的特殊变量,它可以处于锁定(locked)状态,也可以处于解锁(unlocked)状态:

  • 如果互斥锁是锁定的,就是某个特定的线程正持有这个互斥锁;
  • 如果没有线程持有这个互斥锁,那么这个互斥锁就处于解锁状态。

每个互斥锁内部有一个线程等待队列,用来保存等待该互斥锁的线程。当互斥锁处于解锁状态时,如果某个线程试图获取这个互斥锁,那么这个线程就可以得到这个互斥锁而不会阻塞;当互斥锁处于锁定状态时,如果某个线程试图获取这个互斥锁,那么这个线程将阻塞在互斥锁的等待队列内。

互斥量是最简单也是最有效的线程同步机制。程序可以用它来保护临界区,以获得对排它性资源的访问权。另外,互斥量只能被短时间地持有,使用完临界资源后应立即释放锁。

创建与销毁

1.创建互斥量
pthreads 使用 pthread_mutex_t 类型的变量来表示互斥量,同时在使用互斥量进行同步前需要先对它进行初始化,可以用静态或动态的方式对互斥量进行初始化。

(1)静态初始化

对于静态分配的 pthread_mutex_t 变量来说,只要将 PTHREAD_MUTEX_INITIALIZER赋给变量就行了。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

(2).动态初始化

对于动态分配或者不使用默认属性的互斥变量来说,需要调用 pthread_mutex_int()函数来执行初始化工作pthread_mutex_int()函数原型如下:

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

参数 mutex 是一个指向要初始化的互斥量的指针;参数 attr 传递 NULL 来初始化一个带有默认属性的互斥量,否则就要用类似于线程属性对象所使用的方法,先创建互斥量属性对象,再用该属性对象来创建互斥量。

函数成功返回 0,否则返回一个非 0 的错误码.

列出 pthread_mutex_init 出错的错误码。

错误码出错描述
EAGAIN系统缺乏初始化互斥量所需的非内存资源
ENOMEM系统缺乏初始化互斥量所需的内存资源
EPERM调用程序没有适当的优先级

静态初始化程序通常比调用 pthread_mutex_init 更有效,而且在任何线程开始执行之前,确保变量被初始化一次。

  • 以下代码用来动态地初始化默认属性的互斥量 mylock:
int error;
pthread_mutex_t mylock;
if (error = pthread_mutex_init(&mylock, NULL))
fprintf(stderr, "Failed to initialize mylock : %s\n", strerror(error));
  • 静态和动态创建互斥量的区别:

动态需要手动释放内存,而静态不需要。
动态创建时可以设置更多的选项。

2.销毁互斥量

销毁互斥量使用 pthread_mutex_destroy()函数,原型如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数 mutex 指向要销毁的互斥量。以下代码销毁了 mylock 互斥量:

int error;
pthread_mutex_t mylock;
if (error = pthread_mutex_destroy(&mylock))
fprintf(stderr, "Failed to destroy mylock : %s\n", strerror(error));

加锁与解锁

1.加锁

线程试图锁定互斥量的过程称之为加锁。
pthreads 中有两个试图锁定互斥量的函数,pthread_mutex_lock()和 pthread_mutex_trylock()。pthread_mutex_lock()函数会一直阻塞到互斥量可用为止,而 pthread_mutex_trylock()则尝试加锁,但通常会立即返回。函数原型如下:

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

参数 mutex 是需要加锁的互斥量。函数成功返回 0,否则返回一个非 0 的错误码,其中在另一个线程已持有锁的情况下,调用 pthread_mutex_trylock()函数时错误码为 EBUSY。

2.解锁

解锁是线程将互斥量由锁定状态变为解锁状态。
pthread_mutex_unlock()函数用来释放指定的互斥量。函数原型如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数 mutex 是需要解锁的互斥量。函数成功返回 0,否则返回一个非 0 的错误码。
只有在线程进入临界区之前正确地获取了适当的互斥量,才能在离开临界区时释放互斥量。以下伪代码展示了互斥量保护临界区的基本用法:

pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;//静态初始化
pthread_mutex_lock(&mylock);
临界区代码
pthread_mutex_unlock(&mylock);

范例:

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

pthread_t tid[2];
pthread_mutex_t lock;//定义

void* doPrint(void *arg)
{
	int id = (long)arg;
 	int i = 0;
	pthread_mutex_lock(&lock);  /* 使用互斥量保护临界区 */
 	printf("Job %d started\n", id);
	for (i = 0; i < 5; i++) {
 		printf("Job %d printing\n", id);
 		usleep(10);
 	}
 	printf("Job %d finished\n", id);
	pthread_mutex_unlock(&lock);
 	return NULL;
 }

int main(void)
{
 	long i = 0;
 	int err;
 	if (pthread_mutex_init(&lock, NULL) != 0)  /* 动态初始化互斥量 */
	{
 		printf("\n Mutex init failed\n");
 		return 1;
 	}
	while(i < 2)
 	{
  		err = pthread_create(&(tid[i]), NULL, &doPrint,(void*)i);//创建两个线程
 		if (err != 0)
 		printf("Can't create thread :[%s]", strerror(err));
	 	i++;
	 }
 	pthread_join(tid[0], NULL);
 	pthread_join(tid[1], NULL);
    pthread_mutex_destroy(&lock);
    return 0;
 }

使用互斥量来保证多线程同时输出顺序的例子,互斥量能保证只有获取资源的线程打印完,别的线程才能打印,从而避免了打印乱序的问题。

编译运行:
在这里插入图片描述
注释掉锁信号之后:
在这里插入图片描述

死锁和避免

1.什么是死锁
死锁是指两个或两个以上的执行序列在执行过程中,因争夺资源而造成的一种互相等待的现象

例如:一个线程 T1 已锁定了一个资源 R1,又想去锁定资源 R2,而此时另一个线程 T2 已锁定了资源 R2,却想去锁定资源 R1。这两个线程都想得到对方的资源,而又不愿
释放自己的资源,结果就是两个线程都在等待而无法执行,如图

在这里插入图片描述

#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
pthread_t tid[2];
pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;  /* 静态初始化互斥量 */
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER; 
void * t1(void *arg) {
	pthread_mutex_lock(&mutexA); /* 线程 1 获取 mutexA */
	printf("t1 get mutexA\n"); 
	usleep(1000); 
	pthread_mutex_lock(&mutexB); /* 线程 1 获取 mutexB */
	printf("t1 get mutexB\n");
	pthread_mutex_unlock(&mutexB);  /* 线程 1 释放 mutexB */
	printf("t1 release mutexB\n");
	pthread_mutex_unlock(&mutexA);  /* 线程 1 释放 mutexA */
	printf("t1 release mutexA\n");
	return NULL;
}
void * t2(void *arg) {
	pthread_mutex_lock(&mutexB);
	printf("t2 get mutexB\n");
	usleep(1000);
	pthread_mutex_lock(&mutexA);
	printf("t2 get mutexA\n");
	pthread_mutex_unlock(&mutexA);
	printf("t2 release mutexA\n");
	pthread_mutex_unlock(&mutexB);
	printf("t2 release mutexB\n");
	return NULL;
}
int main(void) {
	int err;
	err = pthread_create(&(tid[0]), NULL, &t1, NULL );  /* 创建线程 1 */
	if (err != 0)
	printf("Can't create thread :[%s]", strerror(err));
	err = pthread_create(&(tid[1]), NULL, &t2, NULL);  /* 创建线程 2 */
	if (err != 0)
	printf("Can't create thread :[%s]", strerror(err));
	pthread_join(tid[0], NULL);
	pthread_join(tid[1], NULL);
	return 0;
}

示例了死锁发生的情况,程序创建了两个线程,第一个线程先获取 mutexA锁,再获取 mutexB 锁;第二个线程先获取 mutexB 后获取 mutexA,这时死锁就可能发生。
在这里插入图片描述

t1 线程获取 mutexA 后等待 mutexB,t2 线程获取
mutexB 后等待 mutexA,两个线程互相等待,进入死锁。

2.死锁的避免
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生,如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。例如,规定程序内有三个互斥锁的加锁顺序为 mutexA->mutexB->mutexC,则线程 t1、t2、t3 线程操作伪代码如下所示:

t1t2t3
lock(mutexA)lock(mutexA)lock(mutexB)
lock(mutexB)lock(mutexC)lock(mutexC)
lock(mutexC)

条件变量

在多线程编程中仅使用互斥锁来完成互斥是不够用的, 如以下情形:
假设有两个线程 t1 和 t2, 需要这个两个线程循环对一个共享变量 sum 进行自增操作,那么 t1 和 t2 只需要使用互斥量即可保证操作正确完成,线程执行代码如所示

pthread_mutex_t sumlock= PTHREAD_MUTEX_INITIALIZER;
void * t1t2(void) {
	pthread_mutex_lock(&sumlock);
	sum++;
	pthread_mutex_unlock(&sumlock);
}

如果这时需要增加另一个线程 t3,需要 t3 在 count 大于 100 时将 count 值重新置 0 值,那么可以 t3 可以实现如下:

void * t3 (void) {
	pthread_mutex_lock(&sumlock);
	if (sum >= 100) {
		sum = 0;
		pthread_mutex_unlock(&sumlock);
	} else {
		pthread_mutex_unlock(&sumlock);
		usleep(100);
	}
}

以上代码存在以下问题:

  1. sum 在大多数情况下不会到达 100, 那么对 t3 的代码来说,大多数情况下, 走的是 else分支, 只是 lock 和 unlock,然后 sleep()。 这浪费了 CPU 处理时间。
  2. 为了节省 CPU 处理时间, t3 会在探测到 sum 没到达 100 的时候 usleep()一段时间。这样却又带来另外一个问题, 亦即 t3 响应速度下降。 可能在 sum 到达 200 的时候, t3 才会醒过来。

这样时间与效率出现了矛盾,而条件变量就是解决这个问题的好方法。

创建与销毁

1.创建条件变量
Pthreads 用 pthread_cond_t 类型的变量来表示条件变量。程序必须在使用 pthread_cond_t变量之前对其进行初始化。
1)静态初始化
对于静态分配的变量可以简单地将PTHREAD_COND_INITIALIZER 赋值给变量来初
始化默认行为的条件变量。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

2)动态初始化
对动态分配或者不使用默认属性的条件变量来说可以使用 pthread _cond_init()来初始化。函数原型如下:

int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);

参数 cond 是一个指向需要初始化 pthread_cond_t 变量的指针,参数 attr 传递 NULL 值时, pthread_cond_init()将 cond 初始化为默认属性的条件变量。
函数成功将返回 0;否则返回一个非 0 的错误码。

静态初始化程序通常比调用 pthread_cond_init()更有效,而且在任何线程开始执行之前,确保变量被执行一次。
以下代码示例了条件变量的初始化。

pthread_cond_t cond;
int error;
if (error = pthread_cond_init(&cond, NULL));
fprintf(stderr, "Failed to initialize cond : %s\n", strerror(error));

2.销毁条件变量
函数 pthread_cond_destroy()用来销毁它参数所指出的条件变量,函数原型如下:

int pthread_cond_destroy(pthread_cond_t *cond);

函数成功调用返回 0,否则返回一个非 0 的错误码。以下代码演示了如何销毁一个条件变量。

pthread_cond_t cond;
int error;
if (error = pthread_cond_destroy(&cond))
fprintf(stderr, "Failed to destroy cond : %s\n", strerror(error));

等待与通知

3.等待

条件变量是与条件测试一起使用的,通常线程会对一个条件进行测试,如果条件不满足就会调用条件等待函数来等待条件满足。

条件等待函数有pthread_cond_wait()pthread_cond_timedwait()和两个,函数原型如下

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

pthread_cond_wait()函数在条件不满足时将一直等待, 而 pthread_cond_timedwait()将只等待一段时间。

参数 cond 是一个指向条件变量的指针,参数 mutex 是一个指向互斥量的指针,线程在调用前应该拥有这个互斥量,当线程要加入条件变量的等待队列时,等待操作会使线程释放这个互斥量。

pthread_timedwait()的第三个参数 abstime 是一个指向返回时间的指针,如果条件变量通知信号没有在此等待时间之前出现,等待将超时退出, abstime 是个绝对时间,而不是时间间隔。

以上函数成功调用返回 0,否则返回非 0 的错误码,其中 pthread_cond_timedwait() 函数如果 abstime 指定的时间到期,错误码为 ETIMEOUT。

以下代码使得线程进入等待,直到收到通知并且满足 a 大于等于 b 的条件。

pthread_mutex_lock(&mutex)
while(a < b)
	pthread_cond_wait(&cond, &mutex)
pthread_mutex_unlock(&mutex)

4.通知
当另一个线程修改了某参数可能使得条件变量所关联的条件变成真时,它应该通知一个或者多个等待在条件变量等待队列中的线程。

条件通知函数有 pthread_cond_signal()和 pthread_cond_broadcast()函数,其中 pthread_
cond_signal 函数可以唤醒一个在条件变量等待队列等待的线程,而 pthread_cond_broadcast函数可以所有在条件变量等待队列等待的线程。函数原型如下:

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

参数 cond 是一个指向条件变量的指针。函数成功返回 0,否则返回一个非 0 的错误码。

5.范例

#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
pthread_t tid[3];
int sum = 0;
pthread_mutex_t sumlock = PTHREAD_MUTEX_INITIALIZER; /* 静态初始化互斥量 */
pthread_cond_t cond_sum_ready = PTHREAD_COND_INITIALIZER; /* 静态初始化条件变量 */
void * t1t2(void *arg) {
	int i;
	long id = (long)arg;
	for (i = 0; i < 60; i++) {
		pthread_mutex_lock(&sumlock); /* 使用互斥量保护临界变量 */
		sum++;
		printf("t%ld: read sum value = %d\n", id + 1 , sum);
		pthread_mutex_unlock(&sumlock);
		if (sum >= 100)
	    pthread_cond_signal(&cond_sum_ready); /* 发送条件通知,唤醒等待线程 */
	}
	return NULL;
}
void * t3(void *arg) {
	pthread_mutex_lock(&sumlock);
	while(sum < 100) /* 不满足条件将一直等待 */
	pthread_cond_wait(&cond_sum_ready, &sumlock); /* 等待条件满足 */
	sum = 0;
	printf("t3: clear sum value\n");
	pthread_mutex_unlock(&sumlock);
	return NULL;
}
int main(void) {
	int err;
	long i;
	for (i = 0; i < 2; i++) {
		err = pthread_create(&(tid[i]), NULL, &t1t2, (void *)i); /* 创建线程 1 线程 2 */
		if (err != 0) {
			printf("Can't create thread :[%s]", strerror(err));
		}
	 }
	err = pthread_create(&(tid[2]), NULL, &t3, NULL); /* 创建线程 3 */
	if (err != 0)
	printf("Can't create thread :[%s]", strerror(err));
	for (i = 0; i < 3; i++)
		pthread_join(tid[i], NULL);
	return 0;
}

sum 累加到 100 时发送条件通知,但程序结
果中 sum 计算到 103 时, t3 才被调用,这是因为 signal 与 wait 调用之间有间隙存在。
在这里插入图片描述
我实测得数据是到了120.

pthread_key_t和pthread_key_create()

本例子来源pthread_key_t和pthread_key_create()详解

在多线程程序中,所有线程共享程序中的变量。现在有一全局变量,所有线程都可以使用它,改变它的值。而如果每个线程希望能单独拥有它,那么就需要使用线程存储了。表面上看起来这是一个全局变量,所有线程都可以使用它,而它的值在每一个线程中又是单独存储的。这就是线程存储的意义。

  1. 创建一个类型为 pthread_key_t 类型的变量。

  2. 调用 pthread_key_create() 来创建该变量。该函数有两个参数,第一个参数就是上面声明的 pthread_key_t 变量,第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调用。该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。

  3. 当线程中需要存储特殊值的时候,可以调用 pthread_setspcific() 。该函数有两个参数,第一个为前面声明的 pthread_key_t 变量,第二个为 void* 变量,这样你可以存储任何类型的值。

  4. 如果需要取出所存储的值,调用 pthread_getspecific() 。该函数的参数为前面提到的 pthread_key_t 变量,该函数返回 void * 类型的值。

下面是前面提到的函数的原型:

int pthread_setspecific(pthread_key_t key, const void *value);

void *pthread_getspecific(pthread_key_t key);

int pthread_key_create(pthread_key_t *key, void (destructor)(void));

#include <malloc.h>
#include <pthread.h>
#include <stdio.h>
/* The key used to associate a log file pointer with each thread. */
static pthread_key_t thread_key;
static char flag[5];
/* Write MESSAGE to the log file for the current thread. */
/* Close the log file pointer THREAD_LOG. */
void close_thread (void* thread_log)
{
	printf("exit ")	
}
void* thread_function (void* args)
{
	while(1){
		long r = long(arg);
		if(flag[r] == 0){
			pthread_setspecific(thread_key,r); 
			printf("thread %d start and init log_key = %d\n",r,r);
			
		}
		flag[r]++;
		printf("thread_key = %d",thread_key);
		usleep(10000);
		if(flag[r]==5){
			return NULL;
		}
	}
}
int main ()
{
	pthread_t threads[5];
	/* Create a key to associate thread log file pointers in
	thread-specific data.When the thread to free use close_thread_log to clean up the file
	pointers. */
	pthread_key_create (&thread_key, close_thread);
	long t;
	/* Create threads to do the work. */
	for (t = 0; t < 5; ++t){
		flag[t] = 0;
		pthread_create (&(threads[t]), NULL, thread_function, (void*)t);
	}
	/* Wait for all threads to finish. */
	for (i = 0; i < 5; ++i)
		pthread_join (threads[i], NULL);
	return 0;
}  

注:
其实在Linux 中,新建的线程并不是在原先的进程中,而是系统通过一个系统调用clone() 。该系统copy 了一个和原先进程完全一样的进程,并在这个进程中执行线程函数。不过这个copy 过程和fork 不一样。copy 后的进程和原先的进程共享了所有的变量,运行环境(clone的实现是可以指定新进程与老进程之间的共享关系,100%共享就表示创建了一个线程)。这样,原先进程中的变量变动在copy 后的进程中便能体现出来。

在linux c++类中的成员函数里创建多线程要注意的地方

链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值