多线程, 进程, 线程间同步

时间:2022-11-12 18:09:25

一. 线程\进程的的区别:

  1.多线程可以并行执行,并且资源是共享的,所有线程的资源都在同一块内存区,对应的全局变量所有线程都是可见的。每个线程都操作同一块内存,都可以读写申请的全局变量

多进程的话,不同的进程资源在不同的内存区,资源是独立的。每个进行都操作自己的内存块.

  2. 可以说定义变量和之前没什么区别,不过可能会多个线程异步对他进行操作,那也就存在一些同步机制而已。
  3. 在没有多线程之前,如果想做并行计算,就只能通过多进程来做了,也就是启动两个 程序做了.
  4.这时候的进程地址空间独立的,就会出现一个进程间通讯的问题,资源不共享嘛 .

 二.线程同步:

  1.进程是没有活力的,它只是一个静态的概念。为了让进程完成一些工作,进程必须至少占有一个线程,所以线程是描述进程内的执行,正是线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可以包含几个线程, 它们可以同时执行进程的地址空间中的代码。为了做到这一点,每个线程有自己的一组CPU寄存器和堆栈。

      2.线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说main或WinMain函数,将程序的启动点提供给Windows系统。主执行线程终止了,进程也就随之终止。
       3.每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通讯非常方便,多线程技术的应用也较为广泛。
      4.多线程可以实现并行处理,避免了某项任务长时间占用CPU时间。要说明的一点是,目前大多数的计算机都是单处理器(CPU)的,为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮换方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。由此可见,如果两个非常活跃的线程为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,反而会降低系统的性能。这一点在多线程编程时应该注意。
现代操作系统大都提供了相应的机制,用来处理线程的生存期、同步,以及其他“和线程有关”的属性,如优先级、线程专有存储空间(thread-specific storage)等。多线程编程是一门语言的难点和重点。
接下来让我们看看处理线程的函数:
pthread_create()函数,创建线程
函数原型如下:
  1. int pthread_create(pthread_t *restrict thread,
  2.            const pthread_attr_t *restrict attr,
  3.            void *(*start_routine)(void*), void *restrict arg);
参数讲解:
1、每个线程都有自己的ID即thread ID,可以简称tid,呵呵,是不是想起什么来了?。。。对,和pid有点象。其类型为pthread_t,pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义:
         typedef unsigned long int pthread_t;
         可以看成是线程的标志符。当成功创建一个新线程的时候,系统会为该线程分配一个tid,并将该值通过指针返回给调用它的程序。
2、attr申明线程的属性。                       
     属性结构为pthread_attr_t,它在头文件/usr/include/pthread.h中定义。设为NULL,表示在这里我们只使用线程的默认属性就可以了。
    
3、start_routine表示新创建的线程所要执行的例程。线程以调用该函数开始,直到由该函数返回(return)终止这个线程,或者在start_routine所指向的函数中调用pthread_exit函数终止。start_routine只有一个参数,该参数由随后的arg指针来指出。
4、arg:也是一个指针,也就是start_routine指针所指向的函数的参数。
 
返回值:
  当pthread_create调用成功时,该调用返回0;否则,返回一个错误代码指出错误的类型。
pthread_exit()函数
线程的终止可以是调用了pthread_exit或者该线程的例程结束。也就是说,一个线程可以隐式的退出,也可以显式的调用pthread_exit函数来退出。
函数原型如下:
  1. void pthread_exit( void * value_ptr );
线程的终止可以是调用了pthread_exit或者该线程的例程结束。也就是说,一个线程可以隐式的退出,也可以显式的调用pthread_exit函数来退出
pthread_exit函数唯一的参数value_ptr是函数的返回代码,只要pthread_join中的第二个参数value_ptr不是NULL,这个值将被传递给value_ptr
pthread_join()函数,等待一个线程终止
调用pthread_join的线程将被挂起直到参数thread所代表的线程终止时为止。pthread_join是一个线程阻塞函数,调用它的函数将一直等到被等待的线程结束为止。
函数原型如下:
  1. int pthread_join(pthread_t th, void **thread_return);
如果value_ptr不为NULL,那么线程thread的返回值存储在该指针指向的位置。该返回值可以是由pthread_exit给出的值,或者该线程被取消而返回PTHREAD_CANCELED。
当一个非分离的线程终止后,该线程的内存资源(线程描述符和栈)并不会被释放,直到有线程对它使用了pthread_join时才被释放。因此,必须对每个创建为非分离的线程调用一次pthread_join调用,以避免内存泄漏。否则当线程是可分离的,调用pthread_exit,将终止该调用线程,并释放所有资源,没有线程等待它终止。
 
至多只能有一个线程等待给定的线程终止。如果已经有一个线程在等待thread线程终止了,那么再次调用pthread_join等待同一线程的线程将返回一个错误
 
linux下多线程的实现方法:
1)使用全局变量  2)不使用全局变量   3)线程相关的数据
实例1
  1. #include <string.h>
  2. #include <pthread.h>
  3. #include <stdio.h>
  4. #define MAXLENGTH 20
  5. void another_func(const char* threadName)
  6. {
  7.     printf("%s is running in another_func\n",threadName);
  8. }
  9. void * thread_func(void *args)
  10. {
  11.     char threadName[MAXLENGTH];
  12.     strncpy(threadName,(char*)args,MAXLENGTH-1);
  13.     printf("%s is running in thread_func\n",threadName);
  14.     another_func(threadName);
  15. }
  16. int main(int argc,char* argv[])
  17. {
  18.     pthread_t pa,pb;
  19.     pthread_create(&pa, NULL, thread_func, "Thread A");
  20.     pthread_create(&pb, NULL, thread_func, "Thread B");
  21.     pthread_join(pa, NULL);
  22.     pthread_join(pb, NULL);
  23.     return 0;
  24. }
输出结果为:
Thread A is running in thread_func
Thread A is running in another_func
Thread B is running in thread_func
Thread B is running in another_func
该方法的缺点是:由于要记录是哪一个线程在调用函数,每个函数需要一个额外的参数来
记录线程的名字,例如another_func函数需要一个threadName参数
如果调用的函数多了,则每一个都需要一个这样的参数
 
实例2
  1. #include <string.h>
  2. #include <pthread.h>
  3. #define MAXLENGTH 20
  4. char threadName[MAXLENGTH];
  5. pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;
  6. void another_func ()
  7. {
  8.   printf ("%s is running in another_func\n", threadName);
  9. }
  10. void * thread_func (void * args)
  11. {
  12.   pthread_mutex_lock(&sharedMutex);
  13.   strncpy (threadName, (char *)args, MAXLENGTH-1);
  14.   printf ("%s is running in thread_func\n", threadName);
  15.   another_func ();
  16.   pthread_mutex_unlock(&sharedMutex);
  17.   
  18. }
  19. int main (int argc, char * argv[])
  20. {
  21.   pthread_t pa, pb;
  22.   pthread_create ( &pa, NULL, thread_func, "Thread A");
  23.   pthread_create ( &pb, NULL, thread_func, "Thread B");
  24.   pthread_join (pa, NULL);
  25.   pthread_join (pb, NULL);
  26. }
该方法的缺点是:由于多个线程需要读写全局变量threadName,就需要使用互斥机制
 
分析以上两种实现方法,Thread-Specific Data "线程相关的数据"的一个好处就体现出来了:
(1)"线程相关的数据"可以是一个全局变量,并且
(2)每个线程存取的"线程相关的数据"是相互独立的.
 
实例3
  1. #include <string.h>
  2. #include <pthread.h>
  3. pthread_key_t p_key;
  4. void another_func ()
  5. {
  6.   printf ("%s is running in another_func\n", (char *)pthread_getspecific(p_key));
  7. }
  8. void * thread_func (void * args)
  9. {
  10.   pthread_setspecific(p_key, args);
  11.   printf ("%s is running in thread_func\n", (char *)pthread_getspecific(p_key));
  12.   another_func ();
  13.   
  14. }
  15. int main (int argc, char * argv[])
  16. {
  17.   pthread_t pa, pb;
  18.   pthread_key_create(&p_key, NULL);
  19.   
  20.   pthread_create ( &pa, NULL, thread_func, "Thread A");
  21.   pthread_create ( &pb, NULL, thread_func, "Thread B");
  22.   pthread_join (pa, NULL);
  23.   pthread_join (pb, NULL);
  24. }
 
线程互斥
 
互斥操作,就是对某段代码或某个变量修改的时候只能有一个线程在执行这段代码,其他线程不能同时进入这段代码或同时修改该变量。这个代码或变量称为临界资源。
 
 
例如:有两个线程A和B,临界资源为X,首先线程A进入,将X置为加锁状态,在A将锁打开之前的这段时间里,如果此时恰巧线程B也欲获得X,但它发现X处于加锁状态,说明有其它线程正在执行互斥部分,于是,线程B将自身阻塞。。。线程A处理完毕,在退出前,将X解锁,并将其它线程唤醒,于是线程B开始对X进行加锁操作了。通过这种方式,实现了两个不同线程的交替操作。
 
一个互斥体永远不可能同时属于两个线程。或者处于锁定状态;或者空闲中,不属于任何一个线程。
 
实例1
  1. #include <stdio.h>
  2. #include <pthread.h>
  3. void * pthread_func_test(void * arg);
  4. pthread_mutex_t mu;
  5. int main()
  6. {
  7.     int i;
  8.     pthread_t pt;
  9.     
  10.     pthread_mutex_init(&mu,NULL); //声明mu使用默认属性,此行可以不写
  11.     pthread_create(&pt,NULL,pthread_func_test,NULL);
  12.     for(i = 0; i < 3; i++)
  13.     {
  14.         pthread_mutex_lock(&mu);
  15.         printf("主线程ID是:%lu ",pthread_self()); //pthread_self函数作用:获得当前线程的id
  16.         pthread_mutex_unlock(&mu);
  17.         sleep(1);
  18.     }
  19. }
  20. void * pthread_func_test(void * arg)
  21. {
  22.     int j;
  23.     for(j = 0; j < 3; j++)
  24.     {
  25.         pthread_mutex_lock(&mu);
  26.         printf("新线程ID是:%lu ",pthread_self());
  27.         pthread_mutex_unlock(&mu);
  28.         sleep(1);
  29.     }
  30. }
终端输出结果:
 
主线程ID是 : 3086493376
新线程ID是 : 3086490512
 
主线程ID是 : 3086493376
新线程ID是 : 3086490512
 
主线程ID是 : 3086493376
新线程ID是 : 3086490512
 
pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,都处于加锁状态中,即同一时间只能被一个线程调用执行。当另一个线程执行到pthread_mutex_lock处时,如果该锁此时被其它线程使用,那么该线程被阻塞,即程序将等待到其它线程释放此互斥锁。
pthread_mutex_init()函数  作用:初始化互斥体类型变量mutex,变量的属性由attr进行指定。attr设为NULL,即采用默认属性,这种方式与pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER的方式等价。
 
函数原型如下:
  1. int pthread_mutex_lock(pthread_mutex_t *mutex);
 
pthread_mutex_unlock()函数  作用:如果当前的线程拥有参数mutex所指定的互斥体,那么该函数调用将该互斥体解锁。
函数原型如下:
  1. int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_destroy()函数  作用:用来释放互斥体所占用的资源。
函数原型如下:
  1. int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_self()函数  作用:获得线程自身的ID。前面我们已经提到过,pthread_t的类型为unsigned long int,所以在打印的时候要使用%lu方式,否则将产生奇怪的结果。
函数原型如下:
  1. pthread_t pthread_self(void);
但是上面的代码并不完善,假设将循环次数修改得足够的长,打印后的结果可能并不是我们所希望看到的交替打印,可能象下面这样:
主线程ID是 : 3086493376
新线程ID是 : 3086490512
主线程ID是 : 3086493376
新线程ID是 : 3086490512
 
新线程ID是 : 3086490512
主线程ID是 : 3086493376
 
这是什么原因呢?因为Linux是分时操作系统,采用的是时间片轮转的方式,主线程和新线程可能因为其它因素的干扰,获得了非顺序的时间片。如果想要严格的做到“交替”方式,可以略施小计,即加入一个标志。
 
实例1
  1. #include <stdio.h>
  2. #include <pthread.h>
  3. void * pthread_func_test(void * arg);
  4. pthread_mutex_t mu;
  5. int flag = 0;
  6. int main()
  7. {
  8.     int i;
  9.     pthread_t pt;
  10.     
  11.     pthread_mutex_init(&mu,NULL);
  12.     pthread_create(&pt,NULL,pthread_func_test,NULL);
  13.     for(i = 0; i < 3; i++)
  14.     {
  15.         pthread_mutex_lock(&mu);
  16.         if(flag == 0)
  17.                 printf("主线程ID是:%lu ",pthread_self());
  18.         flag = 1;
  19.         pthread_mutex_unlock(&mu);
  20.         sleep(1);
  21.     }
  22.     pthread_join(pt, NULL);
  23.     pthread_mutex_destroy(&mu);
  24. }
  25. void * pthread_func_test(void * arg)
  26. {
  27.     int j;
  28.     for(j = 0; j < 3; j++)
  29.     {
  30.         pthread_mutex_lock(&mu);
  31.         if(flag == 1)
  32.             printf("新线程ID是:%lu ",pthread_self());
  33.         flag == 0;
  34.         pthread_mutex_unlock(&mu);
  35.         sleep(1);
  36.     }
  37. }
在使用互斥锁的过程中很有可能会出现死锁:即两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,A线程先锁定互斥锁1,B线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数pthread_mutex_trylock,该函数企图锁住一个互斥体,但不阻塞
pthread_mutex_trylock()函数 作用:函数thread_mutex_trylock()用来锁住参数mutex所指定的互斥体。如果参数mutex所指的互斥体已经被上锁,该调用不会阻塞等待互斥体的解锁,而会返回一个错误代码。通过对返回代码的判断,程序员就可以针对死锁做出相应的处理。所以在对多个互斥体编程中,尤其要注意这一点。
函数原型如下:
  1. int pthread_mutex_trylock(pthread_mutex_t *mutex);
线程同步
首先来看一下有关同步机制的概念。同步就是若干个线程等待某个事件的发生,当等待事件发生时,一起开始继续执行。可以这样简单理解同步,就是若干个线程各自对自己的数据进行处理,然后在某个点必须汇总一下数据,否则不能进行下一步的处理工作。
 
线程同步的函数调用有pthread_cond_init、pthread_cond_broadcast、pthread_cond_signal、pthread_cond_wait和pthread_cond_destroy
 
pthread_cond_init()函数  函数说明:按attr指定的属性初始化cond条件变量。如果attr为NULL,效果等同于pthread_cond_t cond = PTHREAD_COND_INITIALIZER
函数原型如下:
  1. int pthread_cond_init(pthread_cond_t *restrict cond,
  2.           const pthread_condattr_t *restrict attr);
pthread_cond_broadcast()函数 函数说明:对所有等待cond这个条件变量的线程解除阻塞。
函数原型如下:
  1. int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_signal()函数  函数说明:仅仅解除等待cond这个条件变量的某一个线程的阻塞状态。如果有若干线程挂起等待该条件变量,该调用只唤起一个线程,被唤起的线程是哪一个是不确定的。
函数原型如下:
  1. int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_wait()函数 函数说明:该调用自动阻塞发出调用的当前线程,并等待由参数cond指定的条件变量,而且为参数mutex指定的互斥体解锁。被阻塞线程直到有其他线程调用pthread_cond_signal或pthread_cond_broadcast函数置相应的条件变量时,而且获得mutex互斥体才解除阻塞。等待状态下的线程不占用CPU时间。
函数原型如下:
  1. int pthread_cond_wait(pthread_cond_t *restrict cond,
  2.        pthread_mutex_t *restrict mutex);
pthread_cond_timedwait()函数  函数说明:该函数自动阻塞当前线程等待参数cond指定的条件变量,并为参数mutex指定的互斥体解锁。被阻塞的线程被唤起继续执行的条件是:有其他线程对条件变量cond调用pthread_cond_signal函数;或有其他线程对条件变量cond调用pthread_cond_broadcast;或系统时间到达abstime参数指定的时间;除了前面三个条件中要有一个被满足外,还要求该线程获得参数mutex指定的互斥体。
函数原型如下:
  1. int pthread_cond_timedwait(pthread_cond_t *restrict cond,
  2.        pthread_mutex_t *restrict mutex,
  3.        const struct timespec *restrict abstime);
pthread_cond_destroy()函数 函数说明:释放cond条件变量占用的资源。
函数原型如下:
  1. int pthread_cond_destroy(pthread_cond_t *cond);
  1. #include <stdio.h>
  2. #include <pthread.h>
  3. pthread_t pt1,pt2;
  4. pthread_mutex_t mu;
  5. pthread_cond_t cond;
  6. int i = 1;
  7. void * decrease(void * arg)
  8. {
  9.     while(1)
  10.     {
  11.         pthread_mutex_lock(&mu);
  12.         if(++i)
  13.         {
  14.             printf("%d ",i);
  15.             if(i != 1) printf("Error ");
  16.             pthread_cond_broadcast(&cond);
  17.             pthread_cond_wait(&cond,&mu);
  18.         }
  19.         sleep(1);
  20.         pthread_mutex_unlock(&mu);
  21.     }
  22. }
  23. void * increase(void * arg)
  24. {
  25.     while(1)
  26.     {
  27.         pthread_mutex_lock(&mu);
  28.         if(i--)
  29.         {
  30.             printf("%d ",i);
  31.             if(i != 0) printf("Error ");
  32.             pthread_cond_broadcast(&cond);
  33.             pthread_cond_wait(&cond,&mu);
  34.         }
  35.         sleep(1);
  36.         pthread_mutex_unlock(&mu);
  37.     }
  38. }
  39. int main()
  40. {
  41.     pthread_create(&pt2,NULL,increase,NULL);
  42.     pthread_create(&pt1,NULL,decrease,NULL);
  43.     pthread_join(pt1,NULL);
  44.     pthread_join(pt2,NULL);
  45. }