5.多线程

  线程概念

    线程定义

    线程的优点

    线程的缺点

    线程异常

    线程用途

  进程与线程比较

    进程可以并发并行

    线程可以并发并行

  进程的多个线程共享的内容

  Linux线程控制

    POSIX线程库

    pthread_create

    pthread_族函数错误检查

    使用pthread_creat创建一个新的线程

    线程ID

    线程终止

    线程等待

    分离线程

  线程互斥

    进程线程互斥相关概念

    互斥量mutex

    互斥量(锁)

    互斥量实现原理

  线程安全

  重入

  可重入与线程安全的关系

  常见锁

    死锁

    死锁发生的必要条件(4)

    避免死锁

    避免死锁算法

  线程同步

    定义

    竞态条件

    线程同步四种方式

    条件变量

    条件变量函数

  生产者消费者模型

    基于BlockingQueue的生产者消费者模型

    C++ queue模拟阻塞队列的生产消费模型

  POSIX信号量

    初始化信号量

    销毁信号量

    等待信号量

    发布信号量

  线程池

    线程池的应用场景

    线程池示例

  线程安全的单例模式

  STL,智能指针和线程安全



线程概念



线程定义

线程是一个进程内部的控制序列,一切进程至少有一个线程;线程在进程内部运行,本质是在进程地址空间内运行;透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

linux下并没有真正意义上的线程存在,linux中使用进程来模拟实现线程,父进程创建子进程,子进程执行父进程的一部分代码,并且与父进程共享同一个地址空间。这些一个一个被创建出来的子进程可看到为线程,这种线程也称之为轻量级进程

在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化



线程的优点
  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

代价小、卡换快、占用少、多处理器并行



线程的缺点
  • 性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

  • 健壮性降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

  • 缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

  • 编程难度提高

编写与调试一个多线程程序比单线程程序困难得多



线程异常
  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃

  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出



线程用途
  • 合理的使用多线程,能提高CPU密集型程序的执行效率

  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)



进程与线程比较

  • 进程是资源分配的基本单位

  • 线程是CPU调度的基本单位

  • 线程共享进程数据,但也拥有自己的一部分数据:

    • 线程ID
    • 一组寄存器
    • errno
    • 信号屏蔽字
    • 调度优先级

进程可以并发并行
  • 进程一般都是并发执行的(轮询抢占处理机资源)

  • 进程在对称式编程中可以并行执行

例如4核心CPU可以 fork 出四个进程,每个进程独立使用一个处理器核心,不再分时轮询共享时间片。也不会在不同处理其中切换。

伯克利分校的 BOINC 项目



线程可以并发并行

线程在多处理机中可以并行执行,线程在单处理机上并发执行



进程的多个线程共享的内容

同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表

  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)

  • 当前工作目录

  • 用户id和组id

多个线程一定共享进程代码段和数据段共享



Linux线程控制



POSIX1线程库
  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的

  • 要使用这些函数库,要通过引入头文<pthread.h>

  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项



pthread_create

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

thread:返回线程ID

attr:设置线程的属性,attr为NULL表示使用默认属性

start_routine:是个函数地址,线程启动后要执行的函数

arg:传给线程启动函数的参数返回值:成功返回0;失败返回错误码



pthread_族函数错误检查
  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。

  • pthread_族函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回

  • pthread_族同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthread_族函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小



使用pthread_creat创建一个新的线程
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void* rout(void* arg) {
	int i;
	for (; ; ) {
		printf("I'am thread 1\n");
		sleep(1);
	}
}
int main(void)
{
	pthread_t tid;
	int ret;
	if ((ret = pthread_create(&tid, NULL, rout, NULL)) != 0) {
		fprintf(stderr, "pthread_create : %s\n", strerror(ret));
		exit(EXIT_FAILURE);
	}
	int i;
	for (; ; ) {
		printf("I'am main thread\n");
		sleep(1);
	}
}

pthread_t:typedef unsigned long int pthread_t;

C 库函数 char *strerror(int errnum) 从内部数组中搜索错误号 errnum,并返回一个指向错误消息字符串的指针。strerror生成的错误字符串取决于开发平台和编译器。

EXIT_FAILURE是C语言头文件库中定义的一个符号常量,在vc++6.0下头文件stdlib.h中定义如下: #define EXIT_FAILURE 1 可以作为exit()的参数来使用,表示没有成功地执行一个程序。

以上代码保存为test_pthread_creat.c

使用多线程库phtread.h需要动态链接,编译命令如下:

gcc test_pthread_creat.c -o test_pthread_creat -lpthread



线程ID

pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和线程实际的ID不是一回事。

线程实际的ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。

pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL2线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。

线程库NPTL提供了pthread_self函数,可以获得线程自身的ID

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void* rout(void* arg) {
	int i;
	for (; ; ) {
		printf("I'am thread 1\n");
		sleep(1);
	}
}
int main(void)
{
	pthread_t tid;
	int ret;
	if ((ret = pthread_create(&tid, NULL, rout, NULL)) != 0) {
		fprintf(stderr, "pthread_create : %s\n", strerror(ret));
		exit(EXIT_FAILURE);
	}
	int i;
	for (; ; ) {
		printf("I'am main thread\n");
		sleep(1);
	}
}

pthread_create函数第一个输出型参数和pthread_self函数对同一个线程获取到的tid值相同,且它们就是pthread_create函数第一个参数指向的虚拟内存单元的地址

在这里插入图片描述

因此pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址:

在这里插入图片描述



线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。

  2. 线程可以调用pthread_exit终止自己。

  3. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

pthread_exit函数
功能

线程终止

原型

void pthread_exit(void *value_ptr);

参数

value_ptr:value_ptr不要指向一个局部变量。

pthread_exit的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

返回值

无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

pthread_cancel函数
功能

取消一个执行中的线程

原型

int pthread_cancel(pthread_t thread);

参数

thread:要被终止的线程ID

返回值

成功返回0;失败返回错误码



线程等待
线程等待必要性
  • 已经退出的线程,其空间没有被释放(典型代表:线程栈等线程独有的资源),仍然在进程的地址空间内。

  • 新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

  • 创建新的线程不会复用刚才退出线程的地址空间。

pthread_join函数
功能

等待线程结束

原型

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

参数

thread:线程ID

value_ptr:它指向一个指针,后者指向线程的返回值

返回值

成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止通过pthread_join得到的终止状态是不同的

  1. 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。

  2. 如果thread线程被别的线程调用pthread_cancel异常终止掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED。

  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread1(void* arg)
{
	printf("thread 1 returning ... \n");
	int* p = (int*)malloc(sizeof(int));
	*p = 1;
	return (void*)p;
}
void* thread2(void* arg)
{
	printf("thread 2 exiting ...\n");
	int* p = (int*)malloc(sizeof(int));
	*p = 2;
	pthread_exit((void*)p);
}
void* thread3(void* arg)
{
	while (1) { //
		printf("thread 3 is running ...\n");
		sleep(1);
	}
	return NULL;
}
int main(void)
{
	pthread_t tid;
	void* ret;
	// thread 1 return
	pthread_create(&tid, NULL, thread1, NULL);
	pthread_join(tid, &ret);
	printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);
	free(ret);
	// thread 2 exit
	pthread_create(&tid, NULL, thread2, NULL);
	pthread_join(tid, &ret);
	printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);
	free(ret);
	// thread 3 cancel by other
	pthread_create(&tid, NULL, thread3, NULL);
	sleep(3);
	pthread_cancel(tid);
	pthread_join(tid, &ret);
	if (ret == PTHREAD_CANCELED)
		printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n", tid);
	else
		printf("thread return, thread id %X, return code:NULL\n", tid);
	return 0;
}

线程返回的是地址,因此返回的应该是全局变量或者堆上malloc出来的变量地址

在这里插入图片描述



分离线程
  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

  • 如果不关心线程的返回值,join是一种负担,这个时候,使用线程分离,当线程退出时,自动释放线程资源。

pthread_detach

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的,如下例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* arg)
{
	pthread_detach(pthread_self());
	printf("%s\n", (char*)arg);
	return NULL;
}
int main(void)
{
	pthread_t tid;
	if (pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0) {
		printf("create thread error\n");
		return 1;
	}
	int ret = 0;
	sleep(1);//很重要,要让线程先分离,再等待
	if (pthread_join(tid, NULL) == 0) {
		printf("pthread wait success\n");
		ret = 0;
	}
	else {
		printf("pthread wait failed\n");
		ret = 1;
	}
	return ret;
}



线程互斥



进程线程互斥相关概念
临界资源

多线程执行流共享的资源就叫做临界资源

临界区

每个线程内部,访问临界资源的代码,就叫做临界区

互斥

任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用

原子性

不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成



互斥量mutex
  • 大部分情况,线程使用的数据都是局部变量,变量在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,就可以通过数据的共享,完成线程之间的交互。

  • 多个线程并发的操作共享变量,会带来一些问题。

创建四个线程模拟多用户抢票
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void* route(void* arg)
{
	char* id = (char*)arg;
	while (1) {
		if (ticket > 0) {
			usleep(1000);
			printf("%s sells ticket:%d\n", id, ticket);
			ticket--;
		}
		else {
			break;
		}
	}
}
int main(void)
{
	pthread_t t1, t2, t3, t4;
	pthread_create(&t1, NULL, route, "thread 1");
	pthread_create(&t2, NULL, route, "thread 2");
	pthread_create(&t3, NULL, route, "thread 3");
	pthread_create(&t4, NULL, route, "thread 4");
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
}

在这里插入图片描述

全局变量ticket,被所有线程可见,共享的,它就是共享变量

无论在多处理机上,多线程并行运行,还是在单处理机上并发执行。usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,当ticket已经是0的时候,若一个线程已经进入if (ticket > 0)但并未ticket–; 其它线程也会进入if (ticket > 0),就会导致最终ticket会小于0;即使ticket不是0的时候,也会出现多个线程同时进入if (ticket > 0)的情况,共享变量ticket没有被正确的争抢

出现以上线程不安全的原因有:
  • 没有互斥的访问共享变量ticket;–ticket 操作本身就不是一个原子操作

  • 没有互斥的进入临界区

要保证上述抢票例子数据一致性就需要使用到锁机制,在Linux中提供的锁叫互斥量



互斥量(锁)
锁实现的原理

在这里插入图片描述

互斥量的接口
初始化互斥量
  • 静态分配:

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

  • 动态分配:

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

参数:

mutex:要初始化的互斥量;attr:NULL

销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);

销毁互斥量需要注意
  • 使用PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 要销毁的互斥量,要确保后面不会有线程再尝试加锁
互斥量加锁和解锁
加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

调用pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功

  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误号

学习完互斥量之后,将抢票例子改成线程安全的
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>  //usleep
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void* route(void* arg)
{
	char* id = (char*)arg;
	while (1) {
		pthread_mutex_lock(&mutex);
		if (ticket > 0) {
			usleep(1000);
			printf("%s sells ticket:%d\n", id, ticket);
			ticket--;
			pthread_mutex_unlock(&mutex);
			sched_yield(); //放弃CPU
		}
		else {
			pthread_mutex_unlock(&mutex);
			break;
		}
	}
}
int main(void)
{
	pthread_t t1, t2, t3, t4;
	pthread_mutex_init(&mutex, NULL);
	pthread_create(&t1, NULL, route, "thread 1");
	pthread_create(&t2, NULL, route, "thread 2");
	pthread_create(&t3, NULL, route, "thread 3");
	pthread_create(&t4, NULL, route, "thread 4");
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
	pthread_mutex_destroy(&mutex);
}

sched_yield()会让出当前线程的CPU占有权,然后把线程放到静态优先队列的尾端,然后一个新的线程会占用CPU。

如果屏蔽sched_yield则票会被一个线程抢占完

如果将加锁pthread_mutex_lock(&mutex);放到if (ticket > 0) 语句的第一行,发现又出现了数据不一致问题,ticket变量是全局共享的变量,是临界资源,对于临界资源的访问都需要属于临界区,因此加锁需要在if语句之前



互斥量实现原理

单纯的i++ 或者++i 都不是原子的,有可能会有数据一致性问题

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。



线程安全

定义

多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现线程安全问题。

线程安全的情况
  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性
线程不安全的情况
  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数



重入

定义

同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

可重入的情况
  • 不调用不可重入函数
  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
不可重入的情况
  • 可重入函数体内使用了静态的数据结构
  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构



可重入与线程安全的关系

联系
  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量且没有加锁保护,那么这个函数既不是线程安全也不是可重入的。
区别
  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的3,而可重入函数则一定是线程安全的。



常见锁



死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用但不会释放的资源而处于的一种永久等待状态。



死锁发生的必要条件(4)
  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系



避免死锁
  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配



避免死锁算法
  • 死锁检测算法
  • 银行家算法



线程同步



定义

"同"字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。

在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步



竞态条件

因为时序问题,而导致程序异常,我们称之为竞态条件。



线程同步四种方式

通用标识互斥锁(互斥量)pthread_mutex_、读写锁 pthread_rwlock_、条件变量pthread_cond_、信号量sem_



条件变量

例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种
情况就需要用到条件变量



条件变量函数
初始化

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

参数:

  • cond:要初始化的条件变量
  • attr:NULL
销毁

int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足

int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥
量恢复成原样。

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:

  • cond:要在这个条件变量上等待
  • mutex:互斥量

pthread_ cond_ wait 的互斥量:

条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。

条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);

实例

pthread_cond_signal、pthread_cond_wait、pthread_mutex_lock、pthread_mutex_unlock

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/*初始化互斥锁*/
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/*初始化条件变量*/
void* thread1(void*);
void* thread2(void*);
int i = 1;
int main(void)
{   
    pthread_t t_a;
    pthread_t t_b;
    pthread_create(&t_a, NULL, thread1, (void*)NULL);/*创建进程t_a*/
    pthread_create(&t_b, NULL, thread2, (void*)NULL); /*创建进程t_b*/
    pthread_join(t_a, NULL);/*等待进程t_a结束*/
    pthread_join(t_b, NULL);/*等待进程t_b结束*/
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    exit(0);
}
void* thread1(void* junk)
{
    sleep(1);
    while (1) {
        pthread_mutex_lock(&mutex);/*锁住互斥量*/
        pthread_cond_signal(&cond);/*条件改变,发送信号,通知t_b进程*/
        printf("thread1: %d\n", __LINE__);
        pthread_mutex_unlock(&mutex);/*解锁互斥量*/
        sleep(1);
    }
}
void* thread2(void* junk)
{
    while (1) {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);/*互斥变量mutex改变回原样,并等待cond改变*/
        printf("thread2: %d\n", __LINE__);
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}

在这里插入图片描述

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/*初始化互斥锁*/
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/*初始化条件变量*/
void* thread1(void*);
void* thread2(void*);
int i = 1;
int main(void)
{
	pthread_t t_a;
	pthread_t t_b;
	pthread_create(&t_a, NULL, thread1, (void*)NULL);/*创建进程t_a*/
	pthread_create(&t_b, NULL, thread2, (void*)NULL); /*创建进程t_b*/
	pthread_join(t_a, NULL);/*等待进程t_a结束*/
	pthread_join(t_b, NULL);/*等待进程t_b结束*/
	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);
	exit(0);
}
void* thread1(void* junk)
{
	for (i = 1; i <= 6; i++)
    {
        pthread_mutex_lock(&mutex);/*锁住互斥量*/
        printf("thread1: lock %d\n", __LINE__);
        if (i % 3 == 0) {
            printf("thread1:signal 1  %d\n", __LINE__);
            pthread_cond_signal(&cond);/*激活一个等待该条件的线程*//*条件改变,发送信号,通知t_b进程*/
            printf("thread1:signal 2  %d\n", __LINE__);
            sleep(1);
        }
        pthread_mutex_unlock(&mutex);/*解锁互斥量*/
        printf("thread1: unlock %d\n\n", __LINE__);
        sleep(1);
    }
}
void* thread2(void* junk)
{
    while (i < 6)
    {
        pthread_mutex_lock(&mutex);
        printf("thread2: lock %d\n", __LINE__);
        if (i % 3 != 0) {
            printf("thread2: wait 1  %d\n", __LINE__);
            pthread_cond_wait(&cond, &mutex);/*互斥变量mutex改变回原样,并等待cond改变*/
            /*pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mutex,
			然后阻塞在等待队列里休眠,直到再次被唤醒
			(大多数情况下是等待的条件成立而被唤醒,唤醒后,
			该进程会先锁定先pthread_mutex_lock(&mutex);,
			再读取资源用这个流程是比较清楚的
			
			进入该函数后,会去看条件量等于0不?等于,就把条件量变成1,直到cond_ wait返回,把条件量改成1
			*/
            printf("thread2: wait 2  %d\n", __LINE__);
        }
        pthread_mutex_unlock(&mutex);
        printf("thread2: unlock %d\n\n", __LINE__);
        sleep(1);
    }
}

LINE: 当前程序行的行号,表示为十进制整型常量

在这里插入图片描述

先创建thread1,再创建thread2
刚开始只有thread1,因此thread1先执行:i = 1, 加锁,i % 3 != 0无法进入if, 解锁,睡眠1秒thread1阻塞
thread2抢占到处理机:i = 1, 加锁, i % 3 != 0进入if, 执行pthread_cond_wait,由于thread1未pthread_cond_signal。条件不满足,因此thread2阻塞在pthread_cond_wait,并解锁
thread1继续执行:i = 2, 加锁,i % 3 != 0无法进入if, 解锁,睡眠1秒thread1阻塞
cond条件依然不满足,thread2依然被阻塞在pthread_cond_wait,等thread1阻塞1秒完成
thread1继续执行:i = 3,i % 3 == 0进入if, 执行pthread_cond_signal,条件改变发送信号通知thread2,解锁,睡眠1秒阻塞
thread2的pthread_cond_wait判断满足条件加锁运行:i = 3,解锁,睡眠1秒



生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

解耦
支持并发
支持忙闲不均



基于BlockingQueue的生产者消费者模型

在这里插入图片描述

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)



C++ queue模拟阻塞队列的生产消费模型
#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>
#define NUM 8
class BlockQueue {
private:
	std::queue<int> q;
	int cap;
	pthread_mutex_t lock;
	pthread_cond_t full;
	pthread_cond_t empty;
private:
	void LockQueue()
	{
		pthread_mutex_lock(&lock);
	}
	void UnLockQueue()
	{
		pthread_mutex_unlock(&lock);
	}
	void ProductWait()
	{
		pthread_cond_wait(&full, &lock);
	}
	void ConsumeWait()
	{
		pthread_cond_wait(&empty, &lock);
	}
	void NotifyProduct()
	{
		pthread_cond_signal(&full);
	}
	void NotifyConsume()
	{
		pthread_cond_signal(&empty);
	}
	bool IsEmpty()
	{
		return (q.size() == 0 ? true : false);
	}
	bool IsFull()
	{
		return (q.size() == cap ? true : false);
}
public:
	BlockQueue(int _cap = NUM) :cap(_cap)
	{
		pthread_mutex_init(&lock, NULL);
		pthread_cond_init(&full, NULL);
		pthread_cond_init(&empty, NULL);
	}
	void PushData(const int& data)
	{
		LockQueue();
		while (IsFull()) {
			NotifyConsume();
			std::cout << "queue full, notify consume data, product stop." <<
				std::endl;
			ProductWait();
		}
		q.push(data);
		// NotifyConsume();
		UnLockQueue();
	}
	void PopData(int& data)
	{
		LockQueue();
		while (IsEmpty()) {
			NotifyProduct();
			std::cout << "queue empty, notify product data, consume stop." <<
				std::endl;
			ConsumeWait();
		}
		data = q.front();
		q.pop();
		// NotifyProduct();
		UnLockQueue();
	}
	~BlockQueue()
	{
		pthread_mutex_destroy(&lock);
		pthread_cond_destroy(&full);
		pthread_cond_destroy(&empty);
	}
};
void* consumer(void* arg)
{
	BlockQueue* bqp = (BlockQueue*)arg;
	int data;
	for (; ; ) {
		bqp->PopData(data);
		std::cout << "Consume data done : " << data << std::endl;
	}
}
//more faster
void* producter(void* arg)
{
	BlockQueue* bqp = (BlockQueue*)arg;
	srand((unsigned long)time(NULL));
	for (; ; ) {
		int data = rand() % 1024;
		bqp->PushData(data);
		std::cout << "Prodoct data done: " << data << std::endl;
		// sleep(1);
	}
}
int main()
{
	BlockQueue bq;
	pthread_t c, p;
	pthread_create(&c, NULL, consumer, (void*)&bq);
	pthread_create(&p, NULL, producter, (void*)&bq);
	pthread_join(c, NULL);
	pthread_join(p, NULL);
	return 0;
}



POSIX4信号量

进程间通信中介绍了进程间通信的三个体系,POSIX信号量是POSIX的一种

POSIX信号量和SystemV信号量作用相同,都是用于进程同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。



初始化信号量

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
参数
  • pshared:0表示线程间共享,非零表示进程间共享
  • value:信号量初始值



销毁信号量

int sem_destroy(sem_t *sem);



等待信号量
int sem_wait(sem_t *sem);

功能:等待信号量,会将信号量的值减1



发布信号量
int sem_post(sem_t *sem);

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。

上面生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序(POSIX信号量):

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <semaphore.h>
#include <pthread.h>
#define NUM 16
class RingQueue {
private:
	std::vector<int> q;
	int cap;
	sem_t data_sem;
	sem_t space_sem;
	int consume_step;
	int product_step;
public:
	RingQueue(int _cap = NUM) :q(_cap), cap(_cap)
	{
		sem_init(&data_sem, 0, 0);
		sem_init(&space_sem, 0, cap);
		consume_step = 0;
		product_step = 0;
	}
	void PutData(const int& data)
	{
		sem_wait(&space_sem); // P
		q[consume_step] = data;
		consume_step++;
		consume_step %= cap;
		sem_post(&data_sem); //V
	}
	void GetData(int& data)
	{
		sem_wait(&data_sem);
		data = q[product_step];
		product_step++;
		product_step %= cap;
		sem_post(&space_sem);
	}
	~RingQueue()
	{
		sem_destroy(&data_sem);
		sem_destroy(&space_sem);
	}
};
void* consumer(void* arg)
{
	RingQueue* rqp = (RingQueue*)arg;
	int data;
	for (; ; ) {
		rqp->GetData(data);
		std::cout << "Consume data done : " << data << std::endl;
		sleep(1);
	}
}
//more faster
void* producter(void* arg)
{
	RingQueue* rqp = (RingQueue*)arg;
	srand((unsigned long)time(NULL));
	for (; ; ) {
		int data = rand() % 1024;
		rqp->PutData(data);
		std::cout << "Prodoct data done: " << data << std::endl;
		// sleep(1);
	}
}
int main()
{
	RingQueue rq;
	pthread_t c, p;
	pthread_create(&c, NULL, consumer, (void*)&rq);
	pthread_create(&p, NULL, producter, (void*)&rq);
	pthread_join(c, NULL);
	pthread_join(p, NULL);
}



线程池

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量



线程池的应用场景:
  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线
    程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任
    务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线
    程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内
    存到达极限,出现错误.



线程池示例:
  1. 创建固定数量线程池,循环从任务队列中获取任务对象,
  2. 获取到任务对象后,执行任务对象中的任务接口
#include <iostream>
#include <queue>
#include <pthread.h>
#define MAX_THREAD 5
typedef bool (*handler_t)(int);
class ThreadTask
{
private:
	int _data;
	handler_t _handler;
public:
	ThreadTask() :_data(-1), _handler(NULL) {}
	ThreadTask(int data, handler_t handler) {
		_data = data;
		_handler = handler;
	}
	void SetTask(int data, handler_t handler) {
		_data = data;
		_handler = handler;
	}
	void Run() {
		_handler(_data);
	}
};
class ThreadPool
{
private:
	int _thread_max;
	int _thread_cur;
	bool _tp_quit;
	std::queue<ThreadTask*> _task_queue;
	pthread_mutex_t _lock;
	pthread_cond_t _cond;
private:
	void LockQueue() {
		pthread_mutex_lock(&_lock);
	}
	void UnLockQueue() {
		pthread_mutex_unlock(&_lock);
	}
	void WakeUpOne() {
		pthread_cond_signal(&_cond);
	}
	void WakeUpAll() {
		pthread_cond_broadcast(&_cond);
	}
	void ThreadQuit() {
		_thread_cur--;
		UnLockQueue();
		pthread_exit(NULL);
	}
	void ThreadWait() {
		if (_tp_quit) {
			ThreadQuit();
		}
		pthread_cond_wait(&_cond, &_lock);
	}
	bool IsEmpty() {
		return _task_queue.empty();
	}
	static void* thr_start(void* arg) {
		ThreadPool* tp = (ThreadPool*)arg;
		while (1) {
			tp->LockQueue();
			while (tp->IsEmpty()) {
				tp->ThreadWait();
			}
			ThreadTask* tt;
			tp->PopTask(&tt);
			tp->UnLockQueue();
			tt->Run();
			delete tt;
		}
		return NULL;
	}
public:
	ThreadPool(int max = MAX_THREAD) :_thread_max(max), _thread_cur(max),
		_tp_quit(false) {
		pthread_mutex_init(&_lock, NULL);
		pthread_cond_init(&_cond, NULL);
	}
	~ThreadPool() {
		pthread_mutex_destroy(&_lock);
		pthread_cond_destroy(&_cond);
	}
	bool PoolInit() {
		pthread_t tid;
		for (int i = 0; i < _thread_max; i++) {
			int ret = pthread_create(&tid, NULL, thr_start, this);
			if (ret != 0) {
				std::cout << "create pool thread error\n";
				return false;
			}
		}
		return true;
	}
	bool PushTask(ThreadTask* tt) {
		LockQueue();
		if (_tp_quit) {
			UnLockQueue();
			return false;
		}
		_task_queue.push(tt);
		WakeUpOne();
		UnLockQueue();
		return true;
	}
	bool PopTask(ThreadTask** tt) {
		*tt = _task_queue.front();
		_task_queue.pop();
		return true;
	}
	bool PoolQuit() {
		LockQueue();
		_tp_quit = true;
		UnLockQueue();
		while (_thread_cur > 0) {
			WakeUpAll();
			usleep(1000);
		}
		return true;
	}
};
#endif
/*main.cpp*/
bool handler(int data)
{
	srand(time(NULL));
	int n = rand() % 5;
	printf("Thread: %p Run Tast: %d--sleep %d sec\n", pthread_self(), data, n);
	sleep(n);
	return true;
}
int main()
{
	int i;
	ThreadPool pool;
	pool.PoolInit();
	for (i = 0; i < 10; i++) {
		ThreadTask* tt = new ThreadTask(i, handler);
		pool.PushTask(tt);
	}
	pool.PoolQuit();
	return 0;
}
g++ -std=c++0x test.cpp -o test -pthread -lrt



线程安全的单例模式

某些类, 只应该具有一个对象(实例), 就称之为单例.

在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.

饿汉和懒汉实现方式

懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度.

饿汉
template <typename T>
class Singleton {
	static T data;
public:
	static T* GetInstance() {
		return &data;
	}
};

懒汉
template <typename T>
class Singleton {
	static T* inst;
public:
	static T* GetInstance() {
		if (inst == NULL) {
			inst = new T();
		}
		return inst;
	}
};

存在一个严重的问题, 线程不安全。第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例。

// 懒汉模式, 线程安全
template <typename T>
class Singleton {
	volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
	static std::mutex lock;
public:
	static T* GetInstance() {
		if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
			lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
			if (inst == NULL) {
				inst = new T();
			}
			lock.unlock();
		}
		return inst;
	}
};

加锁解锁的位置

双重 if 判定, 避免不必要的锁竞争

volatile关键字防止过度优化



STL,智能指针和线程安全

STL

默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全

智能指针
  • 对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.

  • 对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数.

其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,
    行锁等),当其他线程想要访问数据时,被阻塞挂起。

  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据
    前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作5

  • 自旋锁,公平锁,非公平锁?


  1. 可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX)是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称,其正式称呼为IEEE Std 1003,而国际标准名称为ISO/IEC 9945。 ↩︎

  2. NPTL现在位于Linux上的 GNU Libc中,并且是(或至少非常努力地)实现POSIX线程.它是Linux系统上的一堆源代码和二进制代码. 使用gcc -pthread编译并与-pthread链接的应用程序现在在Linux上使用NPTL代码. ↩︎

  3. 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。 ↩︎

  4. Portable Operating System Interface ↩︎

  5. CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若
    不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值