首先通过一个简单的故事让大家了解什么是线程模型?
有兄弟三人开车出去自驾游, 旅途非常遥远,需要好几天的车程。所以需要三个人轮流开车,这个时候可以一个人开车另外两个人可以去睡觉了,这样大家轮流开车。当开车司机想睡觉的时候可以通过轻轻推醒一个人来接替开车另一个仍然可以接着睡,如果开车司机不高兴也可以通过大声说话的方式,将两个人都叫醒,但是也只能有一个人来开车。
这个故事可以用来比喻线程编程模型思想。这里的司机就是线程,汽车可以理解为共享数据,轻推和大声说话就是通信机制,个体在等待这些事件的发生。
多线程模型具有以下优点:
<1> 在多处理器系统中开发程序的并行性。除了并行性这一优点是需要特殊硬件支持外,其他优点对硬件不做要求。
在多处理器系统中,线程模式可以让一个进程同时执行多个独立运算。一个运行在双cpu上的计算密集型多线程程序几乎可以获得传统的单线程程序两倍的性能。对于并行能带来多大的性能提升,可以Amdahl法则预测:
在Amdahl法则登时中,p代表可并行代码与整个执行时间的比率,n代表代码可以使用的处理器的数目。并行工作真个延续时间就等于非并行时间(1-p)的延续时间加上每个处理器执行并行工作(p/n)的延续时间。
<2> 在等待慢速外设I/O操作结束的同时,程序可以执行其他计算,为程序的并发提供更有效,更自然的开发方式。
如果事件不是并发的,程序一次只能做一件事情,如大型数据库排序,用户界面可能相当长的时间内无法响应用户的操作;如果实践引发长时间等待,如通过低速网络连续读取数据,用户也只能再次等待。另一方面,你可以创建一个转么排序数据库的线程,或者从网上读取数据而让用户界面线程立即处理另一用户操作。慢速操作继续执行,而程序还可以相应。
<3> 一种模块化编程模型,能清晰地表达程序中独立事件的相互关系。
即使你的代码从不在多处理器系统上运行,了解线程模型仍然很有意义。线程模型将独立的或者松耦合的功能执行流(线程)显示的分离。如果活动设计为线程,那么每个函数必须包括显示的同步以确保依赖关系。因为同步机制就是可执行的代码,所以依赖性改变时也无法忽略他。同步结构的存在阅读代码的人了解代码中的时间依赖关系,使代码维护更加容易,尤其是对于包含大量独立代码的大型程序而言。
多线程模型的缺陷:
<1> 计算负荷
<2> 编程规则
<3> 更难调试
提供线程功能的系统通常将传统的串行调试工具扩展以提供基本的线程调试支持。系统会提供一个调试器,允许你看到所有线程的条用结构树,并设置只能在特定线程内激活的断点。系统可能提供某种形式的性能分析器,让你计算某个线程或者所有线程中函数的累计占有处理器时间。不幸的是,这仅仅是调试异步代码的开始,调试不可避免的改变事件的顺序。这在调试串行代码时不会有什么太大问题,但是调试异步代码时却是致命的。
何时使用多线程:
最适合使用线程的是实现以下功能的应用:
<1> 计算密集型应用,为了能在多处理器系统上运行,将这些计算分解到多个线程中实现;
多线程的创建和使用:
<1> 创建线程
#include <pthread.h>
pthread_create(thread, attr, start_routine, arg)
参数:
thread 指向线程标识符指针。
start_routine 线程运行函数起始地址,一旦线程被创建就会运行。
arg 运行函数的参数。
<2> 终止线程
#include <pthread.h>
pthread_exit(status)
#include
<
pthread.h
>
using
namespace
std
;
void
*
say_hello
(
void
*
args
)
{
cout
<<
"
Hello Runoob!
"
<<
endl
;
return
0
;
}
int
main
(
)
{
// 定义线程的 id 变量,多个变量使用数组
pthread_t
tids
[
NUM_THREADS
]
;
int
ret
=
pthread_create
(
&
tids
[
i
]
,
NULL
,
say_hello
,
NULL
)
;
if
(
ret
!=
0
)
{
cout
<<
"
pthread_create error: error_code=
"
<<
ret
<<
endl
;
}
}
//等各个线程退出后,进程才结束,否则进程强制结束了,线程可能还没反应过来;
pthread_exit
(
NULL
)
;
}
<3>连接和分离线程
pthread_join(threadid, status);
pthread_detach(threadid);
会定义它是否可连接的或可分离的。只有创建时定义为可连接的线程才可以被连接。如果线程创建时被定
义为可分离的,则永远也不能被连接。
#include
<
iostream
>
#include
<
cstdlib
>
#include
<
pthread.h
>
#include
<
unistd.h
>
using
namespace
std
;
#define
NUM_THREADS
5
void
*
wait
(
void
*
t
)
{
int
i
;
long
tid
;
tid
=
(
long
)
t
;
sleep
(
1
)
;
cout
<<
"
Sleeping in thread
"
<<
endl
;
cout
<<
"
Thread with id :
"
<<
tid
<<
"
...exiting
"
<<
endl
;
pthread_exit
(
NULL
)
;
}
int
main
(
)
{
int
rc
;
int
i
;
pthread_t
threads
[
NUM_THREADS
]
;
pthread_attr_t
attr
;
void
*
status
;
// 初始化并设置线程为可连接的(joinable)
pthread_attr_init
(
&
attr
)
;
pthread_attr_setdetachstate
(
&
attr
,
PTHREAD_CREATE_JOINABLE
)
;
for
(
i
=
0
;
i
<
NUM_THREADS
;
i
++
)
{
cout
<<
"
main() : creating thread,
"
<<
i
<<
endl
;
rc
=
pthread_create
(
&
threads
[
i
]
,
NULL
,
wait
,
(
void
*
)
&
i
)
;
if
(
rc
)
{
cout
<<
"
Error:unable to create thread,
"
<<
rc
<<
endl
;
exit
(
-
1
)
;
}
pthread_attr_destroy
(
&
attr
)
;
for
(
i
=
0
;
i
<
NUM_THREADS
;
i
++
)
{
rc
=
pthread_join
(
threads
[
i
]
, &
status
)
;
if
(
rc
)
{
cout
<<
"
Error:unable to join,
"
<<
rc
<<
endl
;
exit
(
-
1
)
;
}
cout
<<
"
Main: completed thread id :
"
<<
i
;
cout
<<
"
exiting with status :
"
<<
status
<<
endl
;
}
cout
<<
"
Main: program exiting.
"
<<
endl
;
pthread_exit
(
NULL
)
;
}
线程的同步:
线程同步指的是线程之间“协同”,即线程之间按照规定的先后次序执行。线程的同步方式有:
<1> 互斥量 pthread_mutex_
<2> 读写锁 pthread_rwlock_
<3> 条件变量 pthread_cond_
<4> 信号量 sem_
-
互斥锁
互斥锁(又名互斥量)强调的是资源之间的访问互斥:每个线程在对共享资源操作前都会尝试先加锁,加锁成功才能操作,操作结束之后解锁。某个线程对互斥量加锁后,任何其他试图再对互斥量加锁的线程都将被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态。第一个变成运行状态的线程可以对互斥量加锁,其余线程将会看到互斥量依然被锁住,只能回去再次等待它重新变为可用。mutex是睡眠等待(sleep waiting)类型的锁,当线程抢互斥锁失败的时候,线程会陷入休眠。优点就是节省cpu资源,缺点就是休眠唤醒会消耗一点时间。
#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
);
加锁解锁:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
超时加锁,避免死锁:
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timesec *restrict tsptr);
-
读写锁
读写锁和互斥量类似,是另一种实现线程同步的方式,但是它将操作分为读、写两种方式,可以多个线程同时占用读模式,这样使得读写锁具有更高的并行性。相较于互斥锁而言读写锁有一定的性能提升,应对的是单写多读模型:
-
写独占:写锁占用时,其他线程加读锁或者写锁时都会阻塞(并非失败)
-
读共享:读锁占用时,其他线程加写锁时会阻塞,加读锁会成功
读写锁有两种策略:
-
强读同步:读锁优先,只要写锁没有占用那么就可以加读锁
-
强写同步:写锁优先,只能等到所有正在等待或者执行的写锁执行完成后才能加读锁
大部分读写锁的实现都采用的是“强写同步”策略,对尝试加锁的操作进行排队,如果前面已经有尝试加写被锁阻塞住的话,后续加读锁也都会被阻塞住(尽管当前时刻是读锁占用的状态)。这样做的目的主要是为了避免“写饥饿”,在“多读少写”的情况下防止数据修改延迟过高。
#include<pthread.h>/*
* 功能: 初始化rwlock所指向的读写锁
* 返回值: 成功返回0, 失败返回非0错误码
*
* @param1 rwlock: 指向要初始化的读写锁指针
*/int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);/*
* 功能: 销毁rwlock所指向的读写锁
* 返回值: 成功返回0, 失败返回非0错误码
*
* @param1 rwlock: 指向要销毁的读写锁指针
*/int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);/*
* 功能: 加读锁
* 返回值: 成功返回0, 失败返回非0错误码
*
* @param1 rwlock: 读写锁指针
*/int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);// 注: 以阻塞方式在读写锁上获取读锁(读锁定), 如果没有写线程持有该锁并且没有写线程阻塞, 则可以获取读锁
/*
* 功能: 尝试加读锁
* 返回值: 成功返回0, 失败返回非0错误码
*
* @param1 rwlock: 读写锁指针
*/int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);// 注:用于尝试以非阻塞的方式来在读写锁上获取读锁, 如果有任何的写线程持有该锁或者有写阻塞在该锁上,则返回失败
/*
* 功能: 加写锁
* 返回值: 成功返回0, 失败返回非0错误码
*
* @param1 rwlock: 读写锁指针
*/int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);// 注: 在读写锁上获取写锁(写锁定), 如果没有写线程持有该锁且没有读线程持有该锁, 则调用的线程会获取该锁
/*
* 功能: 尝试加写锁
* 返回值: 成功返回0, 失败返回非0错误码
*
* @param1 rwlock: 读写锁指针
*/int pthread_rwlock_tryrwlock(pthread_rwlock_t *rwlock);// 注:用于尝试以非阻塞的方式来在读写锁上获取写锁, 如果有任何读线程或写线程持有该锁, 则立即返回失败
/*
* 功能: 尝试加写锁
* 返回值: 成功返回0, 失败返回非0错误码
*
* @param1 rwlock: 读写锁指针
*/int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);// 注: 无论是读锁还是写锁,都通过此函数解锁
-
条件变量
并发有互斥和等待两大需求,前者是因为线程间存在共享数据依赖而后者是线程间存在依赖,条件变量正是为了解决等待需求。
需要注意的是:
-
条件变量并不是锁(但它几乎总是和互斥量一起使用的),而是线程间的一种通讯机制
-
条件变量本身也不包含条件,它被称为条件变量是因为它经常和条件语句(if/while)一起使用
#include <pthread.h>/*
* 返回值: 成功返回0, 其他返回值表示错误
*
* @param1 cond: 待初始化的条件变量
* @parma2 attr: NULL表示创建default条件变量, 否则条件变量的属性由attr决
*/int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);/*
* 返回值: 成功返回0, 其他返回值表示错误
*/int pthread_cond_destroy(pthread_cond_t *cond);/*
* 返回值: 成功返回0, 其他返回值表示错误
*/int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);// pthread_cond_wait函数包含三步(前两步为一个原子操作):
// 1) 阻塞等待条件变量cond满足
// 2) 释放已掌握的互斥锁mutex
// 3) 当被唤醒时解除阻塞并重新申请获得互斥锁mutex
/*
* 返回值: 成功返回0, 其他返回值表示错误
*/int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);/*
* 返回值: 成功返回0, 其他返回值表示错误
*/int pthread_cond_signal(pthread_cond_t *cond);/*
* 功能: 唤醒全部阻塞在条件变量上的线程
* 返回值: 成功返回0, 其他返回值表示错误
*/
int pthread_cond_broadcast(pthread_cond_t *cond);
-
信号量
信号量分为有名信号量和无名信号量,无名信号量用于线程同步,有名信号量一般用于进程之间管理。
信号量本质上是一个非负的整数计数器,用于控制公共资源的访问,也被称为PV原子操作:
-
P操作:即信号量sem减一,若sem小于等于0则P操作被阻塞,直到sem变量大于0为止
-
V操作:即信号量sem加一
信号量允许多个线程同时进入临界区,而互斥量只允许一个线程进入临界区。
#include <semaphore.h>/*
* 功能: 创建信号量
* 返回值: 成功返回0, 失败返回-1
*
* @param1 sem: 要进行初始化的信号量对象
* @param2 pshared: 控制着信号量的类型, 如果值为0表示它是当前进程的局部信号量, 否则其他进程就能够共享这个信号量
*/int sem_init(sem_t *sem, int pshared, unsigned int value)/*
* 功能: 以原子操作的方式将信号量值减1
int sem_trywait(sem_t *sem);// sem_timedwait()与sem_trywait()功能类似, 只是在指定的abs_timeout时间内等待, 超时返回ETIMEDOUT错误
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);/*
* 功能: 以原子操作的方式将信号量的值加1
* 返回值: 调用成功时返回0,失败返回-1
*/int sem_post(sem_t *sem);/*
* 返回值: 如果当前信号量已经上锁(即同步对象不可用)那么返回值为0; 如果返回负数其绝对值就是等待该信号量解锁的线程数
*/int sem_getvalue(sem_t *restrict, int *restrict);/*
* 功能: 销毁信号量
* 返回值: 成功返回0, 失败返回-1
*/int sem_destory(sem_t *sem);