Linux中用进程标识符process ID(PID)来唯一的标识一个进程或轻量级进程(Linux中没有真正的线程,而是通过线程组或可以称为轻量级进程组实现的多线程进程)。PID被存放在进程描述符的pid字段中,还有一个字段较tgid用于标识一个进程组,即该组中的所有轻量级进程的线程组ID相同,其值等于领头轻量级进程的PID。我们常用的getpid函数返回的便是进程的tgid,而非pid。若想获取pid可以通过系统调用实现:::syscall(SYS_gettid)进行获取。注意:linux下的pthread_t结构不可直接打印。
一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码,程序的全局内存(包括未初始化数据全与已初始化数据区)和堆内存,栈以及文件描述符,但每个线程都有自己的errno。
【待补】:对线程在拥有哪些独立的资源还不太了解
关于线程的终止:如果进程中的任意线程调用了exit,那么整个进程就会终止。如果要单独退出某个线程则又3种方式:1)线程可以从启动例程中退出,返回值是线程的推出码(即从入口函数中退出)。2)线程可以被同一进程中的其它线程取消(在后面的小节进行说明)。3)调用pthread_exit。默认情况下线程的终止状态会被保存,直到对该线程调用pthread_join函数时由其第二个参数返回,之后pthread_join函数自动将线程至于分离状态,但如果线程已经被分离(pthread_detach),线程的底层存储资源可以在线程终止时立即被回收。此时不能使用pthread_join函数等待线程的终止状态,否则会产生未定义的行为。
关于具体的线程构造,终止,等待,分离函数以及线程退出处理程序(类似于进程的atexit)的使用在此处不进行详细说明,可以参考APUE p316,这里给出一个muduo对线程的一个封装,但做了一些改变,代码如下:
// CountDownLatch.h
// 这个类用于实现等待计数到0
#include //std::condition_variable
#include "base/nocopyable.h"
namespace HDU
{
namespace BASE
{
class CountDownLatch : public Nocopyable
{
public:
explicit CountDownLatch(int count) : _count(count)
{
}
void wait();
void countDown();
int getCount() const;
private:
int _count;
mutable std::mutex _mutex;
std::condition_variable _cond;
};
}
}
// CountDownLatch.cpp
#include "countdownlatch.h"
using namespace HDU;
using namespace HDU::BASE;
void CountDownLatch::wait()
{
std::unique_lock locker(_mutex);
while (_count > 0) {
_cond.wait(locker);
}
}
void CountDownLatch::countDown()
{
std::lock_guard locker(_mutex);
_count--;
if(_count == 0){
_cond.notify_all();
}
}
int CountDownLatch::getCount() const
{
std::lock_guard locker(_mutex);
return _count;
}
// thread.h
//-------------------------------------------------------------------
#include //std::function
#include //pthread_xxx
#include "base/countdownlatch.h" //CountDownLatch
namespace HDU
{
namespace CurrentThread
{
extern __thread pid_t t_cacheTid;
}
namespace BASE
{
class Thread : public Nocopyable
{
public:
typedef std::function ThreadRunFun;
Thread(const ThreadRunFun& runFun);
~Thread();
//启动线程
void start();
// 运行该函数的线程将一直等待至调用线程调用exitCurrentThread,从启动例程中返回或者被取消
// 注意:
// - 若线程简单的从它的启动例程返回,rval_ptr就包含返回码
// - 当一个线程调用exitCurrentThread退出或简单的从它的启动例程返回,
// 进程中的其他线程可以通过join函数获得该线程的退出状态
// - 若线程被取消,由rval_ptr指定的内存单元就设置为PTHREAD_CANCELED:(void*)-1
// - 若不关心线程的返回值,可以将rval_ptr设为NULL
// 警告:
// - 对分离状态的线程join()会产生未定义的行为
void join(void** rval_ptr);
// 获得调用线程的Id
pid_t tId() {
return _tid;
}
bool started() const {
return _started;
}
// 获得当前线程的Id
static pid_t getCurrentThreadId();
// 进程中的其它线程可以通过join()访问到这个指针
static void exitCurrentThread(void* rval_ptr);
private:
friend void* readyRun(void* obj);
ThreadRunFun _runFun;
bool _started;
bool _joined;
pid_t _tid;
pthread_t _pthreadId;
CountDownLatch _latch;
};
}
}
// thread.cpp
//-------------------------------------------------------------------------
#include //pid_t
#include //::syscall
#include //assert()
#include "thread.h"
#define MCHECK(ret){__typeof__(ret) errnum = (ret); \
assert(errnum == 0);\
}
using namespace HDU;
using namespace HDU::BASE;
namespace HDU
{
namespace CurrentThread
{
__thread pid_t t_cacheTid = 0;//记录后无需每次需要得知线程id时都进行系统调用
}
namespace BASE
{
void* readyRun(void* obj)
{
Thread* thread = static_cast(obj);
thread->_tid = Thread::getCurrentThreadId();
CurrentThread::t_cacheTid = thread->_tid;
thread->_latch.countDown();
thread->_runFun();
return NULL;
}
}
}
Thread::Thread(const Thread::ThreadRunFun &runFun) : _runFun(runFun),
_started(false),
_joined(false),
_tid(0),
_pthreadId(0),
_latch(1)
{
}
Thread::~Thread()
{
if(_started && !_joined){
MCHECK(pthread_detach(_pthreadId));
}
}
void Thread::start()
{
_started = true;
if(pthread_create(&_pthreadId, NULL, &readyRun, this)) {
//创建失败
_started = false;
}
else {
_latch.wait(); //等待线程创建完成
assert(_tid > 0);
}
}
void Thread::join(void** rval_ptr)
{
assert(_started);
assert(!_joined);
if(int err = pthread_join(_pthreadId, rval_ptr)) {
//join fail
//log _pthread + rval_ptr + err
(void)err;
}
}
pid_t Thread::getCurrentThreadId()
{
if(CurrentThread::t_cacheTid == 0){
CurrentThread::t_cacheTid = static_cast(::syscall(SYS_gettid));
}
return CurrentThread::t_cacheTid;
}
void Thread::exitCurrentThread(void* rval_ptr)
{
pthread_exit(rval_ptr);
}
pthread类型函数接口允许我们传入一个属性对象来设置线程和同步操作的属性,如设置线程是以分离状态启动还是以正常状态启动(通过函数pthread_attr_setdetachstat设置线程属性结构pthread_attr_t中的detachstat属性)。
设置线程属性的总体步骤如下:首先使用pthread_attr_init对pthread_attr_t对象进行初始化,之后通过相应函数(如pthread_attr_setstack函数设置线程的栈属性——线程栈的最小地址与栈的最小长度)设置pthread_attr_t对象中的相应属性,之后用该pthread_attr_t对象做为参数调用pthread_create函数,最后需要调用pthread_attr_destory进行释放,注意pthread_attr_destory不用等到线程结束在释放,此外应该检查该函数的返回值,因为若失败则必须销毁刚刚创建好的线程,即使这个线程已经启动。(其实pthread类型资源的用法都是这种方式,使用pthread_xxx_init初始化,调用pthread_xxx函数进行使用或设置属性,之后使用pthread_xxx_destory释放)。
并不一定每种操作系统都支持设置线程栈属性,可以在编译阶段检查宏_POSIX_THREAD_ATTR_STACKADDR和_POSIX_THREAD_ATTR_STACKSIZE。
关于同步属性的设置在第三部分进行说明。
书中对何种情况下多线程读写同一块内存会出现一致性问题做了一些探讨,但由于对计算机底层知识的匮乏而不甚了解,因此待增补相关知识后进行说明。
1)概述
互斥锁使用pthread_mutex_t数据类型表示,在使用之前必须先对锁进行初始化,对于静态分配(即在编译期完成的,静态变量与全局变量)的互斥锁可以使用PTHREAD_MUTEX_INITIALIZER进行初始化,否则通过pthread_mutex_init函数进行初始化,但若互斥锁是通过malloc等分配的,那么需要在调用pthread_mutex_destory之后释放内存。
pthread_mutex_timedlock允许设置一个阻塞时间(绝对时间,因此想要阻塞10秒则需先获取当前时间在加上10秒),当线程试图获取一个已加锁的互斥锁时,在阻塞指定时间后若仍加锁未成功则返回ETOMEOUT。
2)互斥锁属性
互斥锁属性以pthread_mutexaddr_t结构表示,包含以下三种属性类型
1)概述
读写锁有3种状态:读加锁,写加锁和不加锁。当处于读加锁状态时,任何请求读锁的线程都可以加锁成功,但是任何请求写锁的线程都会被阻塞。当处于写加锁状态时,无论是申请读锁还是写锁都将被阻塞。大部分读写锁的实现都遵循以下原则:当处于读加锁状态时,有一个线程希望获取写锁,那么读写锁通常会阻塞随后申请读锁的线程。一般用于读操作比写操作频繁很多的时候,比如读写环境变量。
【注】:我们应该谨慎使用读写锁,因为若使用不慎则会造成死锁,情况如下:在一个已经申请到读锁的函数里间接进行了某个写调用,从而申请写锁,那么这将会永远阻塞下去。
读写锁使用pthread_rwlock_init函数进行初始化与释放,使用pthread_rwlock_rdlock加读锁,pthread_rwlock_wrlock加写锁,pthread_rwlock_unlock解锁。也可以使用函数pthread_rwlock_timedrdlock进行超时限制加锁。
2)读写锁属性
读写锁的属性由pthread_rwlockattr_t进行表示,只有一种属性类型:
1)概述
条件按变量需要与互斥锁(用于保护与条件变量关联的条件,注意不要使用递归锁,否则容易死锁)配合使用,条件变量使多个线程会和于等待条件上,并允许线程以无竞争的方式等待特定条件的发生。下面我们以一个单生产者多消费者的例子来进行说明,假设在某一时刻生产者与消费者之间的缓冲队列为空,而此时有3个消费者同时希望从缓冲队列种取走数据(缓冲队列种数据的数量大于0就是等待条件),那么这三个消费者都会调用pthread_cond_wait函数,并将一个锁住的互斥锁传给该函数,这样该函数便可以安全的操控等待队列,之后便将调用线程放入该等待条件的等待线程列表上,并随互斥量进行解锁,接着下一个线程进行相同操作。随后生产者线程将数据放入缓冲队列,并对与条件变量相关的互斥锁进行加锁,并检查条件,若条件满足则将这把上锁的互斥锁传给pthread_cond_signal(或pthread_cond_broadcast)函数,该函数中进行解锁,并唤醒一个或多个线程,被唤醒的线程会在pthread_cond_wait函数中重新加锁,接着检查条件。总体思路即:在外部加锁,检查或更改变量,之后再条件变量中相关函数中进行解锁,负责唤醒的线程还要在相关函数中唤醒等待在条件变量上的被阻塞线程。muduo中对这几个函数的封装与使用如下:
// 对条件变量的封装,从中可以参悟一二STL条件变量的实现
#include
#include
#include
namespace muduo
{
class Condition : boost::noncopyable
{
public:
explicit Condition(MutexLock& mutex)
: mutex_(mutex) {
// MCHECK宏用于检查返回值,是自己实现的可以不用在意
MCHECK(pthread_cond_init(&pcond_, NULL));
}
~Condition() {
MCHECK(pthread_cond_destroy(&pcond_));
}
void wait() {
MutexLock::UnassignGuard ug(mutex_); // 这一句可以不看
MCHECK(pthread_cond_wait(&pcond_, mutex_.getPthreadMutex()));
}
// returns true if time out, false otherwise.
bool waitForSeconds(double seconds);
void notify() {
MCHECK(pthread_cond_signal(&pcond_));
}
void notifyAll() {
MCHECK(pthread_cond_broadcast(&pcond_));
}
private:
MutexLock& mutex_;
pthread_cond_t pcond_;
};
}
.h
//-----------------------------------------------------
#include
#include
#include
namespace muduo
{
class CountDownLatch : boost::noncopyable
{
public:
explicit CountDownLatch(int count);
void wait();
void countDown();
int getCount() const;
private:
mutable MutexLock mutex_;
Condition condition_;
int count_;
};
}
.cpp
//---------------------------------------------------
#include
using namespace muduo;
CountDownLatch::CountDownLatch(int count)
: mutex_(),
condition_(mutex_),
count_(count)
{
}
void CountDownLatch::wait()
{
MutexLockGuard lock(mutex_);
while (count_ > 0) // 上锁后安全的检查条件
{
condition_.wait(); //将以上锁的互斥锁传给pthread_cond_wait函数,当将调用线程放入等待线程列表后解锁
}
}
void CountDownLatch::countDown()
{
MutexLockGuard lock(mutex_);
--count_;
if (count_ == 0) // 上锁后安全的改变和检查条件,若条件满足则将锁传入pthread_cond_signal
{
condition_.notifyAll(); // 解锁并唤醒线程
}
}
int CountDownLatch::getCount() const
{
MutexLockGuard lock(mutex_);
return count_;
}
2)条件变量属性
条件变量属性由pthread_condattr_t进行表示,包括两个属性类型:
时钟属性:该属性用于设置函数pthread_cond_timedwait的超时参数使用哪种时钟。(对那几个时钟类型的区别不甚了解)
3)条件变量的虚假唤醒
POSIX规范为了简化pthread_cond_signal的实现,允许其在实现时唤醒一个以上的线程,这便可能造成虚假唤醒,即可能多个线程都被唤醒,但要获取的资源却不足(生产者消费者)。这个过程大致是这样的,当资源不足时,生产者线程本是阻塞在条件变量上的,但之后被生产者唤醒,从而尝试获取锁资源,转而阻塞在了锁上,因而两个消费者会相继获取锁,并获取资源。为了避免虚假唤醒,需要加上while。
自旋锁虽然也有着保护临界区的作用,单自旋锁不会通过休眠来阻塞进程,而是在获取锁之前一直处于忙等状态(自旋)阻塞(即不断的以轮询的方式检查锁的状态),其常用于以下情况:锁被持有的时间很短,且线程不希望陷入内核而在重新调度上耗费时间。
这种锁并不适合在应用层使用,特别是允许抢占式调用的操作系统中,因为当时间片到或有更改优先级的线程被调度时,该线程将进入睡眠,若它此时还持有着自旋锁,那么会导致其它线程仍在自旋等待占用CPU。
事实上有些互斥锁在实现时会在试图获取互斥量时自旋一小段时间,当自旋计数到达某一阈值时才会休眠。
1)概述
屏障是用于协调多个线程并行工作的同步机制,屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续,pthread_join也是一种屏障,但只允许一个线程等待另外一个线程退出。具体的使用方法参见APUE p336。
2)屏障属性
屏障属性由pthread_barrierattr_t进行表示,包括一个属性类型:
如果一个函数对多个线程来说是可重入的,那么这个函数便是线程安全的。但正如博文《信号》中所讨论的那样,可重入函数并不一定是异步信号安全的(如用锁实现的可重入,且未阻塞中断)。我们可以通过宏_POSIX_THREAD_SAVE_FUNCTIONS来判断该操作系统是否支持线程安全函数,对于那些支持的操作系统一般都会提供一些非线程安全函数的线程安全版本,这些函数一般都会在函数名后面加上"_r",如muduo中使用的gmtime_r函数。对于不保证线程安全的函数集合可以参APUE p355。
标准流本不是线程安全的,但FILE结构中有一个关联的锁(是一把可重入锁),我们可以在进行读写操作前调用函数flickfile(FILE *fp)函数获取这把锁,之后使用funlockfile函数释放这把锁。但这仍不是异步信号安全的。
【注】:对于有多把锁的情况,每个线程都应该以相同的顺序进行加锁(比如以锁的地址顺序为序)。
一个进程中的所有线程都可以访问进程的整个所属地址空间,除非使用寄存器,否则一个线程无法阻止另一个线程访问它的数据,即使是线程私有数据。但是我们又希望系统能帮我们维护基于每线程的数据。对于这个问题有以下两种方法。
pthread线程库提供了一种将数据与键值关联,之后在利用键值查询数据的接口。首先使用pthread_key_create(pthread_key_t *keyp, void(*destructor)(void*))函数在用户指定的pthread_key_t 地址空间上初始化一个键值,并与该键值关联一个用户指定的析构函数。之后通过调用pthread_setspecific(pthread_key_t key, void* value)函数将键与值关联起来,当关联后便可以通过键值作为参数调用void* pthread_getspecific(pthread_key_t key)函数来获取值。当线程调用pthread_exit或线程返回正常退出时,析构函数便会被调用。但若线程调用了exit或about或其它非正常退出,则不会调用析构函数
为了保证某个函数在进程中只被调用一次(一般是初始化函数,而且是在多线程程序中),那么可以只用int pthread_once(pthread_once_t *initflag, void(initfn)(void))函数,它可以保证initfn函数只被调用一次。pthread_once_t变量必须是一个非本地变量(如全局变量或静态变量)。
调用pthread_cancle函数可以取消指定进程ID(pid)的线程,被取消的线程就好像调用了pthread_exit(,PTHREAD_CANCLED)一样。但该线程不一定会被取消,这需要看这个线程的连两个属性:可取消状态与可取消类型。这两个并不在线程属性类型pthread_attr_t中,而是通过两个函数来进行设置的。
该属性用于表明是否响应pthread_cancle函数即是否进行取消线程。这个值有两种取值:
该属性描述了何时取消的问题:默认状态下取消类型为推迟取消,即当到达取消点时有某个取消请求被挂起,且状态为PTHREAD_CANCLE_ENABLE,则线程会被取消,调用pthread_cancle的线程也不会被阻塞等待线程取消,而是继续运行。可是当为异步取消(PTHREAD_CANCEL_ASYNCHRONOUS)时线程可以在任意时间撤销,而无需等到取消点。
每个线程都拥有独立的信号屏蔽字,但是信号的处理函数却是进程中所有线程共享的,即一个进程中对某一种信号只能有一个信号处理函数,该处理函数通过sigaction或signal设置。者也就意味着,虽然有些线程屏蔽了某些信号,但只要有一个线程未屏蔽,那么该进程就将接收到该信号。
为该进程产生的信号只递送到单个线程。一般若是某个硬件故障引起的信号,那么该信号会被发送到引起该事件的线程,而其它信号发送至任意线程(若是捕获则中断该线程,然后执行信号处理函数)。在讨论Linux内核时会更深入的讨论这信号与线程的处理。为了防止中断线程,可以把信号加到每个线程的信号屏蔽字中,然后安排一个专门的线程(不屏蔽信号)来处理信号。
【注】:在多线程程序中,某个线程的信号屏蔽字只能使用pthread_sigmask进行设置,sigprocmask函数是用于单线程程序的。
可以通过函数sigwait来等待指定信号集的到来,该函数的第二参数会返回从挂起信号集中删除的信号个数(难道不总是1???),该函数会原子的设置信号屏蔽字并进行等待。若启用了信号排队,那么该函数从中移出一个,其它的仍排队。注意,在调用sigwait之前因该先屏蔽sigwait要等待的信号,因为在线程完成对sigwait的调用之前会有一个时间段,在该段时间内信号可能就已经发送给线程了(APUE中是这样说的,但是在设置屏蔽之前信号就到了怎么办?应该一开始比如构造时先频闭掉那些信号,调用sigwait原子的设置并等待,信号到达后会自动恢复到原先的频闭状态)。
若有多个线程阻塞在sigwait上等待同一信号,那么当进程递送信号时只有一个线程可以从sigwait返回。
【思考】:使用等待信号的函数时应该在程序刚开始时(比如线程刚启动时)便先屏蔽信号,之后再调用sigwait(多线程中使用)或sigsuspend(单线程进程中使用)之类的函数原子设置屏蔽字并陷入等待。否则信号可能在等待函数调用之前便到了。当信号到达时着些等待函数会恢复信号屏蔽字。
可以调用pthread_kill函数将信号发送给某个线程,可以传送0值来测试线程是否存在。若信号的默认动作是终止进程,且未捕获也未忽略则会杀死整个进程。
关于单线程程序我们已在博文《进程控制》中给出如下列子,发现子进程会继承父进程的锁及锁的状态。
int main(int argc, char *argv[])
{
std::mutex mut;
mut.lock();
if(fork() > 0) {
// parent
std::cout<<"parent process"<
那么如果我们在多线程程序中的某个线程中调用fork会怎么样呢?测试代码如下:
void* threadStartFun(void* arg)
{
std::cout<<"thread1"< 0) {
// parent
std::cout<<"parent process"<
我们发现同样会继承锁的状态。由于在多线程程序中调用fork后子进程只存在一个线程,它是由父进程中调用fork的线程的副本所构成的,即子进程是一个单线程程序。若fork前该锁已经被另一个线程所获取,那么将子进程中将无法知道那个线程占有了那些锁,以及需要释放那些锁。但是若fork之后马上调用exec函数便不会存在问题,因为exec会丢弃旧的地址空间中的内容,锁的状态也就变的无关紧要。
由上述可知,在多线程中调用fork很难处理锁的状态。这是因为在子进程中无法获知父进程中的其它线程对锁做了什么操作,因此可以采用如下方法,在调用fork之前,现在父进程中获取所有的锁,之后再进行fork,随后在fork返回后分别在子进程和父进程中释放这些锁。pthread函数库也为我们提供了一个类似功能的函数pthread_atfork(void(*prepare)(void), void (*parent)(), void(*child)(void));该函数允许我们设置三个函数,其中prepare所指的函数由父进程在fork创建子进程之前调用,用于获取所有的锁,parent所指函数在fork创建子进程之后,返回之前由父进程调用,用于释放锁,child函数在fork之后,返回之前由子进程调用,用于释放锁。
可以用pthread_atfork函数注册多个处理程序,若注册了多次处理程序,那么应该注意其调用顺序:prepare函数的调用顺序与其注册顺序相反,而parent和child函数的调用顺序与其注册时的顺序相同。
【注】:要保证获取锁的顺序与其它线程同步,否则可能造成死锁。
当多线程中使用了条件变量时需要谨慎fork,因为有的系统中条件变量不需要做清理,而有的条件变量把锁作为了实现的一部分,可目前有没有允许清理条件变量中锁的接口(APUE中是这样说的,可是不慎了解,如3.4中所给出的代码,随人条件变量中有对锁的操作,但这把锁是外部传入的,若先获取这把锁,在进行fork会有问题码?)。