如何在不阻塞的情况下同步线程?

现在据我所知,互斥体用于同步所有共享相同数据的线程,遵循的原则是当一个线程正在使用该数据时,所有其他线程在使用该公共资源时应该被阻塞,直到它被解锁为止。在一篇博客文章中,我看到了一个解释这个概念的代码,一些人写道,在一个线程访问资源的时候阻塞所有的线程是一个非常糟糕的主意,它与线程的概念是不一致的。 那么我的问题是如何同步线程没有阻塞?

这是博客post的链接

How to Use C Mutex Lock Examples for Linux Thread Synchronization

不能通过同步的定义来阻塞同步线程。 但是,良好的同步技术会将事物被阻塞的范围限制在绝对最小值。 为了说明和准确指出文章的错误原因,请考虑以下内容:

从文章:

pthread_t tid[2]; int counter; pthread_mutex_t lock; void* doSomeThing(void *arg) { pthread_mutex_lock(&lock); unsigned long i = 0; counter += 1; printf("\n Job %d started\n", counter); for(i=0; i<(0xFFFFFFFF);i++); printf("\n Job %d finished\n", counter); pthread_mutex_unlock(&lock); return NULL; } 

它应该是什么:

 pthread_t tid[2]; int counter; pthread_mutex_t lock; void* doSomeThing(void *arg) { unsigned long i = 0; pthread_mutex_lock(&lock); counter += 1; int myJobNumber = counter; pthread_mutex_unlock(&lock); printf("\n Job %d started\n", myJobNumber); for(i=0; i<(0xFFFFFFFF);i++); printf("\n Job %d finished\n", myJobNumber); return NULL; } 

注意,在文章中,正在完成的工作(无意义的循环)是在锁定的情况下完成的。 这是完全无稽之谈,因为这项工作应该同时完成。 需要锁定的原因只是为了保护counter变量。 因此,线程只需要在更改该变量时保持锁定,如第二个示例中所示。

互斥锁保护代码的关键部分 ,即代码中一次只能处理一个线程的代码区域,如果同时尝试访问关键部分,则所有其他线程都必须被阻止。 但是,如果线程1位于临界区域,而线程2不在,那么两者同时运行就完全正常了。

你正在寻找的术语是锁定免费的数据结构 。

总的想法是线程之间共享的状态被扭曲成其中之一。

这些实现不同,通常是编译器或平台特定的。 例如MSVC有一组_Interlocked *函数来执行简单的原子操作。

当一个线程正在访问资源时阻塞所有的线程是一个非常糟糕的主意,它违背了线程的概念,这是真的

这是一个谬论。 锁只阻塞争用线程,允许所有非争用线程同时运行。 运行在任何特定时间运行效率最高的工作,而不是强制执行任何特定的排序并不违背线程的概念。

现在,如果有那么多的线程竞争如此激烈以至于阻止竞争的线程会损害性能,那么有两种可能性:

  1. 很可能你的设计很差,你应该修复它。 不要指责高争用设计的锁。

  2. 在极少数情况下,其他同步机制更适合(如无锁集合)。 但这需要重要的专业知识和具体用例的分析才能找到最佳解决方案。

一般来说,如果你的用例是一个完美的适合原子,使用它们。 否则,互斥(可能与条件变量结合)应该是你的第一个想法。 这将覆盖典型的多线程C程序员将面临的99%的情况。

您可以使用pthread_mutex_trylock()来尝试锁定。 如果失败,那么你知道你被阻止。 你不能做你想做的事,但你的线程没有被阻塞,所以它可以尝试做别的事情。 我认为这个博客上的大部分评论都是关于避免线程之间的争用,即最大化多线程性能是为了避免线程同时在同一个资源上工作。 如果你通过设计避免这种情况,那么你就不需要锁定,因为你从来没有争用过。

有许多技巧可以用来避免并发瓶颈。

  1. 不可变的数据结构。 这里的想法是并发读取可以,但写入不是。 要实现这样的事情,你基本上需要把业务单元看作是工厂,这些业务单元使用这些不可变的数据结构。
  2. 异步回调。 这是事件驱动的发展的本质。 如果您有并发任务,则在资源可用时使用观察者模式执行某些逻辑。 基本上我们执行一些代码,直到需要共享资源,然后添加一个监听器,以便资源何时可用。 这通常会导致较少的可读代码,而且堆栈上的压力太大,但是您绝不会阻塞等待资源的线程。 如果你有任务准备好保持CPU运行热,这种模式将为你做。

即使使用这些工具,也不会完全消除对某些同步的需求(计数器浮现在脑海中)。