For asynchronous or polling access, the better approach is to serialize your hardware access into a single thread for a given bus. This thread should run at relatively high priority and establish the "time-base" for your application control loop
If your devices are synchronous, i.e. provide an interrupt or ready signal (typical for ADC), then it is preferable to have an IRQ per device. You start the devices running with some time offset to guarantee that you don't have too many interrupts-during-interrupt.
If you have completely isolated or very low priority (diagnostic) inputs, you can probably get away with the naive multi-thread approach, but your application should be robust enough to handle jitter and missed signals
Underlying your communication scheme is a single physical bus device that is embedded in the CPU. This device cannot be simultaneously used by multiple threads at the same time to put bits on the wire.
If the kernel driver you are using locks that device, either due to real timing requirements (e.g. fixed delay for bus/device) or due to implementation inefficiency well then you are out of luck. Your user space code will block
ioctl calls to the driver until the previous interaction has finished.
In a polling based approach, using multiple threads is problematic, you have no timing or scheduling guarantees, so it is quite likely that your threads will end up blocking each other anyway, using much more time than manually sequencing it . Using locks in time sensitive code is a recipe for always missing your timing requirements.
Locks are used for protection, if you are constantly thrashing a lock then your system is very poorly controlled, using locks for resource scheduling will not give you the resolution in time that you want.
but it means that the memory consumption increases with each connected sensor (I believe each thread allocates around 1mb of memory) which seems pointless (and stupid) for such a simple task.
Yes, that is part of the trade-off. Is it "stupid" and "pointless"? Well it depends on your requirements...
A thread adds much more than just memory usage. Every time you have a context switch from one thread to another, that may be up to several milliseconds of wasted CPU time in the kernel to handle the context switch. In time sensitive applications this is much more crucial than memory.
In any case, this 1MB usage is likely not true , I suspect you are misinterpreting shared memory or caching reporting. An empty pthread should much smaller than that. In either case this isn't that much, these threads make up your primary control/poll loop, they should be respected more...
Obviously this means a lot of locking/unlocking and other multithreading drawbacks. I believe this multithreaded approach is not very efficient because each additional sensor means spawning an additional thread.
True, however the downside is not the extra thread, but that you cannot use the same bus at the same time in two places, regardless of how many threads you have.
Your multithreaded approach is basically out of laziness. You do not wish to design a robust polling loop so you are asking the kernel, to do its best to schedule your threads and you use locks to resolve resource conflicts when the scheduler screw up. Of course this is less efficient than a serial approach.
However, it is simpler (easier), but can cause unintended side effects, the scheduler/kernel is not always doing what you think it is doing.
The initial idea was that the task of reading sensor values simultaneously was solved long before the development of multithreading in its current form (a simple user space API).
In a sense yes, and you are already taking advantage of this solution. The peripherals that access the bus are integrated on die, they are also usually
DMA Enabled and individually clocked , so they do not require CPU cycles to perform the task (the kernel is not bit-banging).
However, there is no general solution to simultaneously access multiple external devices in the way you describe. Most of this is handled by the bus protocol, which may allow multiplexing, messaging/mailbox and other high level protocols for more convenient sequencing and multiplxing of data.
With a simple serial bus like One-Wire, the solution is either a carefully crafted poll loop, using dedicated interrupts, or some other specific scheme that can be applied to a given system.
In addition, this is somehow solved by the kernel drivers, for which the thread as a concept does not exist.
It is "solved" in the sense that the kernel drivers will try their best to respect what you ask, however you cannot send two different voltage levels on the same bus at the same time, so the "solution" is just heavy handed concurency control which breaks your timing requirements.
Also, kernel absolutely has internal threads and a concept of a thread, it may not be a posix thread as you know it, but in terms of multiple control/execution flows it exists. For example, register an ISR with an interrupt in a driver and the IRQ has its own kernel thread/task associated with it.