[置顶] 主线程和子线程的同步控制

时间:2022-10-19 18:07:58

一个线程的结束有两种途径,一种是象我们下面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。另外需要说明的是,一个线程不能被多个线程等待,也就是说对一个线程只能调用一次pthread_join,否则只有一个能正确返回,其他的将返回ESRCH 错误。在Linux中,默认情况下是在一个线程被创建后,必须使用此函数对创建的线程进行资源回收,但是可以设置Threads attributes来设置当一个线程结束时,直接回收此线程所占用的系统资源,详细资料查看Threads attributes。
范例:
//signaltest.c
  // 子线程阻塞,等待信号,然后输出字符串
  // 主线程从键盘录入字符,给子线程发信号。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556 #include<stdio.h>#include<unistd.h>#include<signal.h>#include<pthread.h>#include<time.h>pthread_ttid; sigset_tset;voidmyfunc(){ printf("hello\n");}staticvoid*mythread(void*p){ intsignum; while(1){ sigwait(&set,&signum); if(SIGUSR1==signum) myfunc(); if(SIGUSR2==signum) { printf("Iwillsleep2secondandexit\n"); sleep(2); break; }}}intmain(){chartmp;void*status;sigemptyset(&set);sigaddset(&set,SIGUSR1);sigaddset(&set,SIGUSR2);sigprocmask(SIG_SETMASK,&set,NULL);pthread_create(&tid,NULL,mythread,NULL);while(1){printf(":");scanf("%c",&tmp);if('a'==tmp){pthread_kill(tid,SIGUSR1);//发送SIGUSR1,打印字符串。}elseif('q'==tmp){//发出SIGUSR2信号,让线程退出,如果发送SIGKILL,线程将直接退出。pthread_kill(tid,SIGUSR2);//等待线程tid执行完毕,这里阻塞。pthread_join(tid,&status);printf("finish\n");break;}elsecontinue;}return0;}
运行结果:// 如果输入a,子线程打印"hello",主程序继续等待输入;// 如果输入q,主程序等待子程序结束。子线程打印"I will sleep 2 second and exit",并延时两秒后结束。主线程随之打印"finish",程序结束。在前面我们提到,可以通过pthread_join()函数来使主线程阻塞等待其他线程退出,这样主线程可以清理其他线程的环境。但是还有一些线程,更喜欢自己来清理退出的状态,他们也不愿意主线程调用pthread_join来等待他们。我们将这一类线程的属性称为detached。如果我们在调用pthread_create()函数的时候将属性设置为NULL,则表明我们希望所创建的线程采用默认的属性,也就是joinable。如果需要将属性设置为detached,则参考下面的例子:
12345678910111213141516 void*start_run(void*arg){//dosomework} intmain(){pthread_tthread_id;pthread_attr_tattr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);pthread_create(&thread_id,&attr,start_run,NULL);pthread_attr_destroy(&attr);sleep(5);exit(0);}
在线程设置为joinable后,可以调用pthread_detach()使之成为detached。但是相反的操作则不可以。还
  有,如果线程已经调用pthread_join()后,则再调用pthread_detach()则不会有任何效果。

互斥量是一种特殊的变量,它可以处于锁定状态(locked),也可以处于解锁状态(unlocked)。如果互斥量是锁定的,那么必然有一个线程持有或拥有这个互斥量。如果没有任何一个线程持有这个互斥量,那么这个互斥量就处于解锁、空闲或可用状态(这三种状态有区别?!)。当互斥量空闲,并且有一个线程试图获取这个互斥量时,这个线程就可以获得这个互斥量而不会被阻塞。如果互斥量处于锁定状态,那么试图获取这个互斥量的线程将被阻塞,并加入到这个互斥量的等待队列中。等待队列中的线程获得互斥量的顺序由系统决定。这样的机制解决了共享资源的互斥(Mutual Exclusive)访问问题。

创建并初始化一个互斥量

POSIX使用pthread_mutex_t类型的变量来表示互斥量。程序在使用pthread_mutex_t变量之前,必须对其进行初始化。

对于静态分配的pthread_mutex_t变量,只要将PTHREAD_MUTEX_INITIALIZER赋给这个变量即可,如:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

对于动态创建或不使用默认属性的互斥量来说,就要调用pthread_mutex_init函数来对其进行初始化,此函数的形式如下:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

静态初始化通常比pthread_mutex_init更有效,而且可以在定义为全局变量时即完成初始化,这样可以保证在任何线程开始执行之前,初始化即已完成。

销毁一个互斥量

当不再使用应经定义了的互斥量时,需要将互斥量销毁。函数pthread_mutex_destroy用于销毁互斥量。它的形式为:

int pthread_mutex_destory(pthread_mutex_t *mutex);

参数mutex指向要销毁的互斥量。如果成功,函数返回0...

可以用pthread_mutex_init()重新初始化被销毁的互斥量。

对互斥量的锁定和解锁

POSIX中有两个可以用来获取互斥量的函数,pthread_mutex_lock()和pthread_mutex_trylock()。pthread_mutex_lock()函数会使调用这个函数的线程移植阻塞到互斥量可用为止,而pthread_mutex_trylock()会立即返回,如果互斥量空闲,那么调用这个函数的线程将获得互斥量,否则返回EBUSY。pthread_mutex_unlock()用来释放互斥量。

这三个函数的形式如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

若成功,都是返回0。

互斥量应用实例

由于在大多数机器中,对变量的增量和减量操作都不是原子的。比如通常情况下,增量操作包括三个步骤:将内存中数值装载到CPU寄存器中,将寄存器的值加1,将寄存器中的值写回内存。而机器并不能保证这三步之间不会发生调度,这就导致对变量的增量操作和减量操作有可能得不到期望的结果...

<span style="font-size:18px;">int increase(int *integer)
{
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int error;
if (error = pthread_mutex_lock(&lock);
return error;
*integer++;
return pthread_mutex_unlock(&lock);
}
int decrease(int *integer)
{
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int error;
if (error = pthread_mutex_lock(&lock);
return error;
*integer--;
return pthread_mutex_unlock(&lock);
}</span>
对于互斥量,我有一些体会:

体会1:互斥量只是一种手段。如何理解呢?它是用来保护某个共享资源的,对互斥量的使用重在“自觉”,比如有三个线程one、two、three,二者都需要处理一个全局变量share,共享一个互斥变量mutex,程序员需要做的是,不管在哪里?不管对于哪个线程,在写变量share时,都要获取(调用pthread_mutex_lock函数)互斥量mutex,然后才开始写,写完之后再释放(pthread_mutex_unlock函数)mutex,如此这般,才能保证share只被一个人写;但若有些线程“不听话”,比如线程three,它不这样做,不进行神马pthread_mutex_lock()、pthread_mutex_unlock()操作,它也能操作share,但是这样就违背了程序设计的初衷。互斥量只是一种手段,使用全屏自觉。

体会2:体会1中讲到的是对share的“写操作”进行锁定,但这不是绝对的,很多时候要是具体情况而定,有时要求“读”“写”都锁定,有时只要求“写”锁定...


条件变量,先谈谈自己的理解吧!

互斥量解决了不同线程处理共享资源的问题,比如有俩线程one和two以及和一个共享资源share,one和two每次处理share的前提都是成功获取互斥量mutex,这样的做法保证了共享资源在一段时间里只被一个线程处理,也即保证了处理共享资源的原子性。

但这还不够,常常会有这样的应用需求(mark nb),线程one处理完共享资源share之后,需要通知处于阻塞状态的two线程来接着进行处理。单是互斥量能够解决这样的问题吗?

显然不行,单是使用互斥量,若不计one线程和two线程处理程序的复杂性,那么one线程和two线程处理share的机会是均等的,但是,mark nb这样的应用需求下,明显one线程对共享资源share的占用更有优势。举例,one线程获取共享资源share,two线程把共享资源share处理成share_after:

<span style="font-size:18px;">one thread:
  while (1) {
  if (ok) {
  pthread_mutex_lock(&mutex);
  ...// got share
  pthread_mutex_unlock(&mutex);
  }
  }
two thread:
  while (1) {
  pthread_mutex_lock(&mutex);
  ...// let share -> share_after
  pthread_mutex_unlock(&mutex);
  }</span>
这种只是使用到了互斥量的程序设计合格吗?

显然不合格!这样会造成这样的问题产生,线程one还没成功获取到新的share,线程two就已经有机会对“老的”share进行处理了。而事实上,我们需要的one线程每“if (ok)”一次,two才有机会执行一次。

如何能满足这样的需求呢?

条件变量!

关于对条件变量的解释,有文献曰:条件变量是用来通知共享数据的状态信息的机制。

在这种机制下,能够实现这样的功能:线程two平时都是处于阻塞状态,直到线程one在if(ok)中激活它,使它有机会执行一次,下一个while循环又重新处于阻塞状态,等待线程one下一次的“召唤”。

来看看如何用代码完成这样的机制吧!值得一提的是,由于涉及共享数据,因此条件变量是结合互斥量来使用的。

创建和销毁条件变量

POSIX用pthread_cond_t类型的变量来表示条件变量。程序必须在使用pthread_cond_t变量之前对其进行初始化。

对于那些静态分配的、使用默认属性的pthread_cond_t变量来说,可以直接将PTHREAD_COND_INITIALIZER赋给变量就可以完成初始化。如下:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

对那些动态分配的或不使用默认属性的变量来说,就要调用pthread_cond_init函数来进行初始化。该函数的形式为:

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); 

参数attr是一个条件变量属性对象,如果将NULL传递给attr,则初始化一个具有默认属性的条件变量,否则,就要用与线程属性对象类似的方式,先创建一个条件变量属性对象,再设置它。

函数pthread_cond_destory销毁一个条件变量,该函数的形式为:

int pthread_cond_destory(pthread_cond_t *cond);

暂时就介绍到这里吧,说得再多还不如看看代码示例。

基于《4.Linux C多线程的执行顺序问题》进行修改,现在要做这么一件事情,线程thread_one和线程thread_two共同处理一个全局变量i,thread_one:++i和打印i,thread_two处理的事情是:打印i。thread_one和thread_two都处理打印i的事务,所不同的是,当i为3的倍数的时候,由thread_two打印,否则由thread_one打印。

程序如下:

<span style="font-size:18px;">#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
static void thread_one(char* msg);
static void thread_two(char* msg);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int i = 1;
int temp;
int main(int argc, char** argv)
{
pthread_t th_one, th_two;
char * msg = "thread";
printf("thread_one starting\n");
if (pthread_create(&th_one, NULL, (void*)&thread_one, msg) != 0) {
exit(EXIT_FAILURE);
}
printf("thread_two starting\n");
if (pthread_create(&th_two, NULL, (void*)&thread_two, msg) != 0) {
exit(EXIT_FAILURE);
}
pthread_join(th_one, NULL);
pthread_join(th_two, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
printf("Main thread is going over!\n");
return 0;
}
static void thread_one(char* msg)
{
int j = 0;
while (i < 10) {
pthread_mutex_lock(&mutex);
if (i % 3 == 0) {
temp = i;
i++;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
usleep(10);// 为了让thread_two有足够执行时间,10ms
} else {
printf("I am one. loop %d\n", i);
i++;
pthread_mutex_unlock(&mutex);
}
}
printf("one is over!\n");
}
static void thread_two(char* msg)
{
int j = 0;
while (i < 10) {
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
printf("I am two. loop %d\n", temp);
}
printf("two is over!\n");
}</span>
编译执行结果如下:

root@book-desktop:/opt/pc_test/multithreading/t# ./main 

thread_one starting

thread_two starting

I am one. loop 1

I am one. loop 2

I am two. loop 3

I am one. loop 4

I am one. loop 5

I am two. loop 6

I am one. loop 7

I am one. loop 8

I am two. loop 9

two is over!

one is over!

Main thread is going over!

先来分析“pthread_cond_wait(&cond, &mutex);”

可以说是pthread_cond_wait()是条件变量机制中的最重要也是最难分析的函数。

使用条件变量机制直观上比较简单:调用pthread_cond_wait()的线程把自个儿给阻塞起来,然后等待别的线程调用pthread_cond_signal()把它唤醒。

pthread_cond_wait(&cond,&mutex)操作有两步,是原子操作:第一步是解锁,先解除之前的pthread_mutex_lock()锁定的mutex;第二步是挂起,阻塞并在等待对列里休眠,即线程thread_two挂起,直到被线程thread_one再次被唤醒,唤醒的条件是由“pthread_cond_signal(&cond);”发出的cond信号来唤醒。(这段话来自网络,总感觉有些问题,但我得明白pthread_mutex_lock()涉及两次mutex操作:上锁和解锁)

值得注意的是,pthread_cond_wait函数的使用方法如下:

pthread_mutex_lock(&mutex); // step a

pthread_cond_wait(&cond, &mutex); // step b

pthread_mutex_unlock(&mutex); // step c

step a比较容易理解了,step c如何理解呢?

这得深刻解剖pthread_cond_wait()了,先看线程thread_two是如何进入阻塞状态的:

a.解锁(解除step a锁定的mutex,如此使得与之共享锁的线程譬如thread_one能够拥有该mutex)、等待(阻塞睡眠之类的,此阻塞发生在pthread_cond_wait函数内部)。

b....等待,直到pthread_cond_signal()函数唤醒之。

c.此时被唤醒的线程上下文仍然在pthread_cond_wait函数内部,此时加锁,使得mutex被线程thread_two重新拥有,然后处理返回值之类的。

所以上面的step a、b、c可以解剖为如下这般:

a------- pthread_mutex_lock(&mutex);

b1------- pthread_mutex_unlock(&mutex);

睡眠............唤醒

b2------- pthread_mutex_lock(&mutex);

c------- pthread_mutex_unlock(&mutex);

理解“pthread_cond_wait(&cond, &mutex);”有两个关键:

1.pthread_cond_wait函数涉及两次mutex操作;

2.pthread_cond_wait函数并不是1次原子操作,线程阻塞于此函数内部,同样线程也是在此函数内部被唤醒。

接着来分析“pthread_cond_signal(&cond);”

关于pthread_cond_signal(),网文曰:

“pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行。如果没有线程处在阻塞等待状态,pthread_cond_signal()也会成功返回。”

“使用pthread_cond_signal函数不会有‘惊群现象’产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal()最多发信号一次。”

需要注意的是pthread_cond_signal函数不涉及mutex的操作,所以在thread_one里调用pthread_cond_signal函数,thread_two只能说被唤醒,但此时mutex还未被释放掉,也即thread_two的程序还不能被执行,直到thread_one释放掉mutex为止。

上文已经提到,对于pthread_cond_wait()的使用方法,大概如下:

pthread_mutex_lock(&mutex); // step a

pthread_cond_wait(&cond, &mutex); // step b

pthread_mutex_unlock(&mutex); // step c

事实上,若需要在thread_two中处理共同资源,最好如下般这样:

pthread_mutex_lock(&mutex); // step a

pthread_cond_wait(&cond, &mutex); // step b

...处理共享数据 // step c

pthread_mutex_unlock(&mutex); // step d

同样,对于pthread_cond_signal()的使用方法,也可以如下般这样:

pthread_mutex_lock(&mutex); // step a

pthread_cond_signal(&cond, &mutex); // step b

...处理共享数据 // step c

pthread_mutex_unlock(&mutex); // step d

本文程序并不是一个多么好的条件变量的应用实例,但通过它足以体会条件变量的使用方法~


参考:

http://baike.baidu.com/link?url=_PeSXdcSw05Twq3JlP7ldzbifo6X8C6YaE7ki1yGNNssnFEW4YWvYeWj0n6uPOTx1sp4-N9EJok5NJFg9yWDQ_

http://baike.baidu.com/link?url=k2z4MrF9QFSw1yK5EiyOWBtqQ5HQGjz8O_yqRQ7Hib9pBm64HwLZ-Zva4IBflqkiarHa9ykWB3nIrGzNUEFF0K

http://m.blog.csdn.net/blog/sadjason/9706353


相关文章