Linux线程同步之条件变量和信号量

条件变量

        条件变量是一种重要的线程同步机制,用于协调线程间的执行顺序。结合互斥锁,条件变量能够高效地实现线程的阻塞与唤醒,避免忙等待。条件变量允许线程在某个条件未满足时主动进入阻塞状态,并释放持有的互斥锁;当其他线程修改条件并发出信号时,阻塞线程被唤醒。

            使用条件变量时,通常涉及到一个或多个线程等待“条件变量”代表的条件成立,而另外一些线程在条件成立时触发条件变量。

            条件变量的使用必须与互斥锁配合,以保证对共享资源的访问是互斥的。

            条件变量提供了一种线程间的通信机制,允许线程以无竞争的方式等待特定条件的发生。

    pthread_cond_wait函数

            函数原型为int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

            调用该方法的线程必须持有mutex锁。调用该方法的线程会阻塞并临时释放mutex锁,并等待其他线程调用pthread_cond_signal或pthread_cond_broadcast唤醒。被唤醒后该线程会尝试重新获取mutex锁。

            cond 指向条件变量的指针。条件变量用于等待某个条件的发生。通过某一cond等待的线程需要通过同一cond的signal唤醒

            mutex 与条件变量配合使用的互斥锁的指针。在调用pthread_cond_wait之前,线程必须已经获得了这个互斥锁。

            return int 成功时返回0;失败时返回错误码,而非-1。错误码可能包括EINVAL、EPERM等,具体取决于错误的性质。

    pthread_cond_timedwait函数

            函数原型为int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

            作用同pthread_cond_wait相似,但是它添加了超时机制。如果在指定的abstime时间内条件变量没有被触发,函数将返回一个超时错误(ETIMEDOUT)。

            cond 指向条件变量的指针

            mutex 与条件变量配合使用的互斥锁的指针

            abstime 指向timespec结构的指针,表示等待条件变量的绝对超时时间。timespec结构包含秒和纳秒两部分,指定了从某一固定点(如UNIX纪元,1970年1月1日)开始的时间。

            return int 成功时返回0;如果超时则返回ETIMEDOUT;其他错误情况返回相应的错误码。

    pthread_cond_signal函数

            函数原型为int pthread_cond_signal(pthread_cond_t *cond);

            作用是唤醒因cond而阻塞的线程,如果有多个线程因为cond阻塞,那么随机唤醒一个。如果没有线程在等待,这个函数什么也不做。

            cond 指向条件变量的指针

            return int 成功时返回0;失败时返回错误码

    pthread_cond_broadcast函数

            函数原型为int pthread_cond_broadcast(pthread_cond_t *cond);

            作用是唤醒所有正在等待条件变量cond的线程。如果没有线程在等待,这个函数什么也不做。

            cond 指向条件变量的指针。

            return int 成功时返回0;失败时返回错误码。

    条件变量的使用过程

            初始化(pthread_cond_init):创建并初始化条件变量,包括动态初始化和静态初始化。

            动态初始化,调用pthread_cond_init函数,当销毁时需要调用pthread_cond_destroy函数。

            静态初始化:使用PTHREAD_COND_INITIALIZER,PTHREAD_COND_INITIALIZER是POSIX线程(Pthreads)库中定义的一个宏,用于在声明时静态初始化条件变量(pthread_cond_t类型的变量)。它提供了一种简单、便捷的方式来初始化条件变量,无需调用初始化函数pthread_cond_init。

            使用PTHREAD_COND_INITIALIZER可以让条件变量在程序启动时即处于可用状态,这对于全局或静态分配的条件变量尤其有用。

            static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

            使用静态初始化的注意事项

            使用PTHREAD_COND_INITIALIZER静态初始化的条件变量通常不需要调用pthread_cond_destroy来销毁。但是,如果条件变量在程序执行期间被重新初始化(通过pthread_cond_init),那么在不再需要时应使用pthread_cond_destroy进行清理。

            PTHREAD_COND_INITIALIZER只适用于静态或全局变量的初始化。对于动态分配的条件变量(例如,通过malloc分配的条件变量),应使用pthread_cond_init函数进行初始化。

            PTHREAD_COND_INITIALIZER提供的是条件变量的默认属性。如果需要自定义条件变量的属性(例如,改变其pshared属性以支持进程间同步),则需要使用pthread_cond_init和pthread_condattr_t类型的属性对象。

              等待(pthread_cond_wait):在给定的互斥锁上等待条件变量。调用时,线程将释放互斥锁并进入等待状态,直到被唤醒。

              定时等待(pthread_cond_timedwait):等待条件变量或直到超过指定的时间。

              信号(pthread_cond_signal):唤醒至少一个等待该条件变量的线程。

              广播(pthread_cond_broadcast):唤醒所有等待该条件变量的线程。

              销毁(pthread_cond_destroy):清理条件变量资源,如果条件变量是静态初始化则不需要调用此函数

      示例

      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <pthread.h>
      
      #define handle_erro(cmd,result) \
              if(result < 0)          \
              {                       \
                  perror(cmd);        \
                  exit(EXIT_FAILURE); \
              }                       \
      
      char buf[100];
      pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
      pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
      pthread_t pthread_in_id,pthread_out_id;
      
      void *pthread_in(void *argv)
      {
          pthread_mutex_lock(&mutex);
          while (read(STDIN_FILENO,buf,100))
          {
              //唤醒输出
              pthread_cond_signal(&cond);
      
              //等待输出完成
              pthread_cond_wait(&cond,&mutex);
          }
      
          //调用取消线程函数,结束out线程
          //out线程使用的是默认属性创建的,所以是延迟取消,pthread_cond_wait函数属于取消点函数
          pthread_cancel(pthread_out_id);
      
          pthread_mutex_unlock(&mutex);
      }
      
      void *pthread_out(void *argv)
      {
          pthread_mutex_lock(&mutex);
          while (1)
          {
              //等待输入,然后输出
              pthread_cond_wait(&cond,&mutex);
              printf("%s",buf);
              //输出之后唤醒输入
              pthread_cond_signal(&cond);
          }
          
          pthread_mutex_unlock(&mutex);
      }
      
      int main(int argc, char const *argv[])
      {
          pthread_create(&pthread_out_id,NULL,pthread_out,NULL);
          pthread_create(&pthread_in_id,NULL,pthread_in,NULL);
      
          pthread_join(pthread_in_id,NULL);
          pthread_join(pthread_out_id,NULL);
          return 0;
      }
      

      运行结果

      信号量

              信号量(Semaphore)是一种广泛使用的同步机制,用于控制对共享资源的访问,主要在操作系统和并发编程领域中得到应用,用来解决多个进程或线程间的同步与互斥问题。与共享存储等不同,在Linux中,信号量是用来协调进程或线程的执行的,并不承担传输数据的职责。

              信号量本质上是一个非负整数变量,可以被用来控制对共享资源的访问。它主要用于两种目的:互斥和同步。

      互斥(Mutex):确保多个进程或线程不会同时访问临界区(即访问共享资源的代码区域)。

      同步(Synchronization):协调多个进程或线程的执行顺序,确保它们按照一定的顺序执行。

              基于不同的目的,信号量可以分为两类:用于实现互斥的“二进制信号量”和用于同步的“计数信号量”。

              二进制信号量(或称作互斥锁):其值只能是0或1,主要用于实现互斥,即一次只允许一个线程进入临界区。通常用于控制共享资源的访问,避免竞态条件的产生。

              计数信号量:其值可以是任意非负整数,表示可用资源的数量。计数信号量允许多个线程根据可用资源的数量进入临界区。通常用于控制不同进程或线程执行的顺序,如消费者必须在生产者发送数据后才可以消费。

              在Linux中,根据是否具有唯一的名称,又可以分为有名信号量(named semaphore)和无名信号量(unnamed semaphore)。这两种信号量特性有所不同:

              无名信号量:无名信号量不是通过名称标识,而是直接通过sem_t结构的内存位置标识。无名信号量在使用前需要初始化,在不再需要时应该销毁。它们不需要像有名信号量那样进行创建和链接,因此设置起来更快,运行效率也更高。

              有名信号量:有名信号量在系统范围内是可见的,可以在任意进程之间进行通信。它们通过名字唯一标识,这使得不同的进程可以通过这个名字访问同一个信号量对象。在当前Linux系统中,有名信号量在临时文件系统中的对应文件位于/dev/shm目录下,创建它们时可以像普通文件一样设置权限模式,限制不同用户的访问权限。

              信号量主要提供了两个操作:P操作和V操作。

              P操作(Proberen,尝试):也称为等待操作(wait),用于减少信号量的值。如果信号量的值大于0,它就减1并继续执行;如果信号量的值为0,则进程或线程阻塞,直到信号量的值变为非零。

              V操作(Verhogen,增加):也称为信号操作(signal),用于增加信号量的值。如果有其他进程或线程因信号量的值为0而阻塞,这个操作可能会唤醒它们。

      无名信号量

              sem_init函数

              函数原型为int sem_init(sem_t *sem, int pshared, unsigned int value);

              作用是在sem指向的地址初始化一个无名信号量。

              sem 信号量地址

              pshared 指明信号量是线程间共享还是进程间共享的

              0: 信号量是线程间共享的,应该被置于所有线程均可见的地址(如,全局变量或在堆中动态分配的变量)

              非0: 信号量是进程间共享的,应该被置于共享内存区域,任何进程只要能访问共享内存区域,即可操作进程间共享的信号量

              value 信号量的初始值

              return int 成功返回0,失败返回-1,同时errno被设置以记录错误信息

              sem_destroy函数

              函数原型为int sem_destroy(sem_t *sem);

              作用是b销毁sem指向的无名信号量

              sem 无名信号量

              return int 成功返回0,失败返回-1,并设置errno指示错误原因

              sem_post函数

              函数原型为int sem_post(sem_t *sem);

              作用是将sem指向的信号量加一,如果信号量从0变为1,且其他进程或线程因信号量而阻塞,则阻塞的进程或线程会被唤醒并获取信号量,然后继续执行。POSIX标准并未明确定义唤醒策略,具体唤醒的是哪个进程或线程取决于操作系统的调度策略。

              sem 信号量指针

              return int 成功返回0,失败则信号量的值未被修改,返回-1,并设置errno以指明错误原因

              sem_wait函数

              函数原型为int sem_wait(sem_t *sem);

              作用是将sem指向的信号量减一。如果信号量的值大于0,函数可以执行减一操作,然后立即返回,调用线程继续执行。如果当前信号量的值是0,则调用阻塞直至信号量的值大于0,或信号处理函数打断当前调用。

              sem 信号量指针

              return int 成功返回0,失败则信号量的值保持不变,返回-1,并设置errno以指明错误原因

              无名信号量用作线程间同步

      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <pthread.h>
      #include <semaphore.h>
      
      int data = 0;
      sem_t sem;
      
      void *plus_one(void *argv)
      {
          int tmp = data;
      
          sem_wait(&sem);
      
          data = tmp + 1;
      
          sem_post(&sem);
      }
      
      int main(int argc, char const *argv[])
      {
          //定义1000个线程ID
          pthread_t test_id[1000];
          //初始化信号量,线程间共享,初始值为1
          sem_init(&sem,0,1);
          //线程创建
          for (int i = 0; i < 1000; i++)
          {
              pthread_create(&test_id[i],NULL,plus_one,NULL);
          }
          //等待线程结束
          for (int i = 0; i < 1000; i++)
          {
              pthread_join(test_id[i],NULL);
          }
      
          printf("value is %d\n",data);
      
          sem_destroy(&sem);
      
          return 0;
      }
      

      运行结果为

      无名信号量用作进程间同步

      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <pthread.h>
      #include <semaphore.h>
      #include <sys/mman.h>
      #include <sys/types.h>
      #include <fcntl.h>
      
      #define handle_error(cmd,result)    \
              if(result < 0)              \
              {                           \
                  perror(cmd);            \
                  exit(EXIT_FAILURE);     \
              }                           \
      
      sem_t *sem = NULL;
      
      int main(int argc, char const *argv[])
      {
          char shm_name[] = "shm_sem";
          int shm_fd;
          shm_fd = shm_open(shm_name,O_RDWR | O_CREAT,0666);
          handle_error("shm_open",shm_fd);
      
          ftruncate(shm_fd,sizeof(int));
      
          sem = (sem_t *)mmap(NULL,sizeof(int),PROT_READ | PROT_WRITE,MAP_SHARED,shm_fd,0);
          
          //初始化信号量为进程间通信,初始化为0
          sem_init(sem,1,0);
      
          pid_t pid = 0;
      
          pid = fork();
          handle_error("fork",pid);
      
          if(pid == 0)
          {//子进程
              sleep(2);
              printf("子进程释放了信号量\n");
              sem_post(sem);
          }
          else if(pid > 0)//父进程
          {
              sem_wait(sem);
              printf("父进程等待到了子进程的信号量\n");
          }
      
          sem_destroy(sem);
      
          munmap(sem,sizeof(int));
          close(shm_fd);
          if(pid > 0)//unlink只需要执行一次
              shm_unlink(shm_name);
          return 0;
      }
      

      运行结果

               如果将子进程中的printf挪到sem_post后边,就会有概率先打印父进程的内容,因为子进程释放了信号量之后,父进程立马就开始执行了。

              无名信号量被用于进程间通信时,需要注意两点:

              ① sem_init()的第二个参数应设置为非零值,来告诉操作系统内核,这个信号量是用来进程间通信的,如果设置为0,则一个进程通过sem_post()释放的信号量无法被其它进程获取,会导致程序卡死。

              ② 信号量必须置于共享内存区域,以确保多个进程都可以访问,否则每个进程各自管理自己的信号量,后者并没有起到进程间通信的作用。

              由于共享内存对象可以被任意进程访问,因此,无名信号量实际上可以用于任意进程间的通信,而不仅限于父子进程。在非父子进程通信时,共享资源的初始化和释放要格外注意,必须按照合理的顺序进行。

      有名信号量

              有名信号量的名称形如/somename,是一个以斜线(/)打头,\0字符结尾的字符串,打头的斜线之后可以有若干字符但不能再出现斜线,长度上限为NAME_MAX-4(即251)。不同的进程可以通过相同的信号量名称访问同一个信号量。
              有名信号量通常用于进程间通信,这是因为线程间通信可以有更高效快捷的方式(全局变量等),不必“杀鸡用牛刀”。但要注意的是,正如上文提到的,可以用于进程间通信的方式通常也可以用于线程间通信。

      sem_open()函数

              函数原型为sem_t *sem_open(const char *name, int oflag,

              mode_t mode, unsigned int value);

              作用是创建或打开一个已存在的POSIX有名信号量。

              name 信号量的名称

              oflag 标记位,控制调用函数的行为。是一个或多个值或操作的结果。常用的是O_CREAT。

      O_CREAT: 如果信号量不存在则创建,指定了这个标记,必须提供mode和value

              mode 有名信号量在临时文件系统中对应文件的权限。需要注意的是,应确保每个需要访问当前有名信号量的进程都可以获得读写权限。

              value 信号量的初始值

              return sem_t* 成功则返回创建的有名信号量的地址,失败则返回SEM_FAILED,同时设置errno以指出错误原因

      sem_close()函数

              函数原型为int sem_close(sem_t *sem);

              作用是关闭对于sem指向的有名信号量的引用,每个打开了有名信号量的进程在结束时都应该关闭引用

              sem 有名信号量指针

              return int 成功返回0,失败返回-1,并设置errno以指明错误原因

      sem_unlink()函数

              函数原型为int sem_unlink(const char *name);

              作用是移除内存中的有名信号量对象,/dev/shm下的有名信号量文件会被清除。当没有任何进程引用该对象时才会执行清除操作。只应该执行一次。

              name 有名信号量的名称

              return int 成功返回0,失败返回-1,并设置errno以指明错误原因

      有名信号量用作线程间通信

      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <semaphore.h>
      #include <fcntl.h>
      #include <sys/stat.h>
      #include <pthread.h>
      
      #define handle_error(cmd,result)    \
              if(result < 0)              \
              {                           \
                  perror(cmd);            \
                  exit(EXIT_FAILURE);     \
              }                           \
      
      sem_t *sem = NULL;
      char *buf = NULL;
      pthread_t in_id,out_id;
      
      void *thread_in(void *argv)
      {
          //读取到ctrl+d之后就跳出循环
          while (read(STDIN_FILENO,buf,100))
          {
              //读取之后,释放信号量
              sem_post(sem);
          }
          //跳出循环后准备结束程序,调用线程关闭函数,关闭另外一个线程
          pthread_cancel(out_id);
      }
      
      void *thread_out(void *argv)
      {
          while (1)
          {
              //获取信号量,等待读取
              //当被调用线程终止函数之后,sem_wait就是一个取消点函数,线程关闭
              sem_wait(sem);
              printf("%s",buf);
          }
      }
      
      int main(int argc, char const *argv[])
      {
          char sem_name[] = "/mysem";
      
          //指定初始值为0
          sem = sem_open(sem_name,O_RDWR | O_CREAT,0666,0);
          handle_error("sem_open",sem);
      
          buf = malloc(100);
      
          pthread_create(&in_id,NULL,thread_in,NULL);
          pthread_create(&out_id,NULL,thread_out,NULL);
      
      
          pthread_join(in_id,NULL);
          pthread_join(out_id,NULL);
      
          free(buf);
      
          sem_close(sem);
          sem_unlink(sem_name);
          return 0;
      }
      

      运行结果为

      有名信号量用作进程间通信 

      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <semaphore.h>
      #include <sys/stat.h>
      #include <sys/wait.h>
      #include <sys/types.h>
      #include <pthread.h>
      #include <fcntl.h>
      #include <sys/mman.h>
      
      #define handle_error(cmd,result)    \
              if(result < 0)              \
              {                           \
                  perror(cmd);            \
                  exit(EXIT_FAILURE);     \
              }                           \
      
      int main(int argc, char const *argv[])
      {
          //注意命名格式,以/开头,之后不能再包含/
          char sem_name[] = "/mysem";
          char shm_name[] = "/myshm";
          char *buf;
          int shm_fd = 0;
      
          shm_fd = shm_open(shm_name,O_RDWR | O_CREAT,0666);
      
          ftruncate(shm_fd,100);
          //由于父子进程之间的变量,内存是继承的,并不是共享,所以需要将buf用在共享内存中
          buf = mmap(NULL,100,PROT_READ | PROT_WRITE,MAP_SHARED,shm_fd,0);
      
          sem_t *sem = NULL;
          sem = sem_open(sem_name,O_RDWR | O_CREAT,0666,0);
      
          pid_t pid = 0;
      
          pid = fork();
          handle_error("fork",pid);
      
          if(pid == 0)
          {
              //子进程
              if (read(STDIN_FILENO,buf,100))
                  sem_post(sem);
              
          }
          else if(pid > 0)
          {
              sem_wait(sem);
      
              printf("%s",buf);
      
              waitpid(pid,NULL,0);
              
          }
      
          //父子进程都需要关闭文件描述符和解除映射
          munmap(buf,100);
          sem_close(sem);
      
          //unlink只需要执行一次
          if(pid > 0)
          {
              sem_unlink(sem_name);
              shm_unlink(shm_name);
          }
              
          
      
      
          return 0;
      }
      

      运行结果

      信号量总结

              可用于进程间通信的方式通常都可以用于线程间通信。

              无名信号量和有名信号量均可用于进程间通信,有名信号量是通过唯一的信号量名称在操作系统中唯一标识的。无名信号量用于进程间通信时必须将信号量存储在进程间可以共享的内存区域,作为内存地址直接在进程间共享。而内存区域的共享是通过内存共享对象的唯一名称来实现的。

              无名信号量和有名信号量都可以作为二进制信号量和计数信号量使用。

              二进制信号量和计数信号量的区别在于前者起到了互斥锁的作用,而后者起到了控制进程或线程执行顺序的作用。而不仅仅是信号量取值范围的差异。

              信号量是用来协调进程或线程协同工作的,本身并不用于传输数据。通常,从编码复杂度和效率的角度考虑,进程间通信使用有名信号量,线程间通信使用无名信号量。

              信号量用于跨进程通信时,要格外注意共享资源的创建和释放顺序,避免资源泄露或在不恰当的时机释放资源从而导致未定义行为。

      评论 4
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

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

      抵扣说明:

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

      余额充值