走进 C/C++后台开发的第四步: Linux 多线程编程精讲

多线程编程

  • Linux 多线程概述
    • 1.1 概述
    • 1.2 线程分类
    • 1.3 进程与线程的操作Linux实现
  • 线程的创建和退出
    • 2.1 线程的创建
    • 2.2 线程的退出
    • 演示
  • 线程的等待退出
    • 3.1 等待线程退出
    • 3.2 线程的取消和终止清理函数
      • 3.2.1 线程的取消
      • 3.2.2 线程的资源清理函数
      • 演示
  • 线程的同步与互斥
    • 4.1 线程的互斥
      • 4.1.1 创建和销毁锁
      • 4.1.2 锁操作
      • 4.1.3 加锁注意事项
      • 演示:
    • 4.2 线程的同步
      • 4.2.1 条件变量的工作原理
      • 4.2.2 创建与注销
      • 4.2.3 等待和激发
      • 4.2.4 条件变量的使用
  • 线程安全和线程属性
    • 5.1 线程安全
    • 5.2 线程的属性
      • 演示



Linux 多线程概述


1.1 概述

进程是系统中程序执行和资源分配的基本单位。每个进程有自己的数据段、代码段和堆栈段。这就造成进程在进行切换等操作时都需要有比较负责的上下文切换等动作,OS并发性也有所下降。

线程的出现: 为了进一步减少处理器的空转时间,提升系统的并发程度,并且支持多处理器和减少上下文切换开销,也就出现了线程。

并发与并行

  • 并行:同一时刻多个进程或线程都在运行。
  • 并发:一个时间段内多个进程或线程在运行,但是同一时间点上面只有一个进程或线程在运行。

进程与线程的关系与区别

  • 进程是操作系统中资源分配的基本单位。线程是CPU调度和程序执行的最小单元。

  • 一个进程内可以有多个线程,默认一个进程内最少有一个线程,这个线程是主线程。

  • 线程优点:开销小,切换快,线程间共享同一进程的地址空间,通信很方便。缺点:不利于资源的管理和保护。

  • 进程优点:便于资源的管理和保护,因为进程间是独立的,每个进程有自己的资源,缺点:开销大,速度慢,进程间有数据传递时要用进程间通信,开销多,不方便。

走进 C/C++后台开发的第四步: Linux 多线程编程精讲_第1张图片

1.2 线程分类

按调度者分为用户级线程和核心级线程

  • 用户级线程:主要解决上下文切换问题,调度算法和调度过程全部由用户决定,在运行时不需要特定的内核支持。缺点是无法发挥多处理器的优势
  • 核心级线程:允许不同进程中的线程按照同一相对优先调度方法调度,发挥多处理器的并发优势

现在大多数系统都采用用户级线程和核心级线程并存的方法。一个用户级线程可以对应一个或多个核心级线程,也就是“一对一”或“一对多”模型


1.3 进程与线程的操作Linux实现

走进 C/C++后台开发的第四步: Linux 多线程编程精讲_第2张图片



线程的创建和退出


2.1 线程的创建

线程的创建使用 pthread_create, 创建完即确定了线程函数的入口点,并开始运行相关的线程函数,运行完毕,线程会主动退出。

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

函数 pthread_create 用来创建线程。返回值:成功,则返回 0;失败,则返回对应错误码。

各参数描述如下:

  • 参数 thread 是传出参数,保存新线程的标识;
  • 参数 attr 是一个结构体指针,结构中的元素分别指定新线程的运行属性,attr 可以用 pthread_attr_init 等
    函数设置各成员的值,但通常传入为 NULL 即可;
  • 参数 start_routine 是一个函数指针,指向新线程的入口点函数,线程入口点函数带有一个 void *的参数由 pthread_create 的第 4 个参数传入;
  • 参数 arg 用于传递给第 3 个参数指向的入口点函数的参数,可以为 NULL,表示不传递
  • 创建线程后,可以通过ps -elLf命令看到线程

2.2 线程的退出

线程的退出的方式还可以使用 pthread_exit 函数,这是线程主动退出行为

void pthread_exit(void *retval);

函数 pthread_exit 表示线程的退出。其参数可以被其它线程用 pthread_join 函数(下面讲)捕获。


演示

编译时需要带上线程库选项:

gcc -o a a.c -lpthread
#include 
#include 
void *ThreadFunc(void *pArg) //参数的值为 123
{
	int i = 0;
	for(; i<10; i++)
	{
		printf("Hi,I'm child thread,arg is:%ld\n", (long)pArg);
		sleep(1);
	}
    	pthread_exit(NULL);
	}
	int main()
	{
		pthread_t thdId;
		pthread_create(&thdId, NULL, ThreadFunc, (void *)123 );
		int i = 0;
	for(; i<10; i++)
	{
		printf("Hi,I'm main thread,child thread id is:%x\n", thdId);
		sleep(1);
     }
    return 0;
}


线程的等待退出


3.1 等待线程退出

线程从入口点函数自然返回时,函数返回值可以被其它线程用 pthread_join 函数获取

#include 
int pthread_join(pthread_t th, void **thread_return)

该函数是一个阻塞函数,一直等到参数 th 指定的线程返回;与多进程中的 wait 或 waitpid 类似。

thread_return 是一个传出参数,接收线程函数的返回值。
1. 如果线程通过调用 pthread_exit()终止,则pthread_exit()中的参数相当于自然返回值,
照样可以被其它线程用 pthread_join 获取到。
2. thid 传递 0 值时,join 返回 ESRCH 错误。

接一个指针类型的退出状态。

char *pRet=NULL;
pthread_join(thid,(void**)&pRet);

接一个long类型的变量,用这个变量的值表示线程不同的退出状态。

long threadRet=0;
pthread_join(thid,(void**)&threadRet);

该函数还有一个非常重要的作用,由于一个进程中的多个线程共享数据段,因此通常在一个线程退出后,退出线程所占用的资源并不会随线程结束而释放。如果 th 线程类型并不是自动清理资源类型的

则 th 线程退出后,线程本身的资源必须通过其它线程调用 pthread_join 来清除,这相当于多进程程序中的 waitpid。

pthread_join 不回收堆内存的,只回收线程的栈内存和内核中的 struct task_struct 结构占用的内存。


3.2 线程的取消和终止清理函数


3.2.1 线程的取消

一个线程可以被其他线程杀死(取消),收到cancel信号的线程,可能直接结束,也可能忽略,可能运行到取消点(calcel-point),线程再结束(线程默认的行为)。

  pthread_cancel(pthread_t thid);

给目标线程发送cancel信号,发送信号后就返回。

3.2.2 线程的资源清理函数

线程间在访问一些共享资源时,需要独占的使用这些资源,先加锁,加锁成功,才能访问资源,访问资源结束后,解锁。假如一个线程加锁成功之后,访问资源时线程终止,不会去解锁,资源没有被释放,其他线程想访问资源的时候,永远都不能加锁成功,其他线程就没办法正常运行。

线程的终止清理函数可以处理这样的问题。

 pthread_cleanup_push(void(*cleanfun)(void*),void* arg);
 pthread_cleanup_pop(int execute);

这两个函数必须配对使用,并且在同一个级别的代码段中。

pthread_cleanup_push()/pthread_cleanup_pop()
是以宏方式实现的,这是 pthread.h 中的宏定义:
#define pthread_cleanup_push(routine,arg) \ {
 struct _pthread_cleanup_buffer _buffer; \
 _pthread_cleanup_push (&_buffer, (routine), (arg)); 
 #define pthread_cleanup_pop(execute) \
 _pthread_cleanup_pop (&_buffer, (execute));
}
可见,pthread_cleanup_push()带有一个"{",而 pthread_cleanup_pop()带有一个"}",
因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。

清理函数采用先入后出的栈结构管理。

清理函数会得到执行的3种情况。

  1. 线程被cancel,会执行清理函数。
  2. 线程通过pthread_exit()退出,会执行清理函数。
  3. 线程通过pthread_cleanup_pop(1),显式的弹栈并执行清理函数。

清理函数不会执行的2种情况。

  1. pthread_cleanup_pop(0)的方式,不会执行清理函数。
  2. 线程通过return 结束,清理函数不会执行。

演示

#include 
#include 
#include 
void freemem(void * args)
{
 free(args);
 printf("clean up the memory!\n");
}
void* threadfunc(void *args)
{
	 char *p = (char*)malloc(10); //自己分配了内存
	 pthread_cleanup_push(freemem,p);
	 int i = 0;
	 for(; i < 10; i++)
	 {
		 printf("hello,my name is wangxiao!\n");
		 sleep(1);
	 }
	 pthread_exit((void*)3);
	 pthread_cleanup_pop(0);
}
int main()
{
 pthread_t pthid;
 pthread_create(&pthid, NULL, threadfunc, NULL);
 int i = 1;
 for(; i < 5; i++) 
 {
	 printf("hello,nice to meet you!\n");
	 sleep(1);
	 if(i % 3 == 0)
	 pthread_cancel(pthid); 
 }

 int retvalue = 0;
 pthread_join(pthid,(void**)&retvalue); //等待子线程释放空间,并获取子线程的返回值
 printf("return value is :%d\n",retvalue);
 return 0;
}


线程的同步与互斥


4.1 线程的互斥

在 Posix Thread 中定义了一套专门用于线程互斥的 mutex 函数。

  1. mutex 是一种简单的加锁的方法来控制对共享资源的存取,这个互斥锁只有两种状态(上锁和解锁)
  2. 为什么需要加锁,就是因为多个线程共用进程的资源,要访问的是公共区间时(全局变量)

互斥锁的工作原理

  • 当一个线程访问的时候,需要加上锁以防止另外的线程对它进行访问,实现资源的独占。在一个时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作。
  • 若其他线程希望上锁一个已经上锁了的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。

4.1.1 创建和销毁锁

采用 pthread_mutex_init()函数来初始化互斥锁,
API 定义如下:

#include 
 int pthread_mutex_init(pthread_mutex_t *mutex, 
 const pthread_mutexattr_t *mutexattr)

其中 mutexattr 用于指定互斥锁属性(见下),如果为 NULL 则使用缺省属性。通常为 NULL

pthread_mutex_destroy()用于注销一个互斥锁,
API 定义如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。

由于在 Linux 中,互斥锁并不占用任何资源,因此 Linux Threads 中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回 EBUSY)没有其他动作。


4.1.2 锁操作

锁操作主要包括

加锁 int pthread_mutex_lock(pthread_mutex_t *mutex)
解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex)
测试加锁 int pthread_mutex_trylock(pthread_mutex_t *mutex)

pthread_mutex_lock:加锁,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回 EPERM;

在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。

pthread_mutex_unlock:根据不同的锁类型,实现不同的行为:
对于快速锁,pthread_mutex_unlock 解除锁定;

pthread_mutex_trylock:语义与 pthread_mutex_lock()类似,不同的是在锁已经被占据时返回 EBUSY而不是挂起等待。


4.1.3 加锁注意事项

注意:

1. 如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,
因此如果在关键区段内有取消点存在,则必须在退出回调函数
pthread_cleanup_push/pthread_cleanup_pop 中解锁。

2. 同时不应该在信号处理函数中使用互斥锁,否则容易造成死锁。

死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

死锁产生的原因

  1. 系统资源的竞争
    系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。
  2. 进程运行推进顺序不合适
    进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。

死锁的四个必要条件

互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
循环等待条件: 若干进程间形成首尾相接循环等待资源的关系

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。



死锁的预防

我们可以通过破坏死锁产生的 4 个必要条件来 预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。

破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新
获得自己原有的资源以及新申请的资源才可以重新启动,执行。

破坏”请求与保持条件“
第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。
第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。

破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。


演示:

1. 定义一个全局的 pthread_mutex_t lock; 或者用
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //则 main 函数中不用 init
2. 在 main 中调用 pthread_mutex_init 函数进行初始化
3. 在子线程函数中调用 pthread_mutex_lock 加锁
4. 在子线程函数中调用 pthread_mutex_unlock 解锁
5. 最后在 main 中调用 pthread_mutex_destroy 函数进行销毁
#include 
#include 
int ticketcount = 20;
pthread_mutex_t lock;
void* salewinds1(void* args)
{
	 while(1)
	 {
	 pthread_mutex_lock(&lock); //因为要访问全局的共享变量,所以就要加锁
	 if(ticketcount > 0) //如果有票
	 {
		 printf("windows1 start sale ticket!the ticket is:%d\n",ticketcount);
		 sleep(3); //卖一张票需要 3 秒的操作时间
		 ticketcount --; //出票
		 printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
	 }
	 else //如果没有票
	 {
		 pthread_mutex_unlock(&lock); //解锁
		 pthread_exit(NULL); //退出线程
		 pthread_mutex_unlock(&lock); //解锁
		 sleep(1); //要放到锁的外面,让另一个有时间锁
	 } 
 }
void* salewinds2(void* args)
{
 while(1)
 {
	 pthread_mutex_lock(&lock); //因为要访问全局的共享变量就要加锁
	 if(ticketcount>0) //如果有票
	 {
		printf("windows2 start sale ticket!the ticket is:%d\n",ticketcount);
		 sleep(3); //卖一张票需要 3 秒的操作时间
		 ticketcount --; //出票
		printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
	 }
	 else //如果没有票
	 {
		 pthread_mutex_unlock(&lock); //解锁
		 pthread_exit(NULL); //退出线程
	 }
	
		 pthread_mutex_unlock(&lock); //解锁
		 sleep(1); //要放到锁的外面,让另一个有时间锁
	 } 
 }
int main()
{
	 pthread_t pthid1 = 0;
	 pthread_t pthid2 = 0;
	 pthread_mutex_init(&lock,NULL); //初始化锁
	 pthread_create(&pthid1,NULL,salewinds1,NULL); //线程 1
	 pthread_create(&pthid2,NULL,salewinds2,NULL); //线程 2
	 pthread_join(pthid1,NULL);
	 pthread_join(pthid2,NULL);
	 pthread_mutex_destroy(&lock); //销毁锁
	 return 0;
}

4.2 线程的同步

条件变量是利用线程间共享的全局变量进行同步的一种机制。
主要包括两个动作:

- 一个线程等待条件变量的条件成立而挂起;
- 另一个线程使条件成立(给出条件成立信号)。

为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

4.2.1 条件变量的工作原理

条件变量完成了仅用锁完成不了的同步效果。协调了线程的执行顺序:

不使用条件变量: 每个线程访问共享资源时,都需要获取锁,之后再使用。

使用条件变量: 线程访问共享资源时,获取锁,被条件变量阻塞,加入到条件变量的阻塞队列中,之后开锁使其他线程获取锁继续访问共享资源并被条件变量阻塞,等待,之后等待被通知唤醒。


4.2.2 创建与注销

1. 创建API 定义如下:

int pthread_cond_init(pthread_cond_t *cond,
 pthread_condattr_t *cond_attr);

尽管 POSIX 标准中为条件变量定义了属性,但在 Linux Threads 中没有实现,因此 cond_attr 值通常为 NULL,且被忽略。

2. 销毁API 定义如下:

int pthread_cond_destroy(pthread_cond_t *cond);

注销一个条件变量需要调用 pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候能注销这个条件变量,否则返回 EBUSY。


4.2.3 等待和激发

等待条件有两种方式:

无条件等待 pthread_cond_wait()和计时等待

pthread_cond_timedwait():
int pthread_cond_wait(pthread_cond_t *cond, 
pthread_mutex_t *mutex);

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

线程解开 mutex 指向的锁并被条件变量 cond 阻塞。

其中计时等待方式表示经历 abstime 段时间后,即使条件变量不满足,阻塞也被解除。

激发条件有两种形式

  • pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个。
  • 而 pthread_cond_broadcast()则激活所有等待线程。

4.2.4 条件变量的使用

  1. 上半部
    • 在条件变量上面排队
    • 解锁
    • 睡眠
  2. 下半部
    1. 被唤醒
    2. 加锁
      1. 如果锁没有锁住,加锁成功
      2. 如果锁是锁住的,线程会阻塞,等到加锁成功为止
    3. 从pthread_cond_wait函数返回(线程从条件变量上被唤醒,不代表一定会立刻从pthread_cond_wait 函数返回。)

线程安全和线程属性


5.1 线程安全

线程安全:如果一个函数能够安全的同时被多个线程调用而得到正确的结果,那么,我们说这个函数是线程安全的。

线程安全产生的原因:大多是因为对全局变量和静态变量的操作。

可重入函数:描述的是函数被多次调用但是结果具有可再现性

(1)可重入概念只和函数访问的变量类型有关,和是否使用锁没有关系。 
(2)线程安全,描述的是函数能同时被多个线程安全的调用,并不要求调用函数的结果具有可再现性。
也就是说,多个线程同时调用该函数,允许出现互相影响的情况,这种情况的出现需要某些机制比如互斥锁来支持,使之安全。 
(3)可重入函数是线程安全函数的一种,其特点在于它们被多个线程调用时,不会引用任何共享数据。
(4)线程安全是在多个线程情况下引发的,而可重入函数可以在只有一个线程的情况下来说。
(5)线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
(6)如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
(7)如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
(8)线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响使结果是相同的。

5.2 线程的属性

走进 C/C++后台开发的第四步: Linux 多线程编程精讲_第3张图片

走进 C/C++后台开发的第四步: Linux 多线程编程精讲_第4张图片

演示

#include 
#include 
#include 
#include 

void *thread_function(void *arg);
char message[] = "Hello World";
int thread_finished = 0;
int main()
{
	 int res = 0;
	 pthread_t a_thread;
	 void *thread_result;
	 pthread_attr_t thread_attr; //定义属性
	 struct sched_param scheduling_value;
	 res = pthread_attr_init(&thread_attr); //属性初始化
	if (res != 0)
	{
		perror("Attribute creation failed");
		exit(EXIT_FAILURE); // EXIT_FAILURE -1 
	}
	 
	//设置调度策略
	res = pthread_attr_setschedpolicy(&thread_attr, 
	                                  SCHED_OTHER);
	if (res != 0)
	{
		perror("Setting schedpolicy failed");
		exit(EXIT_FAILURE);
	}
//设置脱离状态
 res = pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
//创建线程
 res = pthread_create(&a_thread, &thread_attr, thread_function, (void *)message);
 if (res != 0) {
	 perror("Thread creation failed");
	 exit(EXIT_FAILURE);
}
	//获取最大优先级别
	int max_priority = sched_get_priority_max(SCHED_OTHER);
	//获取最小优先级
	int min_priority = sched_get_priority_min(SCHED_OTHER);
	//重新设置优先级别
	scheduling_value.sched_priority = min_priority + 5;

//设置优先级别
	res = pthread_attr_setschedparam(&thread_attr, &scheduling_value);
	pthread_attr_destroy(&thread_attr);
	while(!thread_finished)
	{
		printf("Waiting for thread to say it's finished...\n");
		sleep(1);
	}
 	printf("Other thread finished, bye!\n");
	exit(EXIT_SUCCESS);
}
void *thread_function(void *arg) 
{
	printf("thread_function is running. Argument was %s\n", (char *)arg);
	sleep(4);
	printf("Second thread setting finished flag, and exiting now\n");
	thread_finished = 1;
	pthread_exit(NULL);
}

你可能感兴趣的:(走进,C/C++后台开发,操作系统,多线程,linux,c,面试)