线程 - 读书笔记

线程控制原语:

创建:

 int pthread_create(pthread_t* id, pthread_attr_t *attr, void *(*rtn)(void*), void* param); 

退出:

 主动退出:int pthread_exit(void*); 
 被动退出:int pthread_cancel(pthread_t id); 

数据收集:

 获取数据:int pthread_join(pthread_t id, void* ); 
 主动分离:int pthread_detach(pthread_t id); 
 默认情况下,线程终止后会保留状态等待pthread_join回收。但如果被detach后线程结束后不保留资源。 

 注册清理函数:void pthread_cleanup_push(void *(*rtn)(void*), void *param); 
 主动调用清除函数:void pthread_cleanup_pop(int invo); 
 被动调用清除函数:线程在 1. 调用pthread_exit(void*) 2. 被pthread_cancel(pthread_t id)时会调用由pthread_cleanup_push注册的函数。 

注意:如果在pthread_cleanup_push 和 pthread_cleanup_pop中间调用return,则清理函数不会被执行,而且会产生未定义的行为。

互斥量 mutex:

互斥量就是一把锁,只有一个线程可以加锁。

互斥量控制原语:

可以在定义互斥量时直接赋值为PTHREAD_MUTEX_INITIALIZER.来初始化锁,也可以调用pthread_mutex_init后者可以给互斥量指定属性。 互斥量不用时,需要调用pthread_mutex_destroy. 
 int pthread_mutex_init(pthread_mutex_t *mutex,  pthread_mutexattr_t *attr); 
 int phtread_mutex_destroy(pthread_mutex_t *mutex); 

  如果一个线程已经把锁锁上了,那其它要锁该互斥量的线程被阻塞,直到锁被打开。 
 int pthread_mutex_lock(pthread_mutex_t *mutex); 
 int pthread_mutex_unlock(pthread_mutex_t *mutex); 

 也可以尝试锁住互斥量,如果尝试失败会返回EBUSY. 返回0表示上锁成功,这样线程不会被阻塞。 
 int phtread_mutex_trylock(pthread_mutex_t *mutex); 

 也可以在阻塞时等待一段设置的时间,设置的时间一到函数返回ETIMEDOUT, 标志等待锁超时失败。 
 int phtread_mutex_timedlock(pthread_mutex_t *mutex, struct timespec *sp); 

防止死锁:

 1. 对于多个互斥量,在每个线程中,应该保持锁的顺序一致性。因为如果一个线程在等待另一个线程已经锁住的互斥量,而另一个线程又在等待这个线程已经锁住的互斥量,此时就会形成死锁。 

 2. 不能对一个互斥量加锁多次。第二次锁就是死了。 (递归互斥量除外)

注意: 对一把锁解锁多次,只会真正解锁一次。 如果锁的粒度太粗,就会出现很多线程阻塞等待相同的锁,这可能并不能改善并发性。如果锁的粒度太细,那么过多的锁开销就会使系统性能受到影响,而且代码变得复杂。


读写锁 - 共享互斥锁。

对变量进行访问时,线程可以同读,但不能同时写,读时不写,写时不读。如果读时写被阻塞,后面的读也被阻塞。
换个角度来看,读锁之间是朋友,写锁之间是敌人,读锁和写锁之间也是敌人。

读锁也成为共享锁,写锁成为互斥锁。

 pthread_rwlock_t 读写锁的类型。 

 int  pthread_rwlock_init(pthread_rwlock_t*, pthread_rwlockattr_t*); 
 int pthread_rwlock_destroy(pthread_rwlock_t*); 
 这些和纯正的互斥锁调用规则都差不多。所有的锁函数,只要调用成功都可以用int pthread_rwlock_unlock解锁。 
 int pthread_rwlock_rdlock(pthread_rwlock_t*); 
 int pthread_rwlock_wrlock(pthread_rwlock_t*); 

 int pthread_rwlock_tryrdlock(pthread_rwlock_t*); 
 int pthread_rwlock_trywrlock(pthread_rwlock_t*); 

 int pthread_rwlock_timedrdlock(pthread_rwlock_t*, timespec *); 
 int pthread_rwlock_timedwrlock(pthread_rwlock_t*, timespec *); 

 int pthread_rwlock_unlock(pthread_rwlock_t*); 

条件变量

条件变量是一种同步机制,条件变量有两个值true和false,true代表某一条件达成,false标识还未达成。通过给条件变量发送信号来将false变为true。变为true意味着不再等待,继续执行。
一个条件成立了,可能时数据准备好了,接下来的动作就是锁住保护数据的互斥锁准被在其上操作。等待条件达成或者说等待数据准备好,其实就是在做一件事情:

加锁解锁等待信号加锁操作解锁等待信号
等待加锁操作 - 解锁 -发送信号等待等待加锁操作 - 解锁 -发送信号

如果没有条件变量收发信号的机制,那中间的等待信号可能会变为延迟一段时间, 而延迟多长时间是个不可能确定的值。时间太长可能错过条件,延迟太短了, 另一个线程还没来得及锁呢,你就又锁上了。

 int pthread_cond_init(pthread_cond_t *, pthread_condattr_t*); 
 int pthread_cond_destroy(pthread_cond_t*); 

 wait系函数所做的事情就是先解锁互斥量,然后等待信号,退出时再锁住信号量。 
 int pthread_cond_wait(pthread_cond_t*, pthread_mutex_t*); 
 int pthread_cond_trywait(pthread_cond_t*, pthread_mutex_t*); 
 int pthread_cond_timedwait(pthread_cond_t*, pthread_mutex_t*); 

 int pthread_cond_signal(pthread_cond_t*); 
 int pthread_cond_broadcast(pthread_cond_t*); 

问题是:既然broadcast可以将信号发送给所有等待该条件的条件变量,却只有一个wait函数可以返回,如果那个返回的函数一直不解锁互斥量,那其它的线程也无法从wait退出,消息制造线程也不能再次锁定互斥量来创造下一个条件。是不是哪里理解的有问题?

自旋锁

自旋锁是一种忙等待机制,适合短时间等待的情况,这里的短时间对cpu开销小于线程休眠唤醒对cpu的开销。但是如今的系统已经将互斥量实现为先自旋锁,然后再休眠的机制,效率几乎和自旋锁相同。所以对于用户级应用来说,自旋锁的意义不再那么重要。

屏障

屏障就是我一直在找的那种同步机制。一个线程把任务分派下去然后开始等待(被屏障),剩下的线程组完后等待其它线程完成(被屏障),当屏障的线程个数等于之前设置的个数时,屏障消失。所有线程又开始执行。我那个语音broadcast就可以这样去做。

屏障消失后,并且确保没有人使用该屏蔽量等待,就可以调用destroy和init重新设置其屏障个数了。

int pthread_barrier_init(pthread_barrier_t , pthread_barrierattr_t , int count);
int pthread_barrier_destroy(pthread_barrier_t*);

int pthread_barrier_wati(pthread_barrier_t*);

线程的属性

线程的属性有一部分藏在pthread_attr_t中,还有一部分通过相关专用函数设置。
属性结构本身对用户不透明,所以不知道里面时什么样子,所以内核提供了一系列函数来修改属性值,对于线程来说它有以下几个属性:

detachstat创建时就将线程标志为分离的,线程结束后内核不保留退出状态,同时不能调用join函数pthread_attr_setdetachstate(pthread_attr_t*, int); pthread_attr_getdetachstate(pthread_attr_t*, int*);PHTREAD_CREATE_DETACHED PTHREAD_CREATE_JOINABLE
stackaddrpthread_attr_setstack(pthread_attr_t*, void* , int);pthread_attr_getstack(pthread_attr_t*, void *, int);自己定义一个栈地址给线程使用
stacksizepthread_attr_setstachsize(pthread_attr_t*, int); pthread_attr_getstacksize(pthread_attr_t&, int*);自己设置栈大小,内核自动调整.但不可小于sysconf(_SC_THREAD_STACK_MIN);的值
guardsizepthread_attr_setguardsize(pthread_attr_t*, int); pthread_attr_getguardsize(pthread_attr_t*, int*);用来保护线程栈的多出来的一些内存

线程的Cancel的状态和类型也可以设置,不过得进入到线程内部调用函数才生效。状态是说enable还是disable 如果enable了,类型才有意义:推迟的和异步的. 推迟的遇到退出点才退出,这样的退出点有很多系统函数,如果没有也可以手动调用pthread_canceltest(),如果请求了cancel,调用这个函数时就会cancel了。异步的就是不管执行到哪里了,一cancel就立马cancel,丝毫不留情面。

函数如下:

int pthread_setcancelstate(int new , int* old);
取指:PTHREAD_CANCEL_ENABLE PTHREAD_CANCEL_DISABLE
int pthread_setcanceltype(int new, int* old);
取指:PTHREAD_CANCEL_DEFERRED PTHREAD_CANCEL_ASYNCHRONOUS
void pthread_canceltest();

互斥量属性:

互斥量属性可以控制
1.互斥量是否进程间共享。
2.互斥量的类型;

类型描述
PTHREAD_MUTEX_NORMAL不进行错误检查
PTHREAD_MUTEX_RECURSIVE同一个线程可以对互斥量锁定多次,需要解锁多次才能对其它线程解锁。解锁不占用的锁返回错误。
PTHREAD_MUTEX_ERRORCHECK执行错误检查。
PTHREAD_MUTEX_DEFAULT = PTHREAD_MUTEX_NORMAL在linux下与NORMAL相同

错误检查会检查二次加锁,不占用时解锁,已解锁时解锁这三个错误条件。当然,对于RECURSIVE不会检查二次加锁错误了。

互斥量的健壮与否.

 int pthread_mutexattr_init(pthread_mutexattr_t*); 
 int pthread_mutexattr_destroy(pthread_mutexattr_t*); 

 int pthread_mutexattr_setpshared(pthread_mutexattr_t*, int); 
     PTHREAD_MUTEX_SHARED 
     PTHREAD_MUTEX_PRIVATE;
 int pthread_mutexattr_getpshared(pthread_mutexattr_t*, int*); 

 int pthread_mutexattr_settype(pthread_mutexattr_t*, int); 
     PTHREAD_MUTEX_NORMAL 
     PTHREAD_MUTEX_RECURSIVE 
     PTHREAD_MUTEX_ERRORCHECK 
     PTHREAD_MUTEX_DEFAULT 
 int pthread_mutexattr_gettype(pthread_mutexattr_t*, int*); 

 int pthread_mutexattr_setrobust(pthread_mutexattr_t* int);    PTHREAD_MUTEX_ROBUST PTHREAD_MUTEX_STALLED 
 int pthread_mutexattr_getrobust(pthread_mutexattr_t*, int*); 

robust 被设置之后,一个持有互斥锁的进程再释放该互斥锁之前结束了,系统会让等待该互斥锁的其它进程返回EOWNERDEAD,返回该错误标识的进程应该在解锁之前调用 pthread_mutex_consistent(pthread_mutex_t *); 这样等于是消除了这个互斥量上的错误信息,解锁后就可以被其它线程使用了。如果解锁前并没有调用consistent函数,其它等待该信号量的其它线程会返回ENOTRECOVERABLE,从此之后这个信号量就不能使用了。一用就会返回ENOTRECOVERABLE。

读写锁属性和条件变量属性和屏障锁属性

这三个锁的属性都是为进程间共享锁提供的,在没有看进程锁之前还是别花心思记录了。

线程和信号

• 信号由哪个线程触发,就会被发送到哪个线程中去。
• 如果是非线程触发的信号,那被发送到任意一个线程中去,可能是第一个开启的线程。
• 当线程中所有的线程都阻塞了一个信号(或者说每个线程都有自己的信号屏蔽字,哪个线程的信号屏蔽字中没有它就传给第一个找到的线程),唯独只有一个线程没有阻塞它,那么信号就会传递到那个线程去。
• 每个线程虽然都有不同的信号屏蔽字,但是所有信号处理函数都是共享的。也就是说,用signal或者sigaction设置的信号处理函数在一个线程中设置,所有线程都会设置sigprocmask对多线程的进程无定义,所以要使用:

      int pthread_sigmask(int how, sigset_t *new, sigset_t *old);
                                        SIG_BLOCK
                                        SIG_UNBLOCK
                                        SIG_SETMASK
    线程继承父线程的信号屏蔽字。所以在主线程中调用该函数设置之后,所有的线程都会继承。

      int sigwait(sigset_t*set, int *signo);    
sigwai从信号的挂起队列上取出一个信号到signo中,如果信号的挂起队列为空则阻塞。应该注意到的是,sigwait是和挂起队列交互的,所以在调用sigwait之前要让发生的信号出现在挂起队列中,所以就必须提前早早的把它们阻塞了。调用pthread_sigmask函数而不是sigprocmask,因为后者对多线程进程未定义。

如果有多个线程都调用sigwait,可以一个进程只有一个进程信号挂起队列,当出现信号时也只能有一个sigwait可以pass。

也可以有一种机制,给线程发送信号:int pthread_kill(pthread_t id, int sig);  把信号发送给线程,这个内部机制Unix高级编程并没有具体说明。

总结一下信号来源和信号接收线程之间的关系:
当信号来源是外部时:

  1. 当所有的线程都阻塞了该信号时,发送给随机的sigwait线程,只有一个。
  2. 当有遗漏的线程没有阻塞该信号,也就是该线程中的信号屏蔽字中没有该信号时,信号被发送到这些线程中的随机的一个中。

当信号来源是硬件时:

  1. 发送给引起该信号的线程。

当信号来源时pthread_kill时,发送给指定的线程。
再强调一点:信号屏蔽字每个线程都有,但信号处理函数一个进程一个,切记~

实现:

        一个进程有一个阻塞的信号队列。
        每个线程也有一个阻塞的信号队列。
        通过pthread_kill把信号发送到线程的阻塞队列。外部信号发送到进程的信号队列。
        sigwait同时去拿进程的信号队列也去拿自己所属线程的信号队列。所以在进程中的信号队列总是只有一个线程的sigwait返回。
        而发送给指定线程的信号只会被该线程的sigwait拿到。

普遍的应用场景:

可以把异步信号变成同步的,一个进程中n个线程全部阻塞要处理的信号量, 留出一个线程专门调用sigwait,然后同步的处理这些信号。哈哈哈,美哉美哉~

线程的资源绑定Key

每个线程可能拥有自己的同一类资源,比如errno每个线程都有一个,把同一类资源绑定到同一个key上,然后在不同的线程中对这同一个key执行获取数据动作可以获得不同的一类数据。同样,这些数据是之前在同一线程中设置的。key的最主要同图是在线程取消,返回,退出时会自动执行和该key绑定的析构函数,这个析构函数的参数是key所对应资源的地址。然后在该函数中再执行free等操作,一般如果key绑定的是malloc的内存,直接把free当做析构函数传递即可。如果在析构函数之外调用了free,就必须把key绑定的资源设置为null,防止二次free。

    pthread_key_t key;

    int pthread_key_create(pthread_key_t*, void (*destructor)(void*));
    int pthread_key_delete(pthread_key_t);

    int pthread_setspecific(pthread_key_t, void *);
    int pthread_getspecific(pthread_key_t);

    只执行一次的函数:
    key肯定只能被初始化一次,但是又不能知道到底时哪个线程先启动,所以不知道在哪里初始化了,假设我就是想不到在pthread_create之前初始化,pthread_once_t 被用来原子的标示一个动作是否被执行过。
    pthread_once_t once;  它必须被赋值为PTHREAD_ONCE_INIT;

    然后就可以这样执行:pthread_once(pthread_once_t *, void (*func)());
    然后这个函数不管被调用几次,也不管在同一个线程还是在不同线程,func函数只执行一次,
    如果希望再次通过pthread_once函数调用pthread_once_t指定的函数,只需要再次把pthread_once_t赋值为PTHREAD_ONCE_INIT.就可以了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值