多线程程序设计介绍

多线程程序设计介绍

1.进程和线程的介绍

1.1.两者的历史

最初的操作系统中没有进程和线程的概念,一个任务启动后就占用了整个机器的控制权,在这个任务执行完毕之前,没有办法执行其他任务。
假设一个任务A,在执行到过程中,需要读取大量的数据输入(I/O操 作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。

支持多任务操作系统:之后的操作系统中增加了多任务的概念,任务是指由一个或多个进程为达到一个目的进行的操作。其中每个进程对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程间互不干扰。这样就为进程切换提供了可能。
当进程挂起时,操作系统会保存当前进程的状态(比如进程标识、进程的使用的资源等),在下一次重新切换回来时,便根据之前保存的状态进行恢复,然后继续执行。
并且CPU时间分配由操作系统管理。由操作系统根据当前CPU状态和进程的优先级进行时间分配,从而保证每个进程都能分配到适合的CPU时间。
对应用开发者而言不用再花费精力进行CPU的时间分配,而认为自己的程序一直在占用CPU,只关注自己应用要实现的业务即可。而实际上一个进程运行时,并不会一直占用CPU时间,操作系统会根据进程属性,将CPU时间分配给其他进程,抢占当前进程的CPU时间。

支持多线程的操作系统:
在出现了进程之后,操作系统的性能得到了大大的提升,但是人们仍然不满足,人们逐渐对实时性交互有了要求。因为一个进程在一个时间段内只能做一件事情,如果一个进程有多个子任务,只能逐个地去执行这些子任务。
比如对于一个监控系统来说,它不仅要把图像数据显示在画面上,还要与服务端进行通信获取图像数据,还要处理人们的交互操作。如果某一个时刻该系统正在与服务器通信获取图像数据,而用户又在监控系统上点击了某个按钮,那么该系统就要等待获取完图像数据之后才能处理用户的操作,如果获取图像数据需要耗费 10s,那么用户就只有一直在等待。显然,对于这样的系统,人们是无法满足的。
于是便引入了线程概念,一个进程包含有多个线程,进程是资源分配的最小单位,而线程则是CPU调度的最小单位。一个进程中的线程可以共享此进程内部的地址、文件描述符等信息。CPU时间则由操作系统根据每一个线程的属性进行分配,从而保证每个线程都能分配到合适的CPU时间
所以在多线程系统中,上面监控系统的例子就可以用多线程实现,一个线程用于从服务器获取数据,一个线程用于响应用户交互。
当然这个例子可以使用多进程模式解决,一个进程用于和服务器端通讯,一个进程用于响应用户的操作。

实时系统和非实时系统:
一个实时系统是指计算的正确性不仅取决于程序的逻辑正确性,也取决于结果产生的时间,如果系统的时间约束条件得不到满足, 将会发生系统出错。
通用Linux下的实时调度:
通用Linux可以通过设置线程的优先级来实现实时调度,具体描述见:线程的优先级
但是通用Linux下实时调度存在如下问题,所以需要使用实时的操作系统
1、Linux 系统中的调度单位为10ms,所以它不能够提供精确的定时
2、当一个进程调用系统调用进入内核态运行时,它是不可被抢占的
3、Linux 内核实现中使用了大量的屏蔽中断操作会造成中断的丢失

实时操作系统系统下的调度:
RTAI ( Real-Time Application Interface )是一种实时的操作系统。它的基本思想是,为了在 Linux 系统中提供对于硬实时的支持,它实现了一个微内核的小的实时操作系统(我们也称之为 RT- Linux 的实时子系统),而将普通 Linux 系统作为一个该操作系统中的一个低优先级的任务来运行。
并且Linux 系统中一般的定时精度为 10ms,即时钟周期是 10ms,而 RT- Linux 通过将系统的实时时钟设置为单次触发状态,可以提供十几个微秒级的调度粒度。

1.2.开发时的选择

我们按照多个不同的维度,来看看多线程和多进程的对比
维度 多进程 多线程 总结
数据共享同步 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势
内存
CPU 占用内存多,切换复杂
CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优
创建
销毁
切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 线程占优
编程
调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉 进程占优

1、需要大量资源共享的优先选择线程
2、需要频繁创建销毁的优先用线程

2.一个简单的多线程示例

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

void* Func(void* pParam);

int main()
{

int iData = 3;

pthread_t ThreadId;
pthread_create(&ThreadId, NULL, Func, &iData);

for(int i=0; i<3; i++)
{
	printf("this is main thread\n");
	sleep(1);
}

pthread_join(ThreadId, NULL);

return 1;

}

void* Func(void* pParam)
{

Fopen

int* pData = (int *)pParam;

for(int i=0; i<*pData; i++)
{
	printf("this is Func thread\n");
	sleep(2);
}
fclose
return NULL;	

}

编译运行如下:
[root@localhost thread_linuxprj]# g++ -g -o thread_test thread_test.cpp –lpthread

[root@localhost thread_linuxprj]# ./thread_test
this is main thread
this is Func thread
this is main thread
this is Func thread
this is main thread
this is Func thread

此程序一共有两个线程:主线程和执行函数Func的线程,通过输出结果可以看到这两个线程同时执行输出操作。

2.1.线程的启动

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

参数说明:
pthread_t *thread:创建线程对应的ID,线程的唯一标志。
const pthread_attr_t *attr:线程的属性,可以设置线程的调度模式及优先级、DETACH/JOIN模式。一般设置为NULL,表示按照默认属性。
void *(start_routine) (void ):线程所执行的函数指针,这个函数必须有一个void类型的参数,返回值必须是void类型。
void *arg:线程所执行函数的参数,可以设置为NULL。

返回值:
0表示成功, -1表示失败,可以使用errno获取失败原因。

2.2.线程的停止

2.2.1.线程自动停止

线程函数运行完毕后,线程自动停止。上面例子中Fun函数return后,对应的线程会自动停止,其返回值可以供其他线程获取。

也可以显式的调用pthread_exit函数停止线程。

一般情况下直接返回即可。

2.2.2.外部通知线程停止

可以使用pthread_cancel函数发出信号通知线程停止,线程收到信号后,根据自己的退出属性进行退出,或者其他操作。

一般不建议使用,因为目的线程默认对此操作是直接退出,这样会造成线程内部申请的资源无法释放,出现资源泄漏。建议使用线程自动退出模式。

但是现实场景中往往出现要一个线程通知另一个线程退出,如下面例子:

void* Func(void* pParam)
{
while(true)
{
进行相关的逻辑处理
}

int main()
{

pthread_t Thread1;
pthread_create(&Thread1, NULL, Func, NULL);

等待退出信号
请求子线程退出

主线创建了子线程Thread1,然后处于等待退出状态,当收到外部的退出信号后,通知子线程退出。

子线程启动后,不断进行自己的逻辑处理,一直到收到主线程的退出信号后,才进行退出操作。

因为不建议使用pthread_cancel,可以使用如下模式解决

bool g_ThreadExitFlag = false;

void* Func(void* pParam)
{
while(!g_ThreadExitFlag)
{
进行相关的逻辑处理
}

	清除本线程所申请的资源
return NULL;

int main()
{
pthread_t Thread1;
pthread_create(&Thread1, NULL, Func, NULL);

等待退出信号

g_ThreadExitFlag = true;

pthread_join(Thread1, NULL);

主线程和Thread1之间使用一个共享变量g_ThreadExitFlag,Thread1所执行的线程函数中,每一次逻辑循环都判断一下若g_ThreadExitFlag为true则清除自身申请的资源,并return,Thread1线程即自动退出。

2.2.3.关于pthread_join

执行man函数的主线程退出后,本进程中的其他线程不管执行到什么位置都会立即退出。这样子线程的逻辑因为没有执行完毕,就会有和期望不符的情况出现。
所以上面的例子中主线程屏蔽了pthread_join函数的调用,则编译运行后输出如下:
this is main thread
this is Func thread
this is main thread
this is Func thread
this is main thread

按照代码Func需要打印3次,而实际运行时,因为Fun还未返回时,man函数已经return了,所有线程都被终止,所以Func只打印了2次。

pthread_join函数原型如下:

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

pthread_t thread_id:等待回收资源的线程id
void **retval:对应线程函数的返回值,若不关注,可以设置为NULL

pthread_join用于等待thread_id线程退出,并回收对应线程的资源,若对应的线程没有退出,则一直处于等待状态。

线程创建时可以通过pthread_attr_t参数指定当前线程是join模式还是detach模式。若线程是join模式,则必须使用pthread_join函数回收线程资源。如果不用关注线程的退出,则可以将线程设置为detach模式,此时不用调用pthread_join函数回收线程资源,自然pthread_join也无法等待detach模式的线程。

3.线程之间的竞争和同步

多线程程序设计时,往往会出多个线程同时对一块内存区进行读写现象,这样就会造成多线程冲突,出现结果和期望不符的情况。如下例子:

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

int g_iData = 0;
void* Func1(void* pParam);
void* Func2(void* pParam);

int main()
{
pthread_t Thread1, Thread2;
pthread_create(&Thread1, NULL, Func1, NULL);
pthread_create(&Thread2, NULL, Func2, NULL);

pthread_join(Thread1, NULL);
pthread_join(Thread2, NULL);

return 1;

}

void* Func1(void* pParam)
{
for (int i=0; i<3; i++)
{
g_iData = 1;

    sleep(1);

    printf("Func1 print g_iData:%d\n", g_iData);
}

return NULL;	

}

void* Func2(void* pParam)
{

for (int i=0; i<3; i++)
{
    g_iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);
}

return NULL;		

}

除了主线程外,还有两个工作线程分别运行Func1和Func2,预期Func1打印“Func1 print 1”三次,Fun2打印“Func2 print 2”三次。
实际运行结果如下:
[root@localhost thread_linuxprj]# ./thread_test
Func2 print g_iData:1
Func1 print g_iData:1
Func1 print g_iData:1
Func2 print g_iData:1
Func1 print g_iData:2
Func2 print g_iData:2

[root@localhost thread_linuxprj]# ./thread_test
Func2 print g_iData:1
Func1 print g_iData:1
Func1 print g_iData:1
Func2 print g_iData:1
Func1 print g_iData:2
Func2 print g_iData:2

[root@localhost thread_linuxprj]# ./thread_test
Func1 print g_iData:1
Func2 print g_iData:1
Func2 print g_iData:2
Func1 print g_iData:2
Func1 print g_iData:1
Func2 print g_iData:1

从运行结果可以看到,打引出很多非期望的内容,而且每次运行打印出的都不一样。
这就体现了多线程程序的特点,因为多个线程共享进程数据,同步比较复杂,而且一旦出现问题时,因为线程调度依赖于操作系统,而调度的顺序不同,程序对外的表现也不同,容易出现很多非必现的问题,给定位造成了很多困难。

所以需要尽量在程序设计以及编码时设计好多线程的同步。

3.1.函数若没有必要访问相同资源则尽量避免

如果线程调用的函数没有访问公共的资源(内存,文件描述符),那就根本不会存在多线程冲突问题。所以多线程程序编写时,如果没有必要就不要访问相同的资源。

如上面的多线程冲突的例子中,因为两个线程函数访问了相同的内存:全局变量g_iData,如果所访问的不是全局变量,而是自己栈中的变量就不存在这个问题。
void* Func1(void* pParam)
{
for (int i=0; i<3; i++)
{
int iData = 1;

    sleep(1);

    printf("Func1 print g_iData:%d\n",  iData);
}

return NULL;	

}

void* Func2(void* pParam)
{

for (int i=0; i<3; i++)
{
    int iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n",  iData);
}

return NULL;		

}

但是之所以采取多线程程序设计,一般情况下都是因为多个任务要访问相同的资源,所以多线程访问相同的资源,是一个不可避免的情况。这就需要用下面的技术加以解决。

3.2.互斥锁

如下代码所示,使用g_mutex即可控制同时只有一个线程在访问相同的资源。(注意,锁中不建议使用Sleep操作,下面例子只是为了更好的展示多线程冲突而在锁中加了sleep)

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

int g_iData = 0;
void* Func1(void* pParam);
void* Func2(void* pParam);

pthread_mutex_t g_mutex;

int main()
{
pthread_mutex_init(&g_mutex, NULL);

pthread_t Thread1, Thread2;
pthread_create(&Thread1, NULL, Func1, NULL);
pthread_create(&Thread2, NULL, Func2, NULL);

pthread_join(Thread1, NULL);
pthread_join(Thread2, NULL);

pthread_mutex_destroy(&g_mutex);

return 1;

}

void* Func1(void* pParam)
{
for (int i=0; i<3; i++)
{
pthread_mutex_lock(&g_mutex);

    g_iData = 1;

    sleep(1);

    printf("Func1 print g_iData:%d\n", g_iData);

    pthread_mutex_unlock(&g_mutex);
}

return NULL;	

}

void* Func2(void* pParam)
{

for (int i=0; i<3; i++)
{
    pthread_mutex_lock(&g_mutex);

    g_iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);

    pthread_mutex_unlock(&g_mutex);
}

return NULL;		

}

某一个线程运行执行了pthread_mutex_lock(&g_mutex) ,占用了g_mutex之后,其他线程如果运行到了pthread_mutex_lock(&g_mutex)代码后,就处于等待状态。直到占用g_mutex的线程调用pthread_mutex_unlock (&g_mutex)释放g_mutex之后,操作系统从其他等待的线程中分配一个能执行pthread_mutex_lock(&g_mutex),占用锁,并进行后续逻辑处理,没有占用到锁的线程继续处于等待状态。

使用互斥锁并不是锁住一块内存,而是锁住操作。

锁在使用前必须使用pthread_mutex_init函数进行初始化操作,当锁不再被使用时,必须调用pthread_mutex_destroy进行销毁操作。

3.2.1.注意事项

加锁后一定要记得进行解锁操作
这个概念很容易理解,但是使用中往往容易出现问题,凡是使用锁一定要对每个解锁的地方进行检查,例如
void Func()
{
pthread_mutex_lock(&g_mutex)
if(…)
{
pthread_mutex_unlock (&g_mutex)
return;
}
else if(…)
{
If(…)
{
pthread_mutex_lock(&g_mutex)
Call XXX
pthread_mutex_unlock (&g_mutex)
return
}
else
{
if(…)
{
If()
{
//此处退出未解锁会造成死锁!
Return;
}
… …
pthread_mutex_unlock (&g_mutex)
return;
}
}
pthread_mutex_unlock (&g_mutex)
return;
}

pthread_mutex_unlock (&g_mutex)
}

尽量不要在锁中做耗时较长的操作
如果一个锁的范围中做了耗时较长的算法,那么一旦有线程执行这段操作,就会造成其他需要占用此锁的线程处于长时间等待状态,一直到耗时较长的操作完毕为止,这样会极大的降低程序的效率。

常见的耗时操作:
消耗CPU较长时间的算法、对IO进行读写操作、Sleep。

某写情况下若涉及到多个线程对同一个IO进行读写操作(例如多个线程对同一个文件进行操作),此时不建议对IO加锁,而是需要更改程序设计,转变为只有一个线程对IO进行操作。

例如线程A、线程B同时要写一个文件,可以修改为线程A、线程B同时写一块内存,新增一个线程C把这块内存中的数据刷新到文件。这样只有一个线程访问文件,不涉及多线程访问IO,只要对共用的内存加锁即可。

尽量不要在一个加锁范围内,对另一个锁进行加锁操作
在A锁加锁的范围内,对锁B进行加锁操作,这样锁A如果要解锁就必须要等待到当前线程对锁B加锁成功,即锁A依赖锁B。
这样的代码很容易出现循环依赖,一段代码中锁A依赖锁B,另一段代码中锁B依赖锁A。这样两个线程执行这两段代码就会出现死锁。例如:

void Func1(void* pParam)
{
pthread_mutex_lock(&g_mutexA);

    … …
    pthread_mutex_lock(&g_mutexB);

    … …
		
		pthread_mutex_unlock(&g_mutexB);

   pthread_mutex_unlock(&g_mutexA);

}

void Func2(void* pParam)
{

    pthread_mutex_lock(&g_mutexB);

    … …
    pthread_mutex_lock(&g_mutexA);

    … …
		
		pthread_mutex_unlock(&g_mutexA);

   pthread_mutex_unlock(&g_mutexB);

}

若线程1执行到Func1中的pthread_mutex_lock(&g_mutexB);时候,线程2执行到pthread_mutex_lock(&g_mutexA);此时因为线程2已经执行了pthread_mutex_lock(&g_mutexB);操作,所以线程1处于等待对g_mutexB加锁状态,而同时又因为线程1已经对g_mutexA加锁,则线程2处于等待g_mutexA状态。
这样两个线程永远处于等待状态出现死锁。

加锁的范围要尽量小
加锁范围越大,越容易出现上面的各种问题,也越不利于后续代码的更新维护,所以加锁范围尽量要小。

使用自动锁以避免出现死锁
可以使用C++对操作系统API进行封装,实现自动锁模式,在自动锁生命期内,自动对代码加锁,在自动锁生命期终止,自动解锁,从而可以解决大部分死锁问题。上面例子用自动锁实现如下:

CLock_CS lock_cs;
void Func()
{
AUTO_CRITICAL_SECTION(lock_cs)
if(…)
{
//自动锁生命周期结束,自动解锁。
return;
}
else if(…)
{
If(…)
{
//自动锁生命周期结束,自动解锁。
return
}
else
{
if(…)
{
If()
{
//自动锁生命周期结束,自动解锁。
Return;
}
… …
//自动锁生命周期结束,自动解锁。
return;
}
}
//自动锁生命周期结束,自动解锁。
return;
}

//自动锁生命周期结束,自动解锁。
}
自动锁实现可参考svn如下代码
https://192.168.20.6:8443/svn/hnc8/trunk/apidev/net/comm/src/criticalsection.h

使用读写锁以提高效率
应用场景:多个线程进行读写的操作。写的时候只能由一个进行写,其他读写操作都处于等待状态。读的时候因为对公共资源不会进行任何修改,可以同时读,以提高效率。

但如果使用互斥锁,一旦一个线程占用了锁,其他线程就无法占用,处于等待锁状态。所以无法满足同时读的需求。

可以使用读写锁来实现:

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

3.3.条件变量

使用互斥锁,只能保证同时只能有个线程执行加锁范围内的代码,但是无法保证每个线程的执行顺序。如果开发时涉及到需要保证某个线程执行完毕后,后续线程才能执行,则建议使用条件变量。
如下面的例子,线程1、2、3同时启动,当线程1执行完毕后,才会唤醒线程2或3中的一个进行后续操作。

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

int g_iData = 0;
bool g_bNewThreadRun = false;
void* Func1(void* pParam);
void* Func2(void* pParam);
void* Func3(void* pParam);

pthread_mutex_t g_mutex;
pthread_cond_t g_cond;

int main()
{
pthread_mutex_init(&g_mutex, NULL);
pthread_cond_init(&g_cond, NULL);

pthread_t Thread1, Thread2, Thread3;
pthread_create(&Thread1, NULL, Func1, NULL);
pthread_create(&Thread2, NULL, Func2, NULL);
 //pthread_create(&Thread3, NULL, Func3, NULL);

pthread_join(Thread1, NULL);
pthread_join(Thread2, NULL);
 pthread_join(Thread3, NULL);

pthread_cond_destroy(&g_cond);
pthread_mutex_destroy(&g_mutex);

return 1;

}

void* Func1(void* pParam)
{

for (int i=0; i<3; i++)
{

    g_iData = 1;

    sleep(1);

    printf("Func1 print g_iData:%d\n", g_iData);   
}

g_bNewThreadRun = true;
 pthread_cond_signal(&g_cond);


return NULL;	

}

void* Func2(void* pParam)
{
pthread_mutex_lock(&g_mutex);
while(!g_bNewThreadRun)
{
pthread_cond_wait(&g_cond, &g_mutex);
}
g_bNewThreadRun = false;
for (int i=0; i<3; i++)
{
g_iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);

}

pthread_mutex_unlock(&g_mutex);
return NULL;		

}

void* Func2(void* pParam)
{
pthread_mutex_lock(&g_mutex);
while(!g_ bNewThreadRun)
{
pthread_cond_wait(&g_cond, &g_mutex);
}

    g_bNewThreadRun = false;

for (int i=0; i<3; i++)
{
    g_iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);
}

pthread_mutex_unlock(&g_mutex);

return NULL;		

}

线程1、2、3启动,其中线程1因为没有锁,所以立即执行,并将设置g_bNewThreadRun=true,之后触发条件变量pthread_cond_signal(&g_cond);
线程2首先执行pthread_mutex_lock(&g_mutex);加锁操作,之后调用pthread_cond_wait(&g_cond, &g_mutex)函数,进入此函数首先会进行解锁,之后处于等待g_cond生效状态。
同理线程3执行pthread_mutex_lock(&g_mutex)后之后调用pthread_cond_wait(&g_cond, &g_mutex)函数,处于等待g_cond生效状态。
当线程1触发条件变量g_cond之后,线程2或3中会有一个会被唤醒。pthread_cond_wait(&g_cond, &g_mutex)函数返回,在返回前会对g_mutex进行加锁,以保证后续操作只能有一个线程执行。当执行完毕后,再调用pthread_mutex_unlock(&g_mutex)进行解锁。

3.3.1.注意事项

设置条件有效时,必须在触发pthread_cond_signal之前
因为线程的运行顺序是由操作系统调度,上面的例子中,如果线程1调用pthread_cond_signal之后,再设置g_ bNewThreadRun为true,就有可能在这两步操作之间,线程2或3的pthread_cond_wait被唤醒,结果判断g_ bNewThreadRun仍未false,于是继续调用pthread_cond_wait处于等待状态,造成条件变量生效信号丢失。

在调用pthread_cond_wait之前,必须要对传入的锁进行加锁操作。
根据上面的例子可以看到,进入pthread_cond_wait函数后,系统首先会对传入的锁进行解锁,当等待到条件变量生效后,会对传入的锁加锁后,再返回,这样才能保证后续对公共资源的操作只能有一个线程在进行。
所以在调用pthread_cond_wait之前,必须要对传入的锁进行加锁操作。若没有加锁,则执行结果不可预期。

调用pthread_cond_wait时,需要使用while循环判断进行保护
因为线程的运行顺序是由操作系统调度,上面的例子中,可能会出现线程1已经触发了条件变量信号之后线程2才运行到pthread_cond_wait,此时就会出现条件变量生效信号丢失,线程2永远处于pthread_cond_wait状态。所以一般情况下需要在调用pthread_cond_wait之前,判断当前状态下是否可以立即执行。
if(!g_ bNewThreadRun)
{
pthread_cond_wait(&g_cond, &g_mutex);
}
… …
同时因为系统中断也可能导致pthread_cond_wait返回,而此时可能条件变量信号还未生效。所以需要变为使用while循环判断当前状态下是否可以执行。
while(!g_ bNewThreadRun)
{
pthread_cond_wait(&g_cond, &g_mutex);
}
… …
条件变量重复触发,pthread_cond_wait只能获取一次。
根据上面的解释,在一个线程进行pthread_cond_wait之前,另一个线程设置条件变量信号有效,那这个信号就会丢失。
所以如果线程1pthread_cond_signal同时触发多次,则线程2的pthread_cond_wait可能只会唤醒一次,因为其余的pthread_cond_signal触发时,线程2可能正在执行后续操作,没有处于pthread_cond_wait状态。
如果需要信号触发一次,则业务处理线程就需要执行一次,则建议使用信号量模式。

3.4.信号量

信号量可以用于实现典型的生产者-消费者模式,如下代码例子:

#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <time.h>
#include <stdio.h>
#include <errno.h>
#include

using std::list;

pthread_mutex_t List_Mutex;
list g_PdtList;

sem_t g_HavePdtSem;

bool g_bExit = false;

void PutPdt(int iPdt)
{
pthread_mutex_lock(&List_Mutex);

g_PdtList.push_back(iPdt);

pthread_mutex_unlock(&List_Mutex);

}

void GetPdt(int& iPdt)
{
pthread_mutex_lock(&List_Mutex);

iPdt = g_PdtList.front();
g_PdtList.pop_front();

pthread_mutex_unlock(&List_Mutex);

}

void* ProducterFunc(void* pParam);
void* ConsumerFunc(void* pParam);

int main()
{
pthread_mutex_init(&List_Mutex, NULL);
sem_init(&g_HavePdtSem, 0, 0);

const int MAX_CONSUMER_NUM = 3;

pthread_t ProductThread;
pthread_t ConsumerThread[MAX_CONSUMER_NUM];

pthread_create(&ProductThread, NULL, ProducterFunc, NULL);

for (int i=0; i<MAX_CONSUMER_NUM; i++)
{
    pthread_create(&ConsumerThread[i], NULL, ConsumerFunc, NULL);
}

sleep(3);
g_bExit = true;


pthread_join(ProductThread, NULL);

for (int i=0; i<MAX_CONSUMER_NUM; i++)
{
    pthread_join(ConsumerThread[i], NULL);
}


sem_destroy(&g_HavePdtSem);
pthread_mutex_destroy(&List_Mutex);

return 1;

}

void* ProducterFunc(void* pParam)
{
for (int i=0; i<5; i++)
{
printf(“Producter make pdt:%d\n”, i);
PutPdt(i);

    sem_post(&g_HavePdtSem);
}

return NULL;	

}

void* ConsumerFunc(void* pParam)
{
static int i = 0;
int iIndex = i++;

while(!g_bExit)
{
    timespec abstime;
    clock_gettime(CLOCK_REALTIME, &abstime);

    abstime.tv_sec += 3;
    if (sem_timedwait(&g_HavePdtSem, &abstime) == -1)
    {
        if (errno == ETIMEDOUT)
        {
            continue;
        }
    }

    int iPdt = -1;
    GetPdt(iPdt);

    printf("Consumer[%d] get pdt:%d\n", iIndex, iPdt);
}

return NULL;		

}

编译运行结果如下:
[root@localhost sem_linux_test]# ./thread_test
Producter make pdt:0
Producter make pdt:1
Producter make pdt:2
Consumer[2] get pdt:0
Consumer[0] get pdt:1
Consumer[1] get pdt:2
Producter make pdt:3
Producter make pdt:4
Consumer[0] get pdt:3
Consumer[2] get pdt:4

主函数启动时调用sem_init(&g_HavePdtSem, 0, 0);初始化了一个信号量。
之后主函数创建了一个Producter线程和3个Consumer线程,其中Producter一共生产了6个产品,每创建一次产品则调用sem_post(&g_HavePdtSem); g_HavePdtSem信号量的值+1。
每个Consumer线程创建成功后,调用sem_timedwait(&g_HavePdtSem, &abstime),因为信号量初始化时设置的初始值0(sem_init第三个参数为0)则处于等待信号量生效状态。当获取到一次生效的信号量后,sem_timedwait返回,并将g_HavePdtSem信号量的值-1。直到信号量的值降为0为止。

4.线程的优先级

线程属性中包含有线程的调度策略和线程的优先级的信息,在创建线程时可以通过线程的属性设置线程的优先级。不过一般情况下多线程设计很少涉及到线程优先级的修改。
当线程属性中调度策略有如下三个类型。
	SCHED_FIFO:实时调度,先进先出,线程启动后一直占用CPU运行,一直到有比此线程优先级更高的线程处于就绪态才释放CPU
	SCHED_RR:实时调度,时间片轮转算法,当线程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。
	SCHED_OTHER:分时调度,默认算法。

只有当线程的调度策略是SCHED_FIFO和SCHED_RR时,设置的线程优先级才有效。

可以使用如下函数修改线程的调度算法和优先级
int pthread_attr_setschedparam(pthread_attr_t *attr,
const struct sched_param *param);

int pthread_attr_setschedpolicy(pthread_attr_t *attr,
int policy);

5.Linux和Win之间的区别

以上都是Linux下的线程操作,Win下线程操作API和Linux不同
线程启动、等待退出
uintptr_t _beginthreadex( void *security,
unsigned stack_size,
unsigned ( *start_address )( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr );

DWORD WINAPI WaitForSingleObject(HANDLE hHandle,
DWORD dwMilliseconds)

互斥锁
HANDLE WINAPI CreateMutex(
__in LPSECURITY_ATTRIBUTES lpMutexAttributes,
__in BOOL bInitialOwner,
__in LPCTSTR lpName
);

BOOL WINAPI ReleaseMutex(
__in HANDLE hMutex
);

Win环境下一般建议使用临界区来代替锁,临界区的调用效率远高于锁。

临界区
void WINAPI InitializeCriticalSection(
__out LPCRITICAL_SECTION lpCriticalSection
);
void WINAPI DeleteCriticalSection(
__in_out LPCRITICAL_SECTION lpCriticalSection
);

void WINAPI EnterCriticalSection(
__in_out LPCRITICAL_SECTION lpCriticalSection
);

void WINAPI LeaveCriticalSection(
__in_out LPCRITICAL_SECTION lpCriticalSection
);

同步事件
BOOL WINAPI SetEvent(
__in HANDLE hEvent
);

DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);

信号量
BOOL WINAPI ReleaseSemaphore(
__in HANDLE hSemaphore,
__in LONG lReleaseCount,
__out LPLONG lpPreviousCount
);
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);

6.可用的代码库

因为Windows和Linux下关于多线程操作的API都不一样,在同时支持Win和Linux代码中使用时,往往需要到处增加编译宏,不利于代码维护。
所以可以使用下封装好的代码和动态库(开源代码,无版权问题),以方便上层处理,不用增加编译宏。

猜你喜欢

转载自blog.csdn.net/p309654858/article/details/132145206